diff --git a/packages/tauri-app/src-tauri/capabilities/main-window.json b/packages/tauri-app/src-tauri/capabilities/main-window.json index c51d1f33..61d6e346 100644 --- a/packages/tauri-app/src-tauri/capabilities/main-window.json +++ b/packages/tauri-app/src-tauri/capabilities/main-window.json @@ -3,15 +3,14 @@ "identifier": "main-window-native-dialogs", "description": "Grant the main window access to required core features and native dialog commands.", "remote": { - "urls": [ - "http://127.0.0.1:*", - "http://localhost:*" - ] + "urls": ["http://127.0.0.1:*", "http://localhost:*"] }, "windows": ["main"], "permissions": [ "core:default", + "core:menu:default", "dialog:allow-open", - "opener:allow-default-urls" + "opener:allow-default-urls", + "core:webview:allow-set-webview-zoom" ] } diff --git a/packages/tauri-app/src-tauri/gen/schemas/capabilities.json b/packages/tauri-app/src-tauri/gen/schemas/capabilities.json index 7a25c24f..c98bf3f4 100644 --- a/packages/tauri-app/src-tauri/gen/schemas/capabilities.json +++ b/packages/tauri-app/src-tauri/gen/schemas/capabilities.json @@ -1 +1 @@ -{"main-window-native-dialogs":{"identifier":"main-window-native-dialogs","description":"Grant the main window access to required core features and native dialog commands.","remote":{"urls":["http://127.0.0.1:*","http://localhost:*"]},"local":true,"windows":["main"],"permissions":["core:default","dialog:allow-open","opener:allow-default-urls"]}} \ No newline at end of file +{"main-window-native-dialogs":{"identifier":"main-window-native-dialogs","description":"Grant the main window access to required core features and native dialog commands.","remote":{"urls":["http://127.0.0.1:*","http://localhost:*"]},"local":true,"windows":["main"],"permissions":["core:default","core:menu:default","dialog:allow-open","opener:allow-default-urls","core:webview:allow-set-webview-zoom"]}} \ No newline at end of file diff --git a/packages/tauri-app/src-tauri/src/main.rs b/packages/tauri-app/src-tauri/src/main.rs index 08e85de1..e92cce9d 100644 --- a/packages/tauri-app/src-tauri/src/main.rs +++ b/packages/tauri-app/src-tauri/src/main.rs @@ -4,7 +4,7 @@ mod cli_manager; use cli_manager::{CliProcessManager, CliStatus}; use serde_json::json; -use tauri::menu::Menu; +use tauri::menu::{MenuBuilder, MenuItem, SubmenuBuilder}; use tauri::plugin::{Builder as PluginBuilder, TauriPlugin}; use tauri::webview::Webview; use tauri::{AppHandle, Emitter, Manager, Runtime, Wry}; @@ -84,8 +84,81 @@ fn main() { Ok(()) }) .invoke_handler(tauri::generate_handler![cli_get_status, cli_restart]) - .on_menu_event(|_app_handle, _event| { - // No menu items defined currently + .on_menu_event(|app_handle, event| { + match event.id().0.as_str() { + // File menu + "new_instance" => { + if let Some(window) = app_handle.get_webview_window("main") { + let _ = window.emit("menu:newInstance", ()); + } + } + "close" => { + if let Some(window) = app_handle.get_webview_window("main") { + let _ = window.close(); + } + } + "quit" => { + app_handle.exit(0); + } + + // View menu + "reload" => { + if let Some(window) = app_handle.get_webview_window("main") { + let _ = window.eval("window.location.reload()"); + } + } + "force_reload" => { + if let Some(window) = app_handle.get_webview_window("main") { + let _ = window.eval("window.location.reload(true)"); + } + } + "toggle_devtools" => { + if let Some(window) = app_handle.get_webview_window("main") { + window.open_devtools(); + } + } + + "toggle_fullscreen" => { + if let Some(window) = app_handle.get_webview_window("main") { + let _ = window.set_fullscreen(!window.is_fullscreen().unwrap_or(false)); + } + } + + // Window menu + "minimize" => { + if let Some(window) = app_handle.get_webview_window("main") { + let _ = window.minimize(); + } + } + "zoom" => { + if let Some(window) = app_handle.get_webview_window("main") { + let _ = window.maximize(); + } + } + + // App menu (macOS) + "about" => { + // TODO: Implement about dialog + println!("About menu item clicked"); + } + "hide" => { + if let Some(window) = app_handle.get_webview_window("main") { + let _ = window.hide(); + } + } + "hide_others" => { + // TODO: Hide other app windows + println!("Hide Others menu item clicked"); + } + "show_all" => { + // TODO: Show all app windows + println!("Show All menu item clicked"); + } + + _ => { + println!("Unhandled menu event: {}", event.id().0); + } + } }) .build(tauri::generate_context!()) .expect("error while building tauri application") @@ -118,8 +191,77 @@ fn main() { } fn build_menu(app: &AppHandle) -> tauri::Result<()> { - // Minimal empty menu for now (Tauri v2 menu API differs from v1 roles). - let menu = Menu::new(app)?; + let is_mac = cfg!(target_os = "macos"); + + // Create submenus + let mut submenus = Vec::new(); + + // App menu (macOS only) + if is_mac { + let app_menu = SubmenuBuilder::new(app, "CodeNomad") + .text("about", "About CodeNomad") + .separator() + .text("hide", "Hide CodeNomad") + .text("hide_others", "Hide Others") + .text("show_all", "Show All") + .separator() + .text("quit", "Quit CodeNomad") + .build()?; + submenus.push(app_menu); + } + + // File menu - create New Instance with accelerator + let new_instance_item = MenuItem::with_id( + app, + "new_instance", + "New Instance", + true, + Some("CmdOrCtrl+N") + )?; + + let file_menu = SubmenuBuilder::new(app, "File") + .item(&new_instance_item) + .separator() + .text(if is_mac { "close" } else { "quit" }, if is_mac { "Close" } else { "Quit" }) + .build()?; + submenus.push(file_menu); + + // Edit menu with predefined items for standard functionality + let edit_menu = SubmenuBuilder::new(app, "Edit") + .undo() + .redo() + .separator() + .cut() + .copy() + .paste() + .separator() + .select_all() + .build()?; + submenus.push(edit_menu); + + // View menu + let view_menu = SubmenuBuilder::new(app, "View") + .text("reload", "Reload") + .text("force_reload", "Force Reload") + .text("toggle_devtools", "Toggle Developer Tools") + .separator() + + .separator() + .text("toggle_fullscreen", "Toggle Full Screen") + .build()?; + submenus.push(view_menu); + + // Window menu + let window_menu = SubmenuBuilder::new(app, "Window") + .text("minimize", "Minimize") + .text("zoom", "Zoom") + .build()?; + submenus.push(window_menu); + + // Build the main menu with all submenus + let submenu_refs: Vec<&dyn tauri::menu::IsMenuItem<_>> = submenus.iter().map(|s| s as &dyn tauri::menu::IsMenuItem<_>).collect(); + let menu = MenuBuilder::new(app).items(&submenu_refs).build()?; + app.set_menu(menu)?; Ok(()) } diff --git a/packages/tauri-app/src-tauri/tauri.conf.json b/packages/tauri-app/src-tauri/tauri.conf.json index 1f66ec67..e847379d 100644 --- a/packages/tauri-app/src-tauri/tauri.conf.json +++ b/packages/tauri-app/src-tauri/tauri.conf.json @@ -27,7 +27,8 @@ "fullscreen": false, "decorations": true, "theme": "Dark", - "backgroundColor": "#1a1a1a" + "backgroundColor": "#1a1a1a", + "zoomHotkeysEnabled": true } ], "security": { diff --git a/packages/ui/src/App.tsx b/packages/ui/src/App.tsx index 187bd683..f300f48b 100644 --- a/packages/ui/src/App.tsx +++ b/packages/ui/src/App.tsx @@ -1,4 +1,4 @@ -import { Component, Show, createMemo, createEffect, createSignal } from "solid-js" +import { Component, Show, createMemo, createEffect, createSignal, onMount, onCleanup } from "solid-js" import { Dialog } from "@kobalte/core/dialog" import { Toaster } from "solid-toast" import AlertDialog from "./components/alert-dialog" @@ -14,6 +14,7 @@ import { useCommands } from "./lib/hooks/use-commands" import { useAppLifecycle } from "./lib/hooks/use-app-lifecycle" import { getLogger } from "./lib/logger" import { initReleaseNotifications } from "./stores/releases" +import { runtimeEnv } from "./lib/runtime-env" import { hasInstances, isSelectingFolder, @@ -247,6 +248,28 @@ const App: Component = () => { getActiveSessionIdForInstance: activeSessionIdForInstance, }) + // Listen for Tauri menu events + onMount(() => { + if (runtimeEnv.host === "tauri") { + const tauriBridge = (window as { __TAURI__?: { event?: { listen: (event: string, handler: (event: { payload: unknown }) => void) => Promise<() => void> } } }).__TAURI__ + if (tauriBridge?.event) { + let unlistenMenu: (() => void) | null = null + + tauriBridge.event.listen("menu:newInstance", () => { + handleNewInstanceRequest() + }).then((unlisten) => { + unlistenMenu = unlisten + }).catch((error) => { + log.error("Failed to listen for menu:newInstance event", error) + }) + + onCleanup(() => { + unlistenMenu?.() + }) + } + } + }) + return ( <>