Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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],
Expand Down
64 changes: 45 additions & 19 deletions src/actions/screenshot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand All @@ -14,11 +15,27 @@ pub enum Mode {
Desktop,
}

pub fn capture_raw(mode: Mode) -> anyhow::Result<image::RgbaImage> {
#[derive(Clone, Debug)]
pub struct RawCapture {
pub image: image::RgbaImage,
pub active_window: Option<WindowDebugInfo>,
}

#[derive(Clone, Debug)]
pub struct SavedCapture {
pub path: PathBuf,
pub active_window: Option<WindowDebugInfo>,
}

pub fn capture_raw(mode: Mode, developer_debug: bool) -> anyhow::Result<RawCapture> {
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() };
Expand All @@ -30,12 +47,15 @@ pub fn capture_raw(mode: Mode) -> anyhow::Result<image::RgbaImage> {
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;
Expand All @@ -44,9 +64,10 @@ pub fn capture_raw(mode: Mode) -> anyhow::Result<image::RgbaImage> {

// 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();

Expand All @@ -73,32 +94,37 @@ pub fn capture_raw(mode: Mode) -> anyhow::Result<image::RgbaImage> {
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<PathBuf> {
pub fn capture(mode: Mode, clipboard: bool, developer_debug: bool) -> anyhow::Result<SavedCapture> {
let dir = screenshot_dir();
std::fs::create_dir_all(&dir)?;
let filename = format!(
"multi_launcher_{}.png",
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,
})
}

88 changes: 88 additions & 0 deletions src/gui/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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<DebugWindowEntry>,
last_captured_window: Option<WindowDebugInfo>,
alias_dialog: AliasDialog,
bookmark_alias_dialog: BookmarkAliasDialog,
tempfile_alias_dialog: TempfileAliasDialog,
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -493,6 +538,7 @@ impl LauncherApp {
toast_duration: Option<f32>,
fuzzy_weight: Option<f32>,
usage_weight: Option<f32>,
developer_debug: Option<bool>,
follow_mouse: Option<bool>,
static_enabled: Option<bool>,
static_pos: Option<(i32, i32)>,
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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(),
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
{
Expand Down Expand Up @@ -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 {
Expand Down
27 changes: 15 additions & 12 deletions src/gui/note_panel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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"
);
}
Expand Down
20 changes: 19 additions & 1 deletion src/launcher.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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 => {
Expand Down
1 change: 1 addition & 0 deletions src/plugin_editor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Loading
Loading