From 99c269a505309092f52f722cdf93c32a7b751b47 Mon Sep 17 00:00:00 2001 From: multiplex55 <6619098+multiplex55@users.noreply.github.com> Date: Sun, 21 Dec 2025 07:47:19 -0500 Subject: [PATCH 1/2] Add developer debug window instrumentation --- README.md | 1 + src/actions/screenshot.rs | 64 +++++++++++++++++++--------- src/gui/mod.rs | 88 +++++++++++++++++++++++++++++++++++++++ src/gui/note_panel.rs | 27 ++++++------ src/launcher.rs | 20 ++++++++- src/plugins/screenshot.rs | 14 +++---- src/settings.rs | 4 ++ src/settings_editor.rs | 9 ++++ src/window_manager.rs | 41 ++++++++++++++++++ 9 files changed, 229 insertions(+), 39 deletions(-) diff --git a/README.md b/README.md index 635a64df..dca217f3 100644 --- a/README.md +++ b/README.md @@ -102,6 +102,7 @@ value as shown below: "fuzzy_weight": 1.0, "usage_weight": 1.0, "debug_logging": false, + "developer_debug": false, "log_file": true, "offscreen_pos": [2000, 2000], "window_size": [400, 220], diff --git a/src/actions/screenshot.rs b/src/actions/screenshot.rs index bacae829..8e0705dc 100644 --- a/src/actions/screenshot.rs +++ b/src/actions/screenshot.rs @@ -3,6 +3,7 @@ use std::borrow::Cow; use std::path::PathBuf; use crate::plugins::screenshot::screenshot_dir; +use crate::window_manager::{foreground_window_info, WindowDebugInfo}; use screenshots::Screen; use windows::Win32::Foundation::RECT; use windows::Win32::UI::WindowsAndMessaging::{GetForegroundWindow, GetWindowRect}; @@ -14,11 +15,27 @@ pub enum Mode { Desktop, } -pub fn capture_raw(mode: Mode) -> anyhow::Result { +#[derive(Clone, Debug)] +pub struct RawCapture { + pub image: image::RgbaImage, + pub active_window: Option, +} + +#[derive(Clone, Debug)] +pub struct SavedCapture { + pub path: PathBuf, + pub active_window: Option, +} + +pub fn capture_raw(mode: Mode, developer_debug: bool) -> anyhow::Result { + let active_window = developer_debug.then(foreground_window_info).flatten(); match mode { Mode::Desktop => { let screen = Screen::from_point(0, 0)?; - Ok(screen.capture()?) + Ok(RawCapture { + image: screen.capture()?, + active_window, + }) } Mode::Window => { let hwnd = unsafe { GetForegroundWindow() }; @@ -30,12 +47,15 @@ pub fn capture_raw(mode: Mode) -> anyhow::Result { let width = (rect.right - rect.left) as u32; let height = (rect.bottom - rect.top) as u32; let screen = Screen::from_point(rect.left + 1, rect.top + 1)?; - Ok(screen.capture_area( - rect.left - screen.display_info.x, - rect.top - screen.display_info.y, - width, - height, - )?) + Ok(RawCapture { + image: screen.capture_area( + rect.left - screen.display_info.x, + rect.top - screen.display_info.y, + width, + height, + )?, + active_window, + }) } Mode::Region => { use std::process::Command; @@ -44,9 +64,10 @@ pub fn capture_raw(mode: Mode) -> anyhow::Result { // Wait for the snipping tool to provide a new clipboard image let mut cb = arboard::Clipboard::new()?; - let old = cb.get_image().ok().map(|img| { - (img.width, img.height, img.bytes.into_owned()) - }); + let old = cb + .get_image() + .ok() + .map(|img| (img.width, img.height, img.bytes.into_owned())); let _ = Command::new("explorer").arg("ms-screenclip:").status(); @@ -73,12 +94,15 @@ pub fn capture_raw(mode: Mode) -> anyhow::Result { img.bytes.into_owned(), ) .ok_or_else(|| anyhow::anyhow!("invalid clipboard image"))?; - Ok(buf) + Ok(RawCapture { + image: buf, + active_window, + }) } } } -pub fn capture(mode: Mode, clipboard: bool) -> anyhow::Result { +pub fn capture(mode: Mode, clipboard: bool, developer_debug: bool) -> anyhow::Result { let dir = screenshot_dir(); std::fs::create_dir_all(&dir)?; let filename = format!( @@ -86,19 +110,21 @@ pub fn capture(mode: Mode, clipboard: bool) -> anyhow::Result { Local::now().format("%Y%m%d_%H%M%S") ); let path = dir.join(filename); - let img = capture_raw(mode)?; - img.save(&path)?; + let capture = capture_raw(mode, developer_debug)?; + capture.image.save(&path)?; if clipboard { - let (w, h) = img.dimensions(); + let (w, h) = capture.image.dimensions(); let mut cb = arboard::Clipboard::new()?; cb.set_image(arboard::ImageData { width: w as usize, height: h as usize, - bytes: Cow::Owned(img.into_raw()), + bytes: Cow::Owned(capture.image.into_raw()), })?; } else { open::that(&path)?; } - Ok(path) + Ok(SavedCapture { + path, + active_window: capture.active_window, + }) } - diff --git a/src/gui/mod.rs b/src/gui/mod.rs index cc7de288..4f2aa89c 100644 --- a/src/gui/mod.rs +++ b/src/gui/mod.rs @@ -68,6 +68,7 @@ use crate::settings_editor::SettingsEditor; use crate::toast_log::{append_toast_log, TOAST_LOG_FILE}; use crate::usage::{self, USAGE_FILE}; use crate::visibility::apply_visibility; +use crate::window_manager::WindowDebugInfo; use eframe::egui; use egui_toast::{Toast, ToastKind, ToastOptions, Toasts}; use fst::{IntoStreamer, Map, MapBuilder, Streamer}; @@ -266,6 +267,12 @@ struct PanelStates { plugins: bool, } +#[derive(Clone, Debug)] +struct DebugWindowEntry { + label: String, + info: WindowDebugInfo, +} + /// Primary GUI state for Multi Launcher. /// /// The application may create multiple windows or helper threads. To keep the @@ -324,6 +331,9 @@ pub struct LauncherApp { toasts: egui_toast::Toasts, pub enable_toasts: bool, pub toast_duration: f32, + pub developer_debug: bool, + tracked_windows: Vec, + last_captured_window: Option, alias_dialog: AliasDialog, bookmark_alias_dialog: BookmarkAliasDialog, tempfile_alias_dialog: TempfileAliasDialog, @@ -462,6 +472,41 @@ impl LauncherApp { } } + fn refresh_tracked_windows(&mut self) { + if !self.developer_debug { + self.tracked_windows.clear(); + return; + } + + let mut tracked = Vec::new(); + tracked.push(DebugWindowEntry { + label: "Launcher viewport".into(), + info: WindowDebugInfo { + title: "Multi Launcher".into(), + x: self.window_pos.0, + y: self.window_pos.1, + width: self.window_size.0, + height: self.window_size.1, + }, + }); + + if let Some(info) = crate::window_manager::foreground_window_info() { + tracked.push(DebugWindowEntry { + label: "Foreground window".into(), + info, + }); + } + + if let Some(info) = self.last_captured_window.clone() { + tracked.push(DebugWindowEntry { + label: "Last captured window".into(), + info, + }); + } + + self.tracked_windows = tracked; + } + pub fn plugin_enabled(&self, name: &str) -> bool { match &self.enabled_plugins { Some(set) => set.contains(name), @@ -493,6 +538,7 @@ impl LauncherApp { toast_duration: Option, fuzzy_weight: Option, usage_weight: Option, + developer_debug: Option, follow_mouse: Option, static_enabled: Option, static_pos: Option<(i32, i32)>, @@ -539,6 +585,9 @@ impl LauncherApp { if let Some(v) = usage_weight { self.usage_weight = v; } + if let Some(v) = developer_debug { + self.developer_debug = v; + } if let Some(v) = follow_mouse { self.follow_mouse = v; } @@ -826,6 +875,9 @@ impl LauncherApp { toasts, enable_toasts, toast_duration, + developer_debug: settings.developer_debug, + tracked_windows: Vec::new(), + last_captured_window: None, alias_dialog: AliasDialog::default(), bookmark_alias_dialog: BookmarkAliasDialog::default(), tempfile_alias_dialog: TempfileAliasDialog::default(), @@ -1391,6 +1443,16 @@ impl LauncherApp { self.screenshot_use_editor } + pub fn developer_debug_enabled(&self) -> bool { + self.developer_debug + } + + pub fn record_captured_window(&mut self, info: WindowDebugInfo) { + if self.developer_debug { + self.last_captured_window = Some(info); + } + } + /// Close the top-most open dialog if any is visible. /// Returns `true` when a dialog was closed. pub fn close_front_dialog(&mut self) -> bool { @@ -1901,6 +1963,11 @@ impl eframe::App for LauncherApp { if let Some(rect) = ctx.input(|i| i.viewport().outer_rect) { self.window_pos = (rect.min.x as i32, rect.min.y as i32); } + if self.developer_debug { + self.refresh_tracked_windows(); + } else { + self.tracked_windows.clear(); + } let do_restore = self.restore_flag.swap(false, Ordering::SeqCst); if self.visible_flag.load(Ordering::SeqCst) && self.help_flag.swap(false, Ordering::SeqCst) { @@ -2083,6 +2150,27 @@ impl eframe::App for LauncherApp { self.last_net_update = Instant::now(); } + if self.developer_debug { + egui::TopBottomPanel::bottom("developer_debug_panel").show(ctx, |ui| { + ui.heading("Developer debug: tracked windows"); + if self.tracked_windows.is_empty() { + ui.label("No windows tracked yet. Trigger a screenshot or open a window to populate."); + } else { + for entry in &self.tracked_windows { + ui.monospace(format!( + "{}: \"{}\" @ ({}, {}) {}x{}", + entry.label, + entry.info.title, + entry.info.x, + entry.info.y, + entry.info.width, + entry.info.height + )); + } + } + }); + } + CentralPanel::default().show(ctx, |ui| { ui.heading("🚀 Multi Lnchr"); if let Some(err) = &self.error { diff --git a/src/gui/note_panel.rs b/src/gui/note_panel.rs index 1ba35928..93fa1e51 100644 --- a/src/gui/note_panel.rs +++ b/src/gui/note_panel.rs @@ -837,14 +837,21 @@ impl NotePanel { } } if ui.button("Screenshot...").clicked() { - match capture(ScreenshotMode::Region, true) { - Ok(path) => { - if let Some(fname) = path.file_name().and_then(|s| s.to_str()) { + match capture(ScreenshotMode::Region, true, app.developer_debug_enabled()) { + Ok(capture_result) => { + if let Some(info) = capture_result.active_window { + app.record_captured_window(info); + } + if let Some(fname) = + capture_result.path.file_name().and_then(|s| s.to_str()) + { let dest = assets_dir().join(fname); - let result = std::fs::rename(&path, &dest).or_else(|_| { - std::fs::copy(&path, &dest) - .map(|_| std::fs::remove_file(&path).unwrap_or(())) - }); + let result = + std::fs::rename(&capture_result.path, &dest).or_else(|_| { + std::fs::copy(&capture_result.path, &dest).map(|_| { + std::fs::remove_file(&capture_result.path).unwrap_or(()) + }) + }); if let Err(e) = result { app.set_error(format!("Failed to save screenshot: {e}")); } else { @@ -1469,11 +1476,7 @@ mod tests { }); }); assert_eq!( - output - .platform_output - .open_url - .unwrap() - .url, + output.platform_output.open_url.unwrap().url, "https://www.example.com" ); } diff --git a/src/launcher.rs b/src/launcher.rs index 0895b40b..6cf10154 100644 --- a/src/launcher.rs +++ b/src/launcher.rs @@ -1,5 +1,6 @@ use crate::actions::Action; use crate::plugins::calc_history::{self, CalcHistoryEntry, CALC_HISTORY_FILE, MAX_ENTRIES}; +use crate::settings::Settings; pub(crate) fn set_system_volume(percent: u32) { use windows::Win32::Media::Audio::Endpoints::IAudioEndpointVolume; @@ -851,7 +852,24 @@ pub fn launch_action(action: &Action) -> anyhow::Result<()> { Ok(()) } ActionKind::Screenshot { mode, clip } => { - crate::actions::screenshot::capture(mode, clip)?; + let developer_debug = Settings::load("settings.json") + .map(|s| s.developer_debug) + .unwrap_or(false); + let capture = crate::actions::screenshot::capture(mode, clip, developer_debug)?; + if developer_debug { + if let Some(info) = capture.active_window { + tracing::info!( + title = %info.title, + x = info.x, + y = info.y, + width = info.width, + height = info.height, + "Captured screenshot with active window metadata", + ); + } else { + tracing::info!("Captured screenshot without active window metadata"); + } + } Ok(()) } ActionKind::MediaPlay => { diff --git a/src/plugins/screenshot.rs b/src/plugins/screenshot.rs index d958de97..a7a26e41 100644 --- a/src/plugins/screenshot.rs +++ b/src/plugins/screenshot.rs @@ -2,9 +2,9 @@ use crate::actions::Action; use crate::plugin::Plugin; use crate::settings::Settings; use eframe::egui; +use rfd::FileDialog; use serde::{Deserialize, Serialize}; use std::path::PathBuf; -use rfd::FileDialog; /// Return the directory used to store screenshots. /// @@ -48,7 +48,11 @@ pub fn launch_editor( ) -> anyhow::Result<()> { use chrono::Local; use std::borrow::Cow; - let img = crate::actions::screenshot::capture_raw(mode)?; + let capture = crate::actions::screenshot::capture_raw(mode, app.developer_debug_enabled())?; + if let Some(info) = &capture.active_window { + app.record_captured_window(info.clone()); + } + let img = capture.image; if app.get_screenshot_use_editor() { app.open_screenshot_editor(img, clip); } else { @@ -85,7 +89,6 @@ pub fn launch_editor( Ok(()) } - pub struct ScreenshotPlugin; impl Plugin for ScreenshotPlugin { @@ -183,10 +186,7 @@ impl Plugin for ScreenshotPlugin { "Save file when copying screenshot", ); ui.checkbox(&mut cfg.screenshot_use_editor, "Enable screenshot editor"); - ui.checkbox( - &mut cfg.screenshot_auto_save, - "Auto-save after editing", - ); + ui.checkbox(&mut cfg.screenshot_auto_save, "Auto-save after editing"); if let Ok(v) = serde_json::to_value(&cfg) { *value = v; } diff --git a/src/settings.rs b/src/settings.rs index d888bc1f..cb77eefb 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -57,6 +57,9 @@ pub struct Settings { /// Defaults to `false` when the field is missing in the settings file. #[serde(default)] pub debug_logging: bool, + /// Enable developer-focused debug tools like window instrumentation. + #[serde(default)] + pub developer_debug: bool, /// Enable logging to a file. Use `true` for the default `launcher.log` next /// to the executable or provide a custom path. #[serde(default)] @@ -268,6 +271,7 @@ impl Default for Settings { enabled_plugins: None, enabled_capabilities: None, debug_logging: false, + developer_debug: false, log_file: None, offscreen_pos: Some((2000, 2000)), window_size: Some((400, 220)), diff --git a/src/settings_editor.rs b/src/settings_editor.rs index e5e72088..54fa4836 100644 --- a/src/settings_editor.rs +++ b/src/settings_editor.rs @@ -21,6 +21,7 @@ pub struct SettingsEditor { help_hotkey_valid: bool, last_valid_help_hotkey: String, debug_logging: bool, + developer_debug: bool, show_toasts: bool, toast_duration: f32, offscreen_x: i32, @@ -111,6 +112,7 @@ impl SettingsEditor { help_hotkey_valid, last_valid_help_hotkey, debug_logging: settings.debug_logging, + developer_debug: settings.developer_debug, show_toasts: settings.enable_toasts, toast_duration: settings.toast_duration, offscreen_x: settings.offscreen_pos.unwrap_or((2000, 2000)).0, @@ -228,6 +230,7 @@ impl SettingsEditor { enabled_plugins: current.enabled_plugins.clone(), enabled_capabilities: current.enabled_capabilities.clone(), debug_logging: self.debug_logging, + developer_debug: self.developer_debug, log_file: current.log_file.clone(), enable_toasts: self.show_toasts, toast_duration: self.toast_duration, @@ -351,6 +354,11 @@ impl SettingsEditor { }); }); + ui.checkbox(&mut self.developer_debug, "Enable developer debug overlay") + .on_hover_text( + "Shows live window instrumentation and logs extra window details", + ); + ui.checkbox(&mut self.show_toasts, "Enable toast notifications"); if self.show_toasts { ui.horizontal(|ui| { @@ -646,6 +654,7 @@ impl SettingsEditor { Some(new_settings.toast_duration), Some(new_settings.fuzzy_weight), Some(new_settings.usage_weight), + Some(new_settings.developer_debug), Some(new_settings.follow_mouse), Some(new_settings.static_location_enabled), new_settings.static_pos, diff --git a/src/window_manager.rs b/src/window_manager.rs index 5fb908de..3e587eaa 100644 --- a/src/window_manager.rs +++ b/src/window_manager.rs @@ -177,6 +177,47 @@ pub fn current_mouse_position() -> Option<(f32, f32)> { } use raw_window_handle::{HasWindowHandle, RawWindowHandle}; +#[derive(Clone, Debug)] +pub struct WindowDebugInfo { + pub title: String, + pub x: i32, + pub y: i32, + pub width: i32, + pub height: i32, +} + +/// Fetch the current foreground window title and geometry for debugging. +pub fn foreground_window_info() -> Option { + use windows::Win32::Foundation::RECT; + use windows::Win32::UI::WindowsAndMessaging::{ + GetForegroundWindow, GetWindowRect, GetWindowTextLengthW, GetWindowTextW, + }; + + unsafe { + let hwnd = GetForegroundWindow(); + if hwnd.is_invalid() { + return None; + } + let mut rect = RECT::default(); + if GetWindowRect(hwnd, &mut rect).is_err() { + return None; + } + + let len = GetWindowTextLengthW(hwnd) as usize + 1; + let mut buf = vec![0u16; len]; + let written = GetWindowTextW(hwnd, &mut buf); + let title = String::from_utf16_lossy(&buf[..written as usize]); + + Some(WindowDebugInfo { + title, + x: rect.left, + y: rect.top, + width: rect.right - rect.left, + height: rect.bottom - rect.top, + }) + } +} + /// Ensure the given window resides on the active virtual desktop. /// /// This uses the `IVirtualDesktopManager` COM interface to check if `hwnd` From 7a43bb743b8aff810f8b794e5334cfbf2101ff2a Mon Sep 17 00:00:00 2001 From: multiplex55 <6619098+multiplex55@users.noreply.github.com> Date: Sun, 21 Dec 2025 08:02:21 -0500 Subject: [PATCH 2/2] Fix developer debug update paths argument --- src/plugin_editor.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/plugin_editor.rs b/src/plugin_editor.rs index ba35f149..e996eb6a 100644 --- a/src/plugin_editor.rs +++ b/src/plugin_editor.rs @@ -111,6 +111,7 @@ impl PluginEditor { Some(s.toast_duration), Some(s.fuzzy_weight), Some(s.usage_weight), + Some(s.developer_debug), Some(s.follow_mouse), Some(s.static_location_enabled), s.static_pos,