From 2d23beca5e90e31870d2fdf8e722acb02dce816a Mon Sep 17 00:00:00 2001 From: Caitlin Wilks Date: Sun, 18 Oct 2026 23:43:23 +0100 Subject: [PATCH 1/2] Keep the computer awake while something plays Flex draws mpv's picture itself, so mpv's own screensaver blocking never applied. Videos now keep the screen and computer on, music keeps the computer on; pausing or stopping lets it sleep again. Windows uses SetThreadExecutionState, macOS caffeinate, Linux systemd-inhibit. --- crates/flex-app/Cargo.toml | 2 +- crates/flex-app/src/awake.rs | 107 ++++++++++++++++++++++++++++++++++ crates/flex-app/src/main.rs | 1 + crates/flex-app/src/shared.rs | 10 +++- 4 files changed, 118 insertions(+), 2 deletions(-) create mode 100644 crates/flex-app/src/awake.rs diff --git a/crates/flex-app/Cargo.toml b/crates/flex-app/Cargo.toml index a11d96d..5229420 100644 --- a/crates/flex-app/Cargo.toml +++ b/crates/flex-app/Cargo.toml @@ -21,7 +21,7 @@ ureq = "3" winit = { version = "0.30", default-features = false } [target."cfg(windows)".dependencies] -windows-sys = { version = "0.61.2", features = ["Win32_UI_WindowsAndMessaging", "Win32_UI_Input_KeyboardAndMouse", "Win32_Foundation", "Win32_Storage_FileSystem", "Win32_UI_Input", "Win32_System_Diagnostics_Debug", "Win32_System_Kernel", "Win32_System_LibraryLoader", "Win32_System_Com", "Win32_System_Registry", "Win32_System_Threading"] } +windows-sys = { version = "0.61.2", features = ["Win32_UI_WindowsAndMessaging", "Win32_UI_Input_KeyboardAndMouse", "Win32_Foundation", "Win32_Storage_FileSystem", "Win32_UI_Input", "Win32_System_Diagnostics_Debug", "Win32_System_Kernel", "Win32_System_LibraryLoader", "Win32_System_Com", "Win32_System_Registry", "Win32_System_Threading", "Win32_System_Power"] } [target."cfg(target_os = \"linux\")".dependencies] # Remote buttons egui doesn't report (media keys), read from /dev/input. diff --git a/crates/flex-app/src/awake.rs b/crates/flex-app/src/awake.rs new file mode 100644 index 0000000..f8f576f --- /dev/null +++ b/crates/flex-app/src/awake.rs @@ -0,0 +1,107 @@ +//! Keeps the computer from sleeping while something plays. mpv would do this for its own +//! window, but Flex draws mpv's picture itself, so it has to ask for itself. + +/// What playback needs kept on. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum Need { + /// A video: the screen and the computer. + Screen, + /// Music: only the computer; the screen may go off. + System, +} + +/// Holds the request while playback needs it. Dropping it lets the computer sleep again. +#[derive(Default)] +pub struct KeepAwake { + held: Option<(Need, imp::Hold)>, + failed: bool, +} + +impl KeepAwake { + /// Asks for `need` (or lets go with `None`); only does anything when it changes. + pub fn set(&mut self, need: Option) { + if self.held.as_ref().map(|(n, _)| *n) == need { + return; + } + // Let go of the old one first: on Windows they're the same switch. + self.held = None; + let Some(need) = need else { return }; + match imp::Hold::new(need) { + Ok(hold) => self.held = Some((need, hold)), + // Once is enough in the log; this runs at every play and pause. + Err(e) if !self.failed => { + self.failed = true; + log::warn!("couldn't keep the computer awake while playing: {e}"); + } + Err(_) => {} + } + } +} + +#[cfg(windows)] +mod imp { + use super::Need; + use windows_sys::Win32::System::Power::{ES_CONTINUOUS, ES_DISPLAY_REQUIRED, ES_SYSTEM_REQUIRED, SetThreadExecutionState}; + + /// Windows' own setting for media players. It belongs to the thread that set it, which + /// is always Flex's UI thread. + pub struct Hold; + + impl Hold { + pub fn new(need: Need) -> Result { + let display = if need == Need::Screen { ES_DISPLAY_REQUIRED } else { 0 }; + let previous = unsafe { SetThreadExecutionState(ES_CONTINUOUS | ES_SYSTEM_REQUIRED | display) }; + if previous == 0 { Err("Windows refused".into()) } else { Ok(Hold) } + } + } + + impl Drop for Hold { + fn drop(&mut self) { + unsafe { SetThreadExecutionState(ES_CONTINUOUS) }; + } + } +} + +#[cfg(unix)] +mod imp { + use super::Need; + use std::process::{Child, Command, Stdio}; + + /// A helper that holds the request for as long as it runs: macOS's `caffeinate`, or + /// systemd's `systemd-inhibit` elsewhere. + pub struct Hold(Child); + + impl Hold { + pub fn new(need: Need) -> Result { + let mut cmd = command(need); + // If Flex dies without stopping it, the helper goes too: `caffeinate -w` watches + // Flex, and `cat` ends when Flex's end of its stdin closes. + cmd.stdin(Stdio::piped()).stdout(Stdio::null()).stderr(Stdio::null()); + let name = cmd.get_program().to_string_lossy().into_owned(); + cmd.spawn().map(Hold).map_err(|e| format!("{name}: {e}")) + } + } + + #[cfg(target_os = "macos")] + fn command(need: Need) -> Command { + let mut cmd = Command::new("caffeinate"); + cmd.arg(if need == Need::Screen { "-di" } else { "-i" }).args(["-w", &std::process::id().to_string()]); + cmd + } + + #[cfg(not(target_os = "macos"))] + fn command(need: Need) -> Command { + // "idle" also stops logind blanking the screen; music only needs "sleep". + let what = if need == Need::Screen { "idle:sleep" } else { "sleep" }; + let mut cmd = Command::new("systemd-inhibit"); + cmd.args([&format!("--what={what}"), "--who=Flex", "--why=Playing", "--mode=block", "cat"]); + cmd + } + + impl Drop for Hold { + fn drop(&mut self) { + let _ = self.0.kill(); + let _ = self.0.wait(); + } + } +} diff --git a/crates/flex-app/src/main.rs b/crates/flex-app/src/main.rs index 68731fd..8199004 100644 --- a/crates/flex-app/src/main.rs +++ b/crates/flex-app/src/main.rs @@ -3,6 +3,7 @@ mod ambient; mod app; +mod awake; #[cfg(windows)] mod autostart; mod images; diff --git a/crates/flex-app/src/shared.rs b/crates/flex-app/src/shared.rs index 9cd5794..d0cefc6 100644 --- a/crates/flex-app/src/shared.rs +++ b/crates/flex-app/src/shared.rs @@ -341,6 +341,8 @@ pub struct Shared { playback_sent: Option, /// The latest playback seen (with its library file), for where a stop happened. playback_last: Option<(Playback, Option<(i64, i64)>)>, + /// Stops the computer sleeping while something plays. + awake: crate::awake::KeepAwake, pub youtube: crate::youtube::YouTube, /// Bumped when something is removed from Continue Watching, so Home refreshes. pub continue_version: u64, @@ -390,6 +392,7 @@ impl Shared { ambient, playback_sent: None, playback_last: None, + awake: Default::default(), youtube: crate::youtube::YouTube::new(ctx), continue_version: 0, sync: None, @@ -1828,7 +1831,8 @@ impl Shared { } } - /// Handles plugin messages, tells plugins about playback and updates lights. + /// Handles plugin messages, tells plugins about playback, keeps the computer awake + /// while something plays and updates lights. /// `playing`: what the top screen is playing, if anything, and the library file. pub fn poll_plugins(&mut self, playing: Option<(Playback, Option<(i64, i64)>)>) { if let Some(web) = &self.web { @@ -1842,6 +1846,10 @@ impl Shared { let youtube = self.youtube.tools.is_some() && self.youtube_enabled(); web.set_status(flex_web::remote::Status { playing: now_playing, youtube }); } + let need = playing.as_ref().filter(|(p, _)| p.state == PlaybackState::Playing).map(|(p, _)| { + if p.kind == "video" { crate::awake::Need::Screen } else { crate::awake::Need::System } + }); + self.awake.set(need); let mut wait = self.plugins.poll(&self.db); let plugin_toasts = self.plugins.take_toasts(); self.toasts.extend(plugin_toasts); From 74ea14b515c75797e76763b7e34c7eae7f712cf6 Mon Sep 17 00:00:00 2001 From: Caitlin Wilks Date: Sun, 18 Oct 2026 23:43:40 +0100 Subject: [PATCH 2/2] Version 0.16.1 --- Cargo.lock | 14 +++++++------- Cargo.toml | 2 +- crates/flex-app/Cargo.toml | 6 +++--- crates/flex-core/Cargo.toml | 2 +- crates/flex-web/Cargo.toml | 2 +- plugins/anilist/Cargo.toml | 2 +- plugins/anilist/plugin.toml | 2 +- plugins/lotus-lamp/Cargo.toml | 2 +- plugins/lotus-lamp/plugin.toml | 2 +- plugins/trakt/Cargo.toml | 2 +- plugins/trakt/plugin.toml | 2 +- 11 files changed, 19 insertions(+), 19 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 85dbf9a..1662a5e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1280,7 +1280,7 @@ dependencies = [ [[package]] name = "flex" -version = "0.16.0" +version = "0.16.1" dependencies = [ "anyhow", "chrono", @@ -1304,7 +1304,7 @@ dependencies = [ [[package]] name = "flex-anilist" -version = "0.16.0" +version = "0.16.1" dependencies = [ "flex-plugin", "ureq", @@ -1312,7 +1312,7 @@ dependencies = [ [[package]] name = "flex-core" -version = "0.16.0" +version = "0.16.1" dependencies = [ "anyhow", "directories", @@ -1337,7 +1337,7 @@ dependencies = [ [[package]] name = "flex-lotus-lamp" -version = "0.16.0" +version = "0.16.1" dependencies = [ "flex-plugin", "lotus-lamp", @@ -1346,7 +1346,7 @@ dependencies = [ [[package]] name = "flex-plugin" -version = "0.16.0" +version = "0.16.1" dependencies = [ "serde", "serde_json", @@ -1354,7 +1354,7 @@ dependencies = [ [[package]] name = "flex-trakt" -version = "0.16.0" +version = "0.16.1" dependencies = [ "flex-plugin", "ureq", @@ -1362,7 +1362,7 @@ dependencies = [ [[package]] name = "flex-web" -version = "0.16.0" +version = "0.16.1" dependencies = [ "anyhow", "axum", diff --git a/Cargo.toml b/Cargo.toml index a56dff2..3ad935a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,7 +6,7 @@ default-members = ["crates/*"] [workspace.package] edition = "2024" -version = "0.16.0" +version = "0.16.1" license = "MIT" [profile.release] diff --git a/crates/flex-app/Cargo.toml b/crates/flex-app/Cargo.toml index 5229420..cf827f5 100644 --- a/crates/flex-app/Cargo.toml +++ b/crates/flex-app/Cargo.toml @@ -9,9 +9,9 @@ anyhow = "1.0.104" chrono = { version = "0.4.45", default-features = false, features = ["clock"] } eframe = { version = "0.36", default-features = false, features = ["glow", "default_fonts"] } env_logger = "0.11.11" -flex-core = { version = "0.16.0", path = "../flex-core" } -flex-plugin = { version = "0.16.0", path = "../flex-plugin" } -flex-web = { version = "0.16.0", path = "../flex-web" } +flex-core = { version = "0.16.1", path = "../flex-core" } +flex-plugin = { version = "0.16.1", path = "../flex-plugin" } +flex-web = { version = "0.16.1", path = "../flex-web" } image = { version = "0.25", default-features = false, features = ["jpeg", "png", "webp"] } libloading = "0.9" log = "0.4.34" diff --git a/crates/flex-core/Cargo.toml b/crates/flex-core/Cargo.toml index c7244b8..83b33fb 100644 --- a/crates/flex-core/Cargo.toml +++ b/crates/flex-core/Cargo.toml @@ -7,7 +7,7 @@ publish = false [dependencies] anyhow = "1.0.104" directories = "6.0.0" -flex-plugin = { version = "0.16.0", path = "../flex-plugin" } +flex-plugin = { version = "0.16.1", path = "../flex-plugin" } hunch = "2" lofty = "0.25.4" log = "0.4.34" diff --git a/crates/flex-web/Cargo.toml b/crates/flex-web/Cargo.toml index 75d2d9b..7e05202 100644 --- a/crates/flex-web/Cargo.toml +++ b/crates/flex-web/Cargo.toml @@ -7,7 +7,7 @@ publish = false [dependencies] anyhow = "1.0.104" axum = "0.8.9" -flex-core = { version = "0.16.0", path = "../flex-core" } +flex-core = { version = "0.16.1", path = "../flex-core" } log = "0.4.34" serde = { version = "1.0.229", features = ["derive", "rc"] } serde_json = "1.0.151" diff --git a/plugins/anilist/Cargo.toml b/plugins/anilist/Cargo.toml index 292ae11..fedb288 100644 --- a/plugins/anilist/Cargo.toml +++ b/plugins/anilist/Cargo.toml @@ -7,5 +7,5 @@ license.workspace = true publish = false [dependencies] -flex-plugin = { version = "0.16.0", path = "../../crates/flex-plugin" } +flex-plugin = { version = "0.16.1", path = "../../crates/flex-plugin" } ureq = { version = "3", features = ["json"] } diff --git a/plugins/anilist/plugin.toml b/plugins/anilist/plugin.toml index 002e9c2..0502cc4 100644 --- a/plugins/anilist/plugin.toml +++ b/plugins/anilist/plugin.toml @@ -1,6 +1,6 @@ name = "AniList" description = "Updates your AniList progress as you watch anime in Flex" -version = "0.16.0" +version = "0.16.1" command = "flex-anilist" subscribe = ["playback"] diff --git a/plugins/lotus-lamp/Cargo.toml b/plugins/lotus-lamp/Cargo.toml index 0ce99cf..f6da222 100644 --- a/plugins/lotus-lamp/Cargo.toml +++ b/plugins/lotus-lamp/Cargo.toml @@ -7,6 +7,6 @@ license.workspace = true publish = false [dependencies] -flex-plugin = { version = "0.16.0", path = "../../crates/flex-plugin" } +flex-plugin = { version = "0.16.1", path = "../../crates/flex-plugin" } lotus-lamp = { git = "https://github.com/bombpersons/lotus-lamp-rs", rev = "59cf5ccaa0007f006f1812fd02cc5991aff11e72" } tokio = { version = "1.53.2", features = ["rt", "time", "sync", "macros"] } diff --git a/plugins/lotus-lamp/plugin.toml b/plugins/lotus-lamp/plugin.toml index e712c4f..0d4d655 100644 --- a/plugins/lotus-lamp/plugin.toml +++ b/plugins/lotus-lamp/plugin.toml @@ -1,6 +1,6 @@ name = "LED strip (Lotus Lamp)" description = "Lights a Bluetooth LED strip behind the TV with the colours on screen" -version = "0.16.0" +version = "0.16.1" command = "flex-lotus-lamp" subscribe = ["light"] diff --git a/plugins/trakt/Cargo.toml b/plugins/trakt/Cargo.toml index 27235c4..dc0cbd1 100644 --- a/plugins/trakt/Cargo.toml +++ b/plugins/trakt/Cargo.toml @@ -7,5 +7,5 @@ license.workspace = true publish = false [dependencies] -flex-plugin = { version = "0.16.0", path = "../../crates/flex-plugin" } +flex-plugin = { version = "0.16.1", path = "../../crates/flex-plugin" } ureq = { version = "3", features = ["json"] } diff --git a/plugins/trakt/plugin.toml b/plugins/trakt/plugin.toml index 9e5fcba..93e9c1d 100644 --- a/plugins/trakt/plugin.toml +++ b/plugins/trakt/plugin.toml @@ -1,6 +1,6 @@ name = "Trakt" description = "Keeps your Trakt history up to date with what you watch in Flex" -version = "0.16.0" +version = "0.16.1" command = "flex-trakt" subscribe = ["playback"]