diff --git a/Cargo.lock b/Cargo.lock index b6edc34..c7de43d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1190,6 +1190,7 @@ dependencies = [ "directories", "futures-util", "image", + "libc", "open", "ratatui", "reqwest", @@ -1203,6 +1204,7 @@ dependencies = [ "toml", "tracing", "tracing-subscriber", + "windows-sys 0.61.2", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 57a0997..0d313f1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,3 +31,9 @@ tracing-subscriber = { version = "0.3.23", features = ["env-filter"] } [target.'cfg(target_os="macos")'.dependencies] core-foundation = "0.10.1" + +[target."cfg(windows)".dependencies] +windows-sys = { version = "0.61.2", features = ["Win32_Foundation", "Win32_UI_WindowsAndMessaging", "Win32_System_LibraryLoader"] } + +[target."cfg(unix)".dependencies] +libc = "0.2.186" diff --git a/README.md b/README.md index 828fd39..54a06f1 100644 --- a/README.md +++ b/README.md @@ -31,13 +31,14 @@ zbus (no libdbus), images and audio decoding are Rust crates. ### macOS / Windows -No system packages required. (Windows: media keys are not wired up yet; -playback works.) +No system packages required. ## Configuration -- `~/.config/furumi/keymap.toml` — keybinding overrides, see +- `keymap.toml` in the config dir — keybinding overrides, see `src/config/default_keymap.toml` for the format and defaults. -- `~/.config/furumi/credentials.json` — created on login (0600). + Config dir: `~/.config/furumi` on Linux, + `~/Library/Application Support/furumi` on macOS. +- `credentials.json` in the same dir — created on login (0600). - Logs: in-app on the Logs tab (`5`), and in the cache dir (`furumi-cli.log`), filtered by `RUST_LOG`. diff --git a/src/main.rs b/src/main.rs index 150f826..c232e72 100644 --- a/src/main.rs +++ b/src/main.rs @@ -19,6 +19,7 @@ fn main() -> Result<()> { if let Err(err) = config::logging::init() { startup_warning = Some(format!("logging disabled: {err:#}")); } + capture_stderr(); let (keymap, keymap_warning) = config::keymap::Keymap::load(); let startup_warning = keymap_warning.or(startup_warning); @@ -84,6 +85,46 @@ fn run_app( result } +/// C libraries (ALSA on some distros, in particular) print warnings straight +/// to stderr, which corrupts the TUI. Replace stderr with a pipe and forward +/// every line into tracing — it lands in the Logs tab and the log file +/// instead of the screen. +#[cfg(unix)] +fn capture_stderr() { + use std::io::BufRead as _; + use std::os::fd::FromRawFd as _; + + let mut fds = [0i32; 2]; + // SAFETY: plain pipe/dup2 syscalls on freshly created fds. + unsafe { + if libc::pipe(fds.as_mut_ptr()) != 0 { + return; + } + let [read_fd, write_fd] = fds; + if libc::dup2(write_fd, libc::STDERR_FILENO) == -1 { + libc::close(read_fd); + libc::close(write_fd); + return; + } + libc::close(write_fd); + let reader = std::fs::File::from_raw_fd(read_fd); + std::thread::Builder::new() + .name("stderr".to_string()) + .spawn(move || { + for line in std::io::BufReader::new(reader).lines() { + let Ok(line) = line else { break }; + if !line.trim().is_empty() { + tracing::warn!(target: "stderr", "{line}"); + } + } + }) + .ok(); + } +} + +#[cfg(not(unix))] +fn capture_stderr() {} + /// Kitty keyboard protocol, where supported, disambiguates Esc from alt-keys /// and modifier combos. The flags are popped on exit and on panic — leaving /// them pushed corrupts the user's shell. diff --git a/src/media.rs b/src/media.rs index 0b0fd1e..ede89b5 100644 --- a/src/media.rs +++ b/src/media.rs @@ -4,10 +4,9 @@ //! MPRemoteCommandCenter on macOS, SMTC on Windows. On macOS the command //! callbacks are only delivered while the main thread services its CFRunLoop, //! so the app runs on a worker thread and `run_on_main_thread` keeps the main -//! thread pumping the run loop and applying metadata updates. -//! -//! Windows note: SMTC needs a window handle; creating a hidden window is not -//! wired up yet, so media keys are skipped there with a log line. +//! thread pumping the run loop and applying metadata updates. On Windows, +//! SMTC needs a window: a hidden one is created here and its message queue +//! is pumped the same way. use std::sync::mpsc::{Receiver, RecvTimeoutError}; use std::time::Duration; @@ -86,15 +85,23 @@ pub fn run_on_main_thread( } fn create_controls() -> Option { + // SMTC on Windows attaches to a window; console apps create a hidden one. + #[cfg(target_os = "windows")] + let hwnd = match create_hidden_window() { + Some(hwnd) => Some(hwnd), + None => { + tracing::warn!("media keys: hidden window creation failed"); + return None; + } + }; + #[cfg(not(target_os = "windows"))] + let hwnd = None; + let config = PlatformConfig { display_name: "Furumi", dbus_name: "cy.hexor.furumi", - hwnd: None, + hwnd, }; - if cfg!(windows) { - tracing::info!("media keys: hidden-window SMTC setup not implemented yet, skipping"); - return None; - } match MediaControls::new(config) { Ok(controls) => Some(controls), Err(err) => { @@ -104,6 +111,42 @@ fn create_controls() -> Option { } } +/// An invisible top-level window owning the SMTC session. Created on the +/// main thread, which also pumps its messages in `pump_platform_events`. +#[cfg(target_os = "windows")] +fn create_hidden_window() -> Option<*mut std::ffi::c_void> { + use windows_sys::Win32::System::LibraryLoader::GetModuleHandleW; + use windows_sys::Win32::UI::WindowsAndMessaging::{ + CreateWindowExW, DefWindowProcW, RegisterClassW, WNDCLASSW, + }; + unsafe { + let class_name: Vec = "furumi_media_keys\0".encode_utf16().collect(); + let instance = GetModuleHandleW(core::ptr::null()); + let mut class: WNDCLASSW = core::mem::zeroed(); + class.lpfnWndProc = Some(DefWindowProcW); + class.hInstance = instance; + class.lpszClassName = class_name.as_ptr(); + if RegisterClassW(&class) == 0 { + return None; + } + let hwnd = CreateWindowExW( + 0, + class_name.as_ptr(), + class_name.as_ptr(), + 0, + 0, + 0, + 0, + 0, + core::ptr::null_mut(), + core::ptr::null_mut(), + instance, + core::ptr::null(), + ); + if hwnd.is_null() { None } else { Some(hwnd) } + } +} + fn apply(controls: &mut MediaControls, update: MediaUpdate) { let result = match update { MediaUpdate::Metadata { @@ -155,5 +198,21 @@ fn pump_platform_events() { ); } -#[cfg(not(target_os = "macos"))] +/// On Windows the hidden SMTC window needs its message queue drained on the +/// thread that created it. +#[cfg(target_os = "windows")] +fn pump_platform_events() { + use windows_sys::Win32::UI::WindowsAndMessaging::{ + DispatchMessageW, MSG, PM_REMOVE, PeekMessageW, TranslateMessage, + }; + unsafe { + let mut msg: MSG = core::mem::zeroed(); + while PeekMessageW(&mut msg, core::ptr::null_mut(), 0, 0, PM_REMOVE) != 0 { + TranslateMessage(&msg); + DispatchMessageW(&msg); + } + } +} + +#[cfg(not(any(target_os = "macos", target_os = "windows")))] fn pump_platform_events() {}