33// Responsibilities:
44// - Orchestrate the entire application lifecycle.
55// - Initialize logging (via Tauri plugin), the Echo scheduler,
6- // ApplicationState, and ApplicationRunTime.
6+ // ApplicationState, and ApplicationRunTime.
77// - Serve assets via http://localhost (determined by portpicker) to support
8- // Service Workers.
8+ // Service Workers.
99// - Parse command-line arguments to open a workspace.
1010// - Bootstrap native command registration.
1111// - Set up the Vine gRPC server and spawn the Cocoon sidecar process.
1212// - Create and customize the main Tauri application window.
1313// - Manage the main application event loop and graceful shutdown.
14+ // - [NEW] Manage System Tray and native OS integration events.
1415//
1516// Logging strategy:
1617// - Release default: Info (low noise) unless RUST_LOG overrides.
2021//
2122// NOTE (Webview logs):
2223// - To see Rust logs in the Webview console, enable TargetKind::Webview and
23- // call attachConsole() in the frontend.
24+ // call attachConsole() in the frontend.
2425
2526//! # Mountain Binary Entry Point
2627//!
@@ -38,7 +39,15 @@ use std::{
3839
3940use Echo :: Scheduler :: SchedulerBuilder :: SchedulerBuilder ;
4041use log:: { LevelFilter , debug, error, info, trace, warn} ;
41- use tauri:: { AppHandle , Manager , RunEvent , Wry } ;
42+ use tauri:: {
43+ AppHandle ,
44+ Manager ,
45+ RunEvent ,
46+ Wry ,
47+ image:: Image ,
48+ menu:: { MenuBuilder , MenuItem } ,
49+ tray:: { MouseButton , TrayIconBuilder , TrayIconEvent } ,
50+ } ;
4251use tauri_plugin_log:: { RotationStrategy , Target , TargetKind , TimezoneStrategy } ;
4352
4453use crate :: {
@@ -92,6 +101,109 @@ async fn MountainGetWorkbenchConfiguration(
92101 Ok ( Config )
93102}
94103
104+ /// Dynamically switches the tray icon based on the theme (Light/Dark).
105+ /// Can be invoked from the frontend when the theme changes.
106+ #[ tauri:: command]
107+ fn SwitchTrayIcon ( App : AppHandle , IsDarkMode : bool ) {
108+ debug ! ( "[UI] [Tray] Switching icon. IsDarkMode: {}" , IsDarkMode ) ;
109+
110+ const DARK_ICON_BYTES : & [ u8 ] = include_bytes ! ( "../icons/32x32.png" ) ;
111+
112+ const LIGHT_ICON_BYTES : & [ u8 ] = include_bytes ! ( "../icons/32x32.png" ) ;
113+
114+ let IconBytes = if IsDarkMode { DARK_ICON_BYTES } else { LIGHT_ICON_BYTES } ;
115+
116+ if let Some ( Tray ) = App . tray_by_id ( "tray" ) {
117+ match Image :: from_bytes ( IconBytes ) {
118+ Ok ( IconImage ) => {
119+ if let Err ( e) = Tray . set_icon ( Some ( IconImage ) ) {
120+ error ! ( "[UI] [Tray] Failed to set icon: {}" , e) ;
121+ }
122+ } ,
123+ Err ( e) => error ! ( "[UI] [Tray] Failed to load icon bytes: {}" , e) ,
124+ }
125+ } else {
126+ warn ! ( "[UI] [Tray] Tray with ID 'tray' not found." ) ;
127+ }
128+ }
129+
130+ // =============================================================================
131+ // Tray Initialization Logic
132+ // =============================================================================
133+
134+ /// Configures and builds the system tray with menu and event handling.
135+ fn EnableTray ( Application : & mut tauri:: App ) -> Result < ( ) , Box < dyn std:: error:: Error > > {
136+ let Handle = Application . handle ( ) ;
137+
138+ // Create menu items
139+ let OpenItem = MenuItem :: with_id ( Handle , "open" , "Open Mountain" , true , None :: < & str > ) ?;
140+
141+ let HideItem = MenuItem :: with_id ( Handle , "hide" , "Hide Mountain" , true , None :: < & str > ) ?;
142+
143+ let Separator = tauri:: menu:: PredefinedMenuItem :: separator ( Handle ) ?;
144+
145+ let QuitItem = MenuItem :: with_id ( Handle , "quit" , "Quit" , true , None :: < & str > ) ?;
146+
147+ // Build menu structure
148+ let TrayMenu = MenuBuilder :: new ( Handle )
149+ . item ( & OpenItem )
150+ . item ( & HideItem )
151+ . item ( & Separator )
152+ . item ( & QuitItem )
153+ . build ( ) ?;
154+
155+ // Load initial icon (Defaulting to 32x32 from your icons folder)
156+ let IconBytes = include_bytes ! ( "../icons/32x32.png" ) ;
157+
158+ let TrayIconImage = Image :: from_bytes ( IconBytes ) ?;
159+
160+ // Build the Tray
161+ TrayIconBuilder :: with_id ( "tray" )
162+ . icon ( TrayIconImage )
163+ . menu ( & TrayMenu )
164+ . tooltip ( "Mountain" )
165+ // Handle Menu Item Clicks
166+ . on_menu_event ( |AppHandle , Event | match Event . id . as_ref ( ) {
167+ "open" => {
168+ if let Some ( Window ) = AppHandle . get_webview_window ( "main" ) {
169+ let _ = Window . show ( ) ;
170+
171+ let _ = Window . set_focus ( ) ;
172+
173+ }
174+ } ,
175+ "hide" => {
176+ if let Some ( Window ) = AppHandle . get_webview_window ( "main" ) {
177+ let _ = Window . hide ( ) ;
178+
179+ }
180+ } ,
181+ "quit" => AppHandle . exit ( 0 ) ,
182+ _ => warn ! ( "[UI] [Tray] Unhandled menu item: {:?}" , Event . id) ,
183+ } )
184+ // Handle Native Tray Events (Left Click to Toggle)
185+ . on_tray_icon_event ( |Tray , Event | {
186+ if let TrayIconEvent :: Click { button : MouseButton :: Left , .. } = Event {
187+ let App = Tray . app_handle ( ) ;
188+
189+ if let Some ( Window ) = App . get_webview_window ( "main" ) {
190+ if Window . is_visible ( ) . unwrap_or ( false ) {
191+ let _ = Window . hide ( ) ;
192+ } else {
193+ let _ = Window . show ( ) ;
194+
195+ let _ = Window . set_focus ( ) ;
196+ }
197+ }
198+ }
199+ } )
200+ . build ( Application ) ?;
201+
202+ info ! ( "[UI] [Tray] System tray enabled successfully." ) ;
203+
204+ Ok ( ( ) )
205+ }
206+
95207// =============================================================================
96208// Binary Entrypoint
97209// =============================================================================
@@ -281,7 +393,9 @@ pub fn Fn() {
281393 . plugin ( tauri_plugin_localhost:: Builder :: new ( ServerPort )
282394 . on_request ( |_, Response | {
283395 Response . add_header ( "Access-Control-Allow-Origin" , "*" ) ;
396+
284397 Response . add_header ( "Access-Control-Allow-Methods" , "GET, POST, OPTIONS, HEAD" ) ;
398+
285399 Response . add_header ( "Access-Control-Allow-Headers" , "Content-Type, Authorization, Origin, Accept" ) ;
286400 } )
287401 . build ( ) )
@@ -304,6 +418,17 @@ pub fn Fn() {
304418
305419 TraceStep ! ( "[Lifecycle] [Setup] AppHandle acquired." ) ;
306420
421+ // ---------------------------------------------------------
422+ // [UI] [Tray] Initialize System Tray
423+ // ---------------------------------------------------------
424+ debug ! ( "[UI] [Tray] Initializing system tray..." ) ;
425+
426+ if let Err ( Error ) = EnableTray ( Application ) {
427+ error ! ( "[UI] [Tray] Failed to enable tray: {}" , Error ) ;
428+
429+ // We do not crash the app if tray fails, but we log it.
430+ }
431+
307432 // ---------------------------------------------------------
308433 // [Lifecycle] [Commands] Bootstrap native commands
309434 // ---------------------------------------------------------
@@ -437,6 +562,7 @@ pub fn Fn() {
437562 ) ;
438563
439564 ScanPathsGuard . push ( LocalPath ) ;
565+
440566 }
441567 }
442568
@@ -493,6 +619,7 @@ pub fn Fn() {
493619 // [IPC] Command routing
494620 // ---------------------------------------------------------------------
495621 . invoke_handler ( tauri:: generate_handler![
622+ SwitchTrayIcon ,
496623 MountainGetWorkbenchConfiguration ,
497624 Command :: TreeView :: GetTreeViewChildren ,
498625 Command :: LanguageFeature :: MountainProvideHover ,
@@ -535,10 +662,8 @@ pub fn Fn() {
535662 RunTime . inner ( ) . clone ( ) . Shutdown ( ) . await ;
536663
537664 info ! ( "[Lifecycle] [Shutdown] ApplicationRunTime stopped." ) ;
538-
539665 } else {
540666 error ! ( "[Lifecycle] [Shutdown] ApplicationRunTime not found." ) ;
541-
542667 }
543668
544669 debug ! ( "[Lifecycle] [Shutdown] Stopping Echo scheduler..." ) ;
@@ -555,6 +680,7 @@ pub fn Fn() {
555680
556681 ApplicationHandleClone . exit ( 0 ) ;
557682 } ) ;
683+
558684 }
559685 } ) ;
560686
0 commit comments