From c7079ec96e2609b18c3b2864a8060f12d167491e Mon Sep 17 00:00:00 2001 From: Robert Wagner Date: Fri, 12 Dec 2025 10:36:54 -0500 Subject: [PATCH 1/6] Try pipewire 0.9 again --- .../magnifier/rust-sampler/Cargo.toml | 2 +- .../src/sampler/wayland_portal.rs | 82 ++++++++++--------- 2 files changed, 43 insertions(+), 41 deletions(-) diff --git a/electron-app/magnifier/rust-sampler/Cargo.toml b/electron-app/magnifier/rust-sampler/Cargo.toml index 2d7e80067..f7b7d2502 100644 --- a/electron-app/magnifier/rust-sampler/Cargo.toml +++ b/electron-app/magnifier/rust-sampler/Cargo.toml @@ -41,7 +41,7 @@ x11 = { version = "2.21", features = ["xlib"], optional = true } dirs = { version = "5.0", optional = true } # Optional dependencies for Wayland support (requires libpipewire-0.3-dev) ashpd = { version = "0.9", optional = true } -pipewire = { version = "0.7.2", features = ["v0_3_41"], optional = true } +pipewire = { version = "0.9", optional = true } tokio = { version = "1", features = ["rt", "sync", "macros"], optional = true } futures = { version = "0.3", optional = true } diff --git a/electron-app/magnifier/rust-sampler/src/sampler/wayland_portal.rs b/electron-app/magnifier/rust-sampler/src/sampler/wayland_portal.rs index 920980172..4d55db107 100644 --- a/electron-app/magnifier/rust-sampler/src/sampler/wayland_portal.rs +++ b/electron-app/magnifier/rust-sampler/src/sampler/wayland_portal.rs @@ -170,21 +170,21 @@ impl WaylandPortalSampler { // Initialize PipeWire pw::init(); - + // Create PipeWire main loop - let mainloop = pw::main_loop::MainLoop::new(None) + let mainloop = pw::main_loop::MainLoopRc::new(None) .map_err(|_| "Failed to create PipeWire main loop".to_string())?; - + // Create PipeWire context - let context = pw::context::Context::new(&mainloop) + let context = pw::context::ContextRc::new(&mainloop, None) .map_err(|_| "Failed to create PipeWire context".to_string())?; - + // Connect to PipeWire core - let core = context.connect(None) + let core = context.connect_rc(None) .map_err(|_| "Failed to connect to PipeWire".to_string())?; // Create a stream - let stream = pw::stream::Stream::new( + let stream = pw::stream::StreamBox::new( &core, "swach-screenshot", pw::properties::properties! { @@ -194,20 +194,30 @@ impl WaylandPortalSampler { }, ).map_err(|_| "Failed to create PipeWire stream".to_string())?; - // Buffer to store the screenshot - let screenshot_buffer = Arc::clone(&self.screenshot_buffer); - let frame_captured = Arc::new(std::sync::atomic::AtomicBool::new(false)); - let frame_captured_clone = Arc::clone(&frame_captured); - // Video format info let video_info: Arc>> = Arc::new(Mutex::new(None)); - let video_info_clone = Arc::clone(&video_info); - let video_info_process = Arc::clone(&video_info); - + + // User data for callbacks + struct CallbackData { + screenshot_buffer: Arc>>, + frame_captured: Arc, + video_info: Arc>>, + mainloop: pw::main_loop::MainLoopRc, + } + + let callback_data = Arc::new(CallbackData { + screenshot_buffer: Arc::clone(&self.screenshot_buffer), + frame_captured: Arc::clone(&frame_captured), + video_info: Arc::clone(&video_info), + mainloop: mainloop.clone(), + }); + + let callback_data_clone = Arc::clone(&callback_data); + // Add listener to receive one frame let _listener = stream - .add_local_listener_with_user_data(screenshot_buffer) - .param_changed(move |_stream, _user_data, id, param| { + .add_local_listener_with_user_data(callback_data) + .param_changed(move |_stream, user_data, id, param| { use pw::spa::param::ParamType; if id != ParamType::Format.as_raw() { @@ -218,14 +228,14 @@ impl WaylandPortalSampler { use pw::spa::param::video::VideoInfoRaw; if let Ok((_media_type, _media_subtype)) = pw::spa::param::format_utils::parse_format(param) { - let mut info = VideoInfoRaw::new(); + let mut info = VideoInfoRaw::default(); if let Ok(_) = info.parse(param) { let size = info.size(); let width = size.width; let height = size.height; let stride = width as usize * 4; // BGRA format - if let Ok(mut vi) = video_info_clone.lock() { + if let Ok(mut vi) = user_data.video_info.lock() { *vi = Some((width, height, stride)); } } @@ -234,10 +244,10 @@ impl WaylandPortalSampler { }) .process(move |stream, user_data| { // Only capture one frame - if frame_captured_clone.load(std::sync::atomic::Ordering::SeqCst) { + if user_data.frame_captured.load(std::sync::atomic::Ordering::SeqCst) { return; } - + match stream.dequeue_buffer() { None => {} Some(mut buffer) => { @@ -245,38 +255,39 @@ impl WaylandPortalSampler { if datas.is_empty() { return; } - + let data = &mut datas[0]; let chunk = data.chunk(); let size = chunk.size() as usize; - + if size == 0 { return; } - + // Get video format - let format_info = if let Ok(vi) = video_info_process.lock() { + let format_info = if let Ok(vi) = user_data.video_info.lock() { *vi } else { None }; - + // Get frame data if let Some(slice) = data.data() { if slice.len() >= size { let pixel_data = slice[..size].to_vec(); - + if let Some((width, height, stride)) = format_info { if width > 0 && height > 0 { eprintln!("[Wayland] Captured screenshot: {}x{} ({} bytes)", width, height, pixel_data.len()); - if let Ok(mut buf) = user_data.lock() { + if let Ok(mut buf) = user_data.screenshot_buffer.lock() { *buf = Some(ScreenshotBuffer { data: pixel_data, width, height, stride, }); - frame_captured_clone.store(true, std::sync::atomic::Ordering::SeqCst); + user_data.frame_captured.store(true, std::sync::atomic::Ordering::SeqCst); + user_data.mainloop.quit(); } } } @@ -297,17 +308,8 @@ impl WaylandPortalSampler { ) .map_err(|e| format!("Failed to connect stream: {:?}", e))?; - // Iterate mainloop until we capture one frame (timeout after 5 seconds) - let start_time = std::time::Instant::now(); - let timeout = std::time::Duration::from_secs(5); - - while !frame_captured.load(std::sync::atomic::Ordering::SeqCst) { - if start_time.elapsed() > timeout { - return Err("Timeout waiting for screenshot frame".to_string()); - } - - let _ = mainloop.loop_().iterate(std::time::Duration::from_millis(10)); - } + // Run mainloop until we capture one frame or timeout + mainloop.run(); Ok(()) } From 69889fa4ae4cf66934511a5c41dea426680a6f55 Mon Sep 17 00:00:00 2001 From: Robert Wagner Date: Fri, 12 Dec 2025 10:38:05 -0500 Subject: [PATCH 2/6] Update wayland_portal.rs --- .../magnifier/rust-sampler/src/sampler/wayland_portal.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/electron-app/magnifier/rust-sampler/src/sampler/wayland_portal.rs b/electron-app/magnifier/rust-sampler/src/sampler/wayland_portal.rs index 4d55db107..365a47d68 100644 --- a/electron-app/magnifier/rust-sampler/src/sampler/wayland_portal.rs +++ b/electron-app/magnifier/rust-sampler/src/sampler/wayland_portal.rs @@ -197,6 +197,9 @@ impl WaylandPortalSampler { // Video format info let video_info: Arc>> = Arc::new(Mutex::new(None)); + // Frame captured flag + let frame_captured = Arc::new(std::sync::atomic::AtomicBool::new(false)); + // User data for callbacks struct CallbackData { screenshot_buffer: Arc>>, From 3457b2497baf3e48cf7f3400636efa163cf12eb9 Mon Sep 17 00:00:00 2001 From: Robert Wagner Date: Fri, 12 Dec 2025 10:41:14 -0500 Subject: [PATCH 3/6] Update wayland_portal.rs --- .../magnifier/rust-sampler/src/sampler/wayland_portal.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/electron-app/magnifier/rust-sampler/src/sampler/wayland_portal.rs b/electron-app/magnifier/rust-sampler/src/sampler/wayland_portal.rs index 365a47d68..409337a35 100644 --- a/electron-app/magnifier/rust-sampler/src/sampler/wayland_portal.rs +++ b/electron-app/magnifier/rust-sampler/src/sampler/wayland_portal.rs @@ -215,8 +215,6 @@ impl WaylandPortalSampler { mainloop: mainloop.clone(), }); - let callback_data_clone = Arc::clone(&callback_data); - // Add listener to receive one frame let _listener = stream .add_local_listener_with_user_data(callback_data) From 29f4029319448d8249261a4be4b019fa4021e8bb Mon Sep 17 00:00:00 2001 From: Robert Wagner Date: Fri, 12 Dec 2025 10:45:46 -0500 Subject: [PATCH 4/6] Remove console logs --- electron-app/magnifier/magnifier-main-rust.ts | 21 ------------------- .../magnifier/rust-sampler-manager.ts | 7 ------- 2 files changed, 28 deletions(-) diff --git a/electron-app/magnifier/magnifier-main-rust.ts b/electron-app/magnifier/magnifier-main-rust.ts index 7a8e30b21..3621ca2d0 100644 --- a/electron-app/magnifier/magnifier-main-rust.ts +++ b/electron-app/magnifier/magnifier-main-rust.ts @@ -59,12 +59,7 @@ class MagnifyingColorPicker { try { // Pre-start the sampler to trigger permission dialogs BEFORE showing magnifier // This is critical on Wayland where the permission dialog needs to be clickable - console.log( - '[Magnifying Color Picker] Pre-starting sampler for permission check...' - ); await this.samplerManager.ensureStarted(this.gridSize, 15); - console.log('[Magnifying Color Picker] Sampler ready, showing magnifier'); - await this.createMagnifierWindow(); return await this.startColorPicking(); } catch (error) { @@ -166,20 +161,12 @@ class MagnifyingColorPicker { const newDiameter = getNextDiameter(this.magnifierDiameter, delta); if (newDiameter !== this.magnifierDiameter) { - console.log( - `[Magnifier] Diameter change: ${this.magnifierDiameter} → ${newDiameter}` - ); this.magnifierDiameter = newDiameter; - const oldGridSize = this.gridSize; this.gridSize = calculateGridSize( this.magnifierDiameter, this.squareSize ); - console.log( - `[Magnifier] Grid size change: ${oldGridSize} → ${this.gridSize}` - ); - // Update grid size in Rust sampler this.samplerManager.updateGridSize(this.gridSize); } @@ -189,20 +176,12 @@ class MagnifyingColorPicker { const newSquareSize = adjustSquareSize(this.squareSize, delta); if (newSquareSize !== this.squareSize) { - console.log( - `[Magnifier] Square size change: ${this.squareSize} → ${newSquareSize}` - ); this.squareSize = newSquareSize; - const oldGridSize = this.gridSize; this.gridSize = calculateGridSize( this.magnifierDiameter, this.squareSize ); - console.log( - `[Magnifier] Grid size change: ${oldGridSize} → ${this.gridSize}` - ); - // Update grid size in Rust sampler this.samplerManager.updateGridSize(this.gridSize); } diff --git a/electron-app/magnifier/rust-sampler-manager.ts b/electron-app/magnifier/rust-sampler-manager.ts index 2bde1abe1..0c5bfebce 100644 --- a/electron-app/magnifier/rust-sampler-manager.ts +++ b/electron-app/magnifier/rust-sampler-manager.ts @@ -181,13 +181,11 @@ export class RustSamplerManager { } updateGridSize(gridSize: number): void { - console.log(`[RustSampler] Sending update_grid command: ${gridSize}`); const command = { command: 'update_grid', grid_size: gridSize, }; this.sendCommand(command); - console.log(`[RustSampler] Command sent:`, JSON.stringify(command)); } stop(): Promise { @@ -195,8 +193,6 @@ export class RustSamplerManager { return Promise.resolve(); } - console.log('[RustSampler] Stopping process'); - const proc = this.process; this.process = null; this.dataCallback = null; @@ -205,7 +201,6 @@ export class RustSamplerManager { return new Promise((resolve) => { // Set up exit handler const onExit = () => { - console.log('[RustSampler] Process exited'); if (this.forceKillTimeout) { clearTimeout(this.forceKillTimeout); this.forceKillTimeout = null; @@ -259,9 +254,7 @@ export class RustSamplerManager { try { const json = JSON.stringify(command); - console.log('[RustSampler] Writing to stdin:', json); this.process.stdin.write(json + '\n'); - console.log('[RustSampler] Write successful'); } catch (e) { console.error('[RustSampler] Failed to send command:', e); } From 0c834463d07c013f6e055066641ab34aac4b7e89 Mon Sep 17 00:00:00 2001 From: Robert Wagner Date: Fri, 12 Dec 2025 10:47:20 -0500 Subject: [PATCH 5/6] Delete screenshot-provider.ts --- electron-app/src/screenshot-provider.ts | 241 ------------------------ 1 file changed, 241 deletions(-) delete mode 100644 electron-app/src/screenshot-provider.ts diff --git a/electron-app/src/screenshot-provider.ts b/electron-app/src/screenshot-provider.ts deleted file mode 100644 index 2ffd56c7e..000000000 --- a/electron-app/src/screenshot-provider.ts +++ /dev/null @@ -1,241 +0,0 @@ -import { exec } from 'node:child_process'; -import { unlinkSync } from 'node:fs'; -import { join } from 'node:path'; -import { promisify } from 'node:util'; - -import { app, nativeImage, screen } from 'electron'; - -const execAsync = promisify(exec); - -interface ScreenshotResult { - width: number; - height: number; - rawData: Uint8Array; - dataURL: string; - monitorInfo: { - x: number; - y: number; - width: number; - height: number; - }; -} - -interface NodeScreenshotsMonitor { - x: number; - y: number; - width: number; - height: number; - captureImageSync: () => { - width: number; - height: number; - toRawSync: () => Uint8Array; - }; -} - -interface NodeScreenshotsModule { - Monitor: { - fromPoint: (x: number, y: number) => NodeScreenshotsMonitor | null; - }; -} - -let nodeScreenshots: NodeScreenshotsModule | null = null; -let fallbackMode = false; - -// Try to load node-screenshots, fall back to shell commands if not available -try { - // Dynamic import to allow graceful fallback - // eslint-disable-next-line @typescript-eslint/no-require-imports - nodeScreenshots = require('node-screenshots') as NodeScreenshotsModule; - console.log('[Screenshot Provider] Using node-screenshots'); -} catch { - console.log( - '[Screenshot Provider] node-screenshots not available, using fallback' - ); - fallbackMode = true; -} - -async function captureScreenshotWithFallback(): Promise { - if (!fallbackMode && nodeScreenshots) { - return captureWithNodeScreenshots(); - } - - return captureWithShellCommands(); -} - -function captureWithNodeScreenshots(): ScreenshotResult { - if (!nodeScreenshots) { - throw new Error('node-screenshots not available'); - } - - const primaryDisplay = screen.getPrimaryDisplay(); - const centerX = Math.floor(primaryDisplay.workAreaSize.width / 2); - const centerY = Math.floor(primaryDisplay.workAreaSize.height / 2); - - const monitor = nodeScreenshots.Monitor.fromPoint(centerX, centerY); - if (!monitor) { - throw new Error('No monitor found for initial screenshot'); - } - - const fullImage = monitor.captureImageSync(); - const rawImageData = fullImage.toRawSync(); - - // Create a nativeImage from raw data and convert to data URL - const image = nativeImage.createFromBuffer(Buffer.from(rawImageData), { - width: fullImage.width, - height: fullImage.height, - }); - const dataURL = image.toDataURL(); - - return { - width: fullImage.width, - height: fullImage.height, - rawData: rawImageData, - dataURL, - monitorInfo: { - x: monitor.x, - y: monitor.y, - width: monitor.width, - height: monitor.height, - }, - }; -} - -async function tryScreenshotCommand(tempFile: string): Promise { - // Try gnome-screenshot first (works on both X11 and Wayland GNOME) - try { - console.log('[Screenshot Provider] Trying gnome-screenshot...'); - await execAsync(`gnome-screenshot -f "${tempFile}"`); - // Verify the file was actually created - const fs = await import('node:fs'); - if (!fs.existsSync(tempFile)) { - throw new Error('gnome-screenshot did not create the file'); - } - console.log('[Screenshot Provider] gnome-screenshot succeeded'); - return; - } catch (err) { - console.log('[Screenshot Provider] gnome-screenshot not available:', err); - } - - // Try grim (Wayland-native, works on wlroots-based compositors) - try { - console.log('[Screenshot Provider] Trying grim...'); - await execAsync(`grim "${tempFile}"`); - console.log('[Screenshot Provider] grim succeeded'); - return; - } catch (err) { - console.log('[Screenshot Provider] grim not available:', err); - } - - // Try spectacle (KDE screenshot tool, works on both X11 and Wayland) - try { - console.log('[Screenshot Provider] Trying spectacle...'); - await execAsync(`spectacle -b -n -o "${tempFile}"`); - console.log('[Screenshot Provider] spectacle succeeded'); - return; - } catch (err) { - console.log('[Screenshot Provider] spectacle not available:', err); - } - - // Try scrot (X11 only - won't work on Wayland) - try { - console.log('[Screenshot Provider] Trying scrot...'); - await execAsync(`scrot "${tempFile}"`); - console.log('[Screenshot Provider] scrot succeeded'); - return; - } catch (err) { - console.log('[Screenshot Provider] scrot not available:', err); - } - - // Try import from ImageMagick (X11 only) - try { - console.log('[Screenshot Provider] Trying import (ImageMagick)...'); - await execAsync(`import -window root "${tempFile}"`); - console.log('[Screenshot Provider] import succeeded'); - return; - } catch (err) { - console.log('[Screenshot Provider] import not available:', err); - } - - throw new Error( - 'No screenshot tool available. Please install one of: gnome-screenshot (GNOME), grim (Wayland), spectacle (KDE), scrot (X11), or imagemagick (X11)' - ); -} - -async function captureWithShellCommands(): Promise { - const tempFile = join( - app.getPath('temp'), - `swach-screenshot-${Date.now()}.png` - ); - - console.log('[Screenshot Provider] Capturing with shell commands...'); - console.log('[Screenshot Provider] Temp file:', tempFile); - - try { - await tryScreenshotCommand(tempFile); - console.log('[Screenshot Provider] Screenshot captured successfully'); - - // Read the PNG file using Electron's nativeImage - const image = nativeImage.createFromPath(tempFile); - if (image.isEmpty()) { - throw new Error('Failed to load screenshot image'); - } - - const size = image.getSize(); - console.log( - `[Screenshot Provider] Image size: ${size.width}x${size.height}` - ); - - // Get data URL from the image (this preserves the original PNG) - const dataURL = image.toDataURL(); - console.log(`[Screenshot Provider] Data URL length: ${dataURL.length}`); - console.log( - `[Screenshot Provider] Data URL starts with: ${dataURL.substring(0, 50)}` - ); - - // Get raw BGRA bitmap data (Electron uses BGRA format) - const bitmap = image.toBitmap(); - console.log(`[Screenshot Provider] Bitmap size: ${bitmap.length} bytes`); - - // Convert BGRA to RGBA for pixel reading - const rawData = new Uint8Array(bitmap.length); - for (let i = 0; i < bitmap.length; i += 4) { - rawData[i] = bitmap[i + 2]!; // R <- B - rawData[i + 1] = bitmap[i + 1]!; // G <- G - rawData[i + 2] = bitmap[i]!; // B <- R - rawData[i + 3] = bitmap[i + 3]!; // A <- A - } - - // Get monitor info from Electron's screen API - const primaryDisplay = screen.getPrimaryDisplay(); - console.log( - '[Screenshot Provider] Primary display:', - primaryDisplay.bounds - ); - - return { - width: size.width, - height: size.height, - rawData, - dataURL, - monitorInfo: { - x: primaryDisplay.bounds.x, - y: primaryDisplay.bounds.y, - width: primaryDisplay.bounds.width, - height: primaryDisplay.bounds.height, - }, - }; - } catch (error) { - console.error('[Screenshot Provider] Error capturing screenshot:', error); - throw error; - } finally { - // Clean up temp file - try { - unlinkSync(tempFile); - console.log('[Screenshot Provider] Temp file cleaned up'); - } catch { - // Ignore cleanup errors - } - } -} - -export { captureScreenshotWithFallback, type ScreenshotResult }; From 3c550c921880082376b914b785ba2b26811a1b11 Mon Sep 17 00:00:00 2001 From: Robert Wagner Date: Fri, 12 Dec 2025 11:06:43 -0500 Subject: [PATCH 6/6] Remove unused files --- electron-app/magnifier/magnifier-main.ts | 33 -- electron-app/resources/fullscreen-picker.html | 303 ------------------ 2 files changed, 336 deletions(-) delete mode 100644 electron-app/resources/fullscreen-picker.html diff --git a/electron-app/magnifier/magnifier-main.ts b/electron-app/magnifier/magnifier-main.ts index 8b3745dec..2a21dc9de 100644 --- a/electron-app/magnifier/magnifier-main.ts +++ b/electron-app/magnifier/magnifier-main.ts @@ -93,14 +93,6 @@ class MagnifyingColorPicker { const cursorPos = screen.getCursorScreenPoint(); const display = screen.getDisplayNearestPoint(cursorPos); - console.log('[DEBUG] Cursor position:', cursorPos); - console.log('[DEBUG] Display info:', { - id: display.id, - bounds: display.bounds, - size: display.size, - scaleFactor: display.scaleFactor, - }); - const sources = await desktopCapturer.getSources({ types: ['screen'], thumbnailSize: { @@ -109,24 +101,6 @@ class MagnifyingColorPicker { }, }); - console.log( - '[DEBUG] desktopCapturer.getSources() returned:', - sources.length, - 'sources' - ); - sources.forEach((s, index) => { - console.log(`[DEBUG] Source ${index}:`, { - id: s.id, - name: s.name, - display_id: s.display_id, - appIcon: s.appIcon ? 'present' : 'null', - thumbnail: s.thumbnail - ? `${s.thumbnail.getSize().width}x${s.thumbnail.getSize().height}` - : 'null', - }); - }); - console.log('[DEBUG] Looking for display_id:', display.id.toString()); - // Find the source that matches the display under the cursor let source = sources.find((s) => s.display_id === display.id.toString()); @@ -148,13 +122,6 @@ class MagnifyingColorPicker { throw new Error(`No screen source found for display ${display.id}`); } - console.log( - '[DEBUG] Using source:', - source.id, - 'display_id:', - source.display_id - ); - const nativeImage = source.thumbnail; const bitmap = nativeImage.toBitmap(); diff --git a/electron-app/resources/fullscreen-picker.html b/electron-app/resources/fullscreen-picker.html deleted file mode 100644 index 8d1dea078..000000000 --- a/electron-app/resources/fullscreen-picker.html +++ /dev/null @@ -1,303 +0,0 @@ - - - - - - - -
-
-
-
- Color: #FFFFFF -
-
Click to select color | ESC to cancel
- - - -