Skip to content

Commit ea44eb7

Browse files
committed
Fix global hotkeys, fix size on different dpi displays, added keep alive ping
1 parent 778ea30 commit ea44eb7

File tree

7 files changed

+85
-17
lines changed

7 files changed

+85
-17
lines changed

.github/workflows/build-tray.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ jobs:
4949
with:
5050
workspaces: './lgtv-tray-remote/src-tauri -> target'
5151

52-
- name: Update version (single source: tag → Cargo.toml, tauri.conf.json, flake.nix)
52+
- name: "Update version (single source: tag → Cargo.toml, tauri.conf.json, flake.nix)"
5353
shell: bash
5454
run: |
5555
VERSION="${{ inputs.version }}"

lgtv-tray-remote/flake.nix

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@
7777

7878
packages.default = pkgs.rustPlatform.buildRustPackage {
7979
pname = "lgtv-tray-remote";
80-
version = "1.1.0";
80+
version = "1.1.1";
8181

8282
src = ./.;
8383

lgtv-tray-remote/src-tauri/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "lgtv-tray-remote"
3-
version = "1.1.0"
3+
version = "1.1.1"
44
description = "Cross-platform system tray remote for LG webOS TVs"
55
authors = [""]
66
edition = "2024"

lgtv-tray-remote/src-tauri/src/main.rs

Lines changed: 73 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ use tauri_plugin_autostart::{MacosLauncher, ManagerExt};
2525
static 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")]
2829
static 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.
465513
const 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) {

lgtv-tray-remote/src-tauri/src/tv.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -362,6 +362,14 @@ impl TvConnection {
362362
Ok(CommandResult::ok_with_message("TV powered off"))
363363
}
364364

365+
/// Lightweight keepalive to prevent idle connection drops.
366+
/// Sends a minimal SSAP request; if it fails, connection is marked disconnected.
367+
pub async fn keepalive_ping(&mut self) -> Result<(), String> {
368+
self.send_command("ssap://com.webos.service.connectionmanager/getinfo", None)
369+
.await
370+
.map(|_| ())
371+
}
372+
365373
pub async fn get_network_info(&mut self) -> Result<Value, String> {
366374
// Get MAC addresses from getinfo endpoint
367375
self.send_command("ssap://com.webos.service.connectionmanager/getinfo", None).await

lgtv-tray-remote/src-tauri/tauri.conf.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"$schema": "https://schema.tauri.app/config/2",
33
"productName": "LG TV Remote",
4-
"version": "1.1.0",
4+
"version": "1.1.1",
55
"identifier": "com.lgtv.remote",
66
"build": {
77
"frontendDist": "../src"

lgtv-tray-remote/sync-version.sh

100644100755
File mode changed.

0 commit comments

Comments
 (0)