@@ -25,6 +25,7 @@ use tauri_plugin_autostart::{MacosLauncher, ManagerExt};
2525static WINDOW_VISIBLE : AtomicBool = AtomicBool :: new ( false ) ;
2626
2727// When true, cancel the pending hide scheduled on Focused(false) (e.g. user is resizing).
28+ #[ cfg( target_os = "windows" ) ]
2829static CANCEL_PENDING_HIDE : AtomicBool = AtomicBool :: new ( false ) ;
2930
3031/// On Windows with decorations: false, the OS adds ~16×9 to inner size to get outer.
@@ -117,6 +118,24 @@ async fn connect(state: tauri::State<'_, Arc<AppState>>) -> Result<CommandResult
117118 let _ = config. save ( ) ;
118119 }
119120
121+ // Spawn keepalive task to prevent idle connection drops (routers/TVs often close idle sockets)
122+ let state_keepalive = state. inner ( ) . clone ( ) ;
123+ tauri:: async_runtime:: spawn ( async move {
124+ let mut interval = tokio:: time:: interval ( std:: time:: Duration :: from_secs ( 25 ) ) ;
125+ interval. set_missed_tick_behavior ( tokio:: time:: MissedTickBehavior :: Skip ) ;
126+ loop {
127+ interval. tick ( ) . await ;
128+ let mut tv = state_keepalive. tv . lock ( ) . await ;
129+ if !tv. connected {
130+ break ;
131+ }
132+ if let Err ( e) = tv. keepalive_ping ( ) . await {
133+ log:: warn!( "Keepalive failed, connection dropped: {}" , e) ;
134+ break ;
135+ }
136+ }
137+ } ) ;
138+
120139 Ok ( result)
121140}
122141
@@ -371,21 +390,18 @@ async fn reset_window_size(
371390 config. window_size = None ;
372391 config. save ( ) ?;
373392 }
374- // Use default size from tauri.conf.json (app.windows[0])
393+ // Use default size from tauri.conf.json (app.windows[0]). Use Logical size so the window
394+ // has the same apparent size on all displays (e.g. Retina 2x vs 1x); Physical would make
395+ // the window look tiny on high-DPI Macs.
375396 let ( width, height) = app
376397 . config ( )
377398 . app
378399 . windows
379400 . first ( )
380- . map ( |w| ( w. width as u32 , w. height as u32 ) )
381- . unwrap_or ( ( 330 , 520 ) ) ;
401+ . map ( |w| ( w. width as f64 , w. height as f64 ) )
402+ . unwrap_or ( ( 375.0 , 525.0 ) ) ;
382403 if let Some ( window) = app. get_webview_window ( "main" ) {
383- // set_size sets the inner (content) size. get_window_size returns outer_size(), so on
384- // Windows the OS adds a few pixels for the undecorated frame (e.g. 400×550 → 416×559).
385- let _ = window. set_size ( tauri:: Size :: Physical ( tauri:: PhysicalSize {
386- width,
387- height,
388- } ) ) ;
404+ let _ = window. set_size ( tauri:: Size :: Logical ( tauri:: LogicalSize { width, height } ) ) ;
389405 }
390406 Ok ( ( ) )
391407}
@@ -460,6 +476,38 @@ async fn set_action_shortcuts(
460476 Ok ( ( ) )
461477}
462478
479+ /// Run an action by id (used for global shortcuts so they work when window is hidden).
480+ async fn run_action_impl ( state : Arc < AppState > , action_id : & str ) -> Result < ( ) , String > {
481+ let mut tv = state. tv . lock ( ) . await ;
482+ match action_id {
483+ "up" => tv. send_button ( "UP" ) . await . map ( |_| ( ) ) ,
484+ "down" => tv. send_button ( "DOWN" ) . await . map ( |_| ( ) ) ,
485+ "left" => tv. send_button ( "LEFT" ) . await . map ( |_| ( ) ) ,
486+ "right" => tv. send_button ( "RIGHT" ) . await . map ( |_| ( ) ) ,
487+ "enter" => tv. send_button ( "ENTER" ) . await . map ( |_| ( ) ) ,
488+ "back" => tv. send_button ( "BACK" ) . await . map ( |_| ( ) ) ,
489+ "volume_up" => tv. volume_up ( ) . await . map ( |_| ( ) ) ,
490+ "volume_down" => tv. volume_down ( ) . await . map ( |_| ( ) ) ,
491+ "mute" => tv. set_mute ( true ) . await . map ( |_| ( ) ) ,
492+ "unmute" => tv. set_mute ( false ) . await . map ( |_| ( ) ) ,
493+ "power_off" => tv. power_off ( ) . await . map ( |_| ( ) ) ,
494+ "home" => tv. send_button ( "HOME" ) . await . map ( |_| ( ) ) ,
495+ "power_on" => {
496+ drop ( tv) ;
497+ let config = state. config . lock ( ) . await ;
498+ let ( _, tv_config) = config. get_active_tv ( ) . ok_or ( "No TV configured" ) ?;
499+ let mac = tv_config
500+ . mac
501+ . as_ref ( )
502+ . ok_or ( "MAC address not saved" ) ?
503+ . clone ( ) ;
504+ drop ( config) ;
505+ tv:: wake_on_lan ( & mac) . map ( |_| ( ) )
506+ }
507+ _ => Ok ( ( ) ) ,
508+ }
509+ }
510+
463511/// Modifier key names (case-insensitive). Global hotkeys must include at least one
464512/// so they don't capture keys during normal typing.
465513const GLOBAL_MODIFIERS : & [ & str ] = & [ "ctrl" , "control" , "alt" , "shift" , "super" , "command" , "meta" ] ;
@@ -532,11 +580,23 @@ fn register_all_global_shortcuts(app: &AppHandle) -> Result<(), String> {
532580 }
533581 } ;
534582 let action_id_emit = action_id. clone ( ) ;
583+ let action_id_run = action_id. clone ( ) ;
535584 let app_handle = app. clone ( ) ;
536- if let Err ( e) = manager. on_shortcut ( shortcut, move |_app , _shortcut, event| {
585+ if let Err ( e) = manager. on_shortcut ( shortcut, move |app , _shortcut, event| {
537586 if event. state != ShortcutState :: Released {
538587 return ;
539588 }
589+ // Run action in Rust so it works when window is hidden
590+ if let Some ( state) = app. try_state :: < Arc < AppState > > ( ) {
591+ let state = state. inner ( ) . clone ( ) ;
592+ let action_id = action_id_run. clone ( ) ;
593+ tauri:: async_runtime:: spawn ( async move {
594+ if let Err ( e) = run_action_impl ( state, & action_id) . await {
595+ log:: warn!( "Global shortcut action {} failed: {}" , action_id, e) ;
596+ }
597+ } ) ;
598+ }
599+ // Also emit to frontend so UI can update when window is visible
540600 if let Some ( window) = app_handle. get_webview_window ( "main" ) {
541601 let _ = window. emit ( "run-command" , & action_id_emit) ;
542602 }
@@ -620,7 +680,7 @@ fn main() {
620680 None :: < Vec < & str > > ,
621681 ) ) ;
622682
623- let app = builder
683+ let mut app = builder
624684 . manage ( state. clone ( ) )
625685 . setup ( |app| {
626686 // Hide window on startup - we're a tray app
@@ -639,7 +699,7 @@ fn main() {
639699
640700 // Handle window events
641701 let window_clone = window. clone ( ) ;
642- let app_handle = app. app_handle ( ) . clone ( ) ;
702+ let _app_handle = app. app_handle ( ) . clone ( ) ;
643703 window. on_window_event ( move |event| {
644704 match event {
645705 // On Windows, clicking X sends CloseRequested and destroys the window
@@ -657,7 +717,7 @@ fn main() {
657717 {
658718 CANCEL_PENDING_HIDE . store ( false , Ordering :: SeqCst ) ;
659719 let w = window_clone. clone ( ) ;
660- let a = app_handle . clone ( ) ;
720+ let a = _app_handle . clone ( ) ;
661721 std:: thread:: spawn ( move || {
662722 std:: thread:: sleep ( std:: time:: Duration :: from_millis ( 200 ) ) ;
663723 if !CANCEL_PENDING_HIDE . load ( Ordering :: SeqCst ) {
0 commit comments