-
Notifications
You must be signed in to change notification settings - Fork 1.6k
fix: run Cap desktop + CLI on Linux (Studio Mode + CLI E2E) #1923
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
42717ea
c1175df
3bb564f
6b09650
9db6c54
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,47 @@ | ||
| use cap_recording::screenshot::capture_screenshot; | ||
| use cap_recording::sources::screen_capture::ScreenCaptureTarget; | ||
| use image::{ImageEncoder, codecs::png::PngEncoder}; | ||
| use std::io::Cursor; | ||
|
|
||
| use super::*; | ||
|
|
||
| pub async fn capture_display_thumbnail(display: &scap_targets::Display) -> Option<String> { | ||
| capture_target_thumbnail(ScreenCaptureTarget::Display { id: display.id() }).await | ||
| } | ||
|
|
||
| pub async fn capture_window_thumbnail(window: &scap_targets::Window) -> Option<String> { | ||
| capture_target_thumbnail(ScreenCaptureTarget::Window { id: window.id() }).await | ||
| } | ||
|
|
||
| async fn capture_target_thumbnail(target: ScreenCaptureTarget) -> Option<String> { | ||
| let image = match capture_screenshot(target).await { | ||
| Ok(image) => image, | ||
| Err(error) => { | ||
| warn!(error = %error, "Failed to capture thumbnail on Linux"); | ||
| return None; | ||
| } | ||
| }; | ||
|
|
||
| let rgba = image.to_rgba8(); | ||
| if rgba.width() == 0 || rgba.height() == 0 { | ||
| return None; | ||
| } | ||
|
|
||
| let thumbnail = normalize_thumbnail_dimensions(&rgba); | ||
| let mut png_data = Cursor::new(Vec::new()); | ||
| let encoder = PngEncoder::new(&mut png_data); | ||
| if let Err(error) = encoder.write_image( | ||
| thumbnail.as_raw(), | ||
| thumbnail.width(), | ||
| thumbnail.height(), | ||
| image::ColorType::Rgba8.into(), | ||
| ) { | ||
| warn!(error = %error, "Failed to encode Linux thumbnail as PNG"); | ||
| return None; | ||
| } | ||
|
|
||
| Some(base64::Engine::encode( | ||
| &base64::engine::general_purpose::STANDARD, | ||
| png_data.into_inner(), | ||
| )) | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -87,6 +87,19 @@ fn is_system_dark_mode() -> bool { | |||||||||||||
| false | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| #[cfg(target_os = "linux")] | ||||||||||||||
| fn is_system_dark_mode() -> bool { | ||||||||||||||
| let output = std::process::Command::new("gsettings") | ||||||||||||||
| .args(["get", "org.gnome.desktop.interface", "color-scheme"]) | ||||||||||||||
| .output(); | ||||||||||||||
| if let Ok(output) = output | ||||||||||||||
| && output.status.success() | ||||||||||||||
| { | ||||||||||||||
| return String::from_utf8_lossy(&output.stdout).contains("dark"); | ||||||||||||||
| } | ||||||||||||||
| false | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| pub fn hide_overlay(window: &WebviewWindow) { | ||||||||||||||
| let _ = window.set_ignore_cursor_events(true); | ||||||||||||||
| let _ = window.hide(); | ||||||||||||||
|
|
@@ -1582,9 +1595,27 @@ impl ShowCapWindow { | |||||||||||||
| window_builder = window_builder.inner_size(100.0, 100.0).position(0.0, 0.0); | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| #[cfg(target_os = "linux")] | ||||||||||||||
| { | ||||||||||||||
| let position = display.raw_handle().physical_position().unwrap(); | ||||||||||||||
| let size = display.physical_size().unwrap(); | ||||||||||||||
| window_builder = window_builder | ||||||||||||||
| .inner_size(size.width(), size.height()) | ||||||||||||||
| .position(position.x(), position.y()); | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| let window = window_builder.build()?; | ||||||||||||||
| lock_window_text_scale(&window); | ||||||||||||||
|
|
||||||||||||||
| #[cfg(target_os = "linux")] | ||||||||||||||
| { | ||||||||||||||
| use tauri::{LogicalSize, PhysicalPosition}; | ||||||||||||||
| let position = display.raw_handle().physical_position().unwrap(); | ||||||||||||||
| let size = display.physical_size().unwrap(); | ||||||||||||||
| let _ = window.set_position(PhysicalPosition::new(position.x(), position.y())); | ||||||||||||||
| let _ = window.set_size(LogicalSize::new(size.width(), size.height())); | ||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Minor nit: this is using
Suggested change
|
||||||||||||||
| } | ||||||||||||||
|
Comment on lines
+1610
to
+1617
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Both Prompt To Fix With AIThis is a comment left during a code review.
Path: apps/desktop/src-tauri/src/windows.rs
Line: 1610-1617
Comment:
**Physical values passed as `LogicalSize`**
Both `TargetSelectOverlay` and `CaptureTarget` Linux post-build blocks derive `size` from `physical_size()` / `physical_bounds()` but pass those values into `LogicalSize::new(...)`. On a 2× HiDPI display (fractional scaling via X11 or Wayland XWayland), the window will be rendered twice as large as the display. The Windows branch correctly obtains `logical_size` from `display.logical_size()` and uses that. The same fix should apply to both Linux post-build blocks (lines 1610–1617 and the analogous block around line 2330).
How can I resolve this? If you propose a fix, please make it concise. |
||||||||||||||
|
|
||||||||||||||
| #[cfg(windows)] | ||||||||||||||
| { | ||||||||||||||
| let position = display.raw_handle().physical_position().unwrap(); | ||||||||||||||
|
|
@@ -2218,6 +2249,9 @@ impl ShowCapWindow { | |||||||||||||
| #[cfg(windows)] | ||||||||||||||
| let position = display.raw_handle().physical_position().unwrap(); | ||||||||||||||
|
|
||||||||||||||
| #[cfg(target_os = "linux")] | ||||||||||||||
| let position = display.raw_handle().physical_position().unwrap(); | ||||||||||||||
|
|
||||||||||||||
| let bounds = display.physical_size().unwrap(); | ||||||||||||||
|
|
||||||||||||||
| let mut window_builder = self | ||||||||||||||
|
|
@@ -2281,9 +2315,29 @@ impl ShowCapWindow { | |||||||||||||
| .position(bounds.position().x(), bounds.position().y()); | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| #[cfg(target_os = "linux")] | ||||||||||||||
| if let Some(bounds) = display.raw_handle().physical_bounds() { | ||||||||||||||
| window_builder = window_builder | ||||||||||||||
| .inner_size(bounds.size().width(), bounds.size().height()) | ||||||||||||||
| .position(bounds.position().x(), bounds.position().y()); | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| let window = window_builder.build()?; | ||||||||||||||
| lock_window_text_scale(&window); | ||||||||||||||
|
|
||||||||||||||
| #[cfg(target_os = "linux")] | ||||||||||||||
| if let Some(bounds) = display.raw_handle().physical_bounds() { | ||||||||||||||
| use tauri::{LogicalSize, PhysicalPosition}; | ||||||||||||||
| let _ = window.set_position(PhysicalPosition::new( | ||||||||||||||
| bounds.position().x(), | ||||||||||||||
| bounds.position().y(), | ||||||||||||||
| )); | ||||||||||||||
| let _ = window.set_size(LogicalSize::new( | ||||||||||||||
| bounds.size().width(), | ||||||||||||||
| bounds.size().height(), | ||||||||||||||
| )); | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| #[cfg(target_os = "macos")] | ||||||||||||||
| crate::platform::set_window_level( | ||||||||||||||
| window.as_ref().window(), | ||||||||||||||
|
|
@@ -2355,6 +2409,25 @@ impl ShowCapWindow { | |||||||||||||
| )) | ||||||||||||||
| .build()?; | ||||||||||||||
|
|
||||||||||||||
| #[cfg(target_os = "linux")] | ||||||||||||||
| let window = self | ||||||||||||||
| .window_builder(app, "/in-progress-recording") | ||||||||||||||
| .maximized(false) | ||||||||||||||
| .resizable(false) | ||||||||||||||
| .fullscreen(false) | ||||||||||||||
| .shadow(false) | ||||||||||||||
| .always_on_top(true) | ||||||||||||||
| .transparent(true) | ||||||||||||||
| .visible_on_all_workspaces(true) | ||||||||||||||
| .content_protected(should_protect) | ||||||||||||||
| .inner_size(width, height) | ||||||||||||||
| .skip_taskbar(false) | ||||||||||||||
| .initialization_script(format!( | ||||||||||||||
| "window.COUNTDOWN = {};", | ||||||||||||||
| countdown.unwrap_or_default() | ||||||||||||||
| )) | ||||||||||||||
| .build()?; | ||||||||||||||
|
|
||||||||||||||
| lock_window_text_scale(&window); | ||||||||||||||
|
|
||||||||||||||
| #[cfg(target_os = "windows")] | ||||||||||||||
|
|
@@ -2605,6 +2678,13 @@ impl ShowCapWindow { | |||||||||||||
| builder = builder.decorations(false).zoom_hotkeys_enabled(false); | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| // Linux has no native macOS-style traffic lights, so we drop the window | ||||||||||||||
| // manager decorations and draw our own chrome (matching the macOS layout). | ||||||||||||||
| #[cfg(target_os = "linux")] | ||||||||||||||
| { | ||||||||||||||
| builder = builder.decorations(false); | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| builder | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
|
|
@@ -2984,6 +3064,30 @@ impl MonitorExt for Display { | |||||||||||||
| .into_iter() | ||||||||||||||
| .any(|(x, y)| x >= left && x < right && y >= top && y < bottom) | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| #[cfg(target_os = "linux")] | ||||||||||||||
| { | ||||||||||||||
| let Some(bounds) = self.raw_handle().physical_bounds() else { | ||||||||||||||
| return false; | ||||||||||||||
| }; | ||||||||||||||
|
|
||||||||||||||
| let left = bounds.position().x() as i32; | ||||||||||||||
| let right = left + bounds.size().width() as i32; | ||||||||||||||
| let top = bounds.position().y() as i32; | ||||||||||||||
| let bottom = top + bounds.size().height() as i32; | ||||||||||||||
|
|
||||||||||||||
| [ | ||||||||||||||
| (position.x, position.y), | ||||||||||||||
| (position.x + size.width as i32, position.y), | ||||||||||||||
| (position.x, position.y + size.height as i32), | ||||||||||||||
| ( | ||||||||||||||
| position.x + size.width as i32, | ||||||||||||||
| position.y + size.height as i32, | ||||||||||||||
| ), | ||||||||||||||
| ] | ||||||||||||||
| .into_iter() | ||||||||||||||
| .any(|(x, y)| x >= left && x < right && y >= top && y < bottom) | ||||||||||||||
| } | ||||||||||||||
| } | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
|
|
||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -536,8 +536,91 @@ mod macos_impl { | |||||||||||||||||||
| } | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| #[cfg(target_os = "linux")] | ||||||||||||||||||||
| mod linux_impl { | ||||||||||||||||||||
| use super::*; | ||||||||||||||||||||
|
|
||||||||||||||||||||
| #[derive(Debug, Clone, Serialize, Type)] | ||||||||||||||||||||
| #[serde(rename_all = "camelCase")] | ||||||||||||||||||||
| pub struct SystemDiagnostics { | ||||||||||||||||||||
| pub kernel_version: Option<String>, | ||||||||||||||||||||
| pub available_encoders: Vec<String>, | ||||||||||||||||||||
| pub screen_capture_supported: bool, | ||||||||||||||||||||
| pub gpu_name: Option<String>, | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| pub fn collect_diagnostics() -> SystemDiagnostics { | ||||||||||||||||||||
| let kernel_version = get_kernel_version(); | ||||||||||||||||||||
| let available_encoders = get_available_encoders(); | ||||||||||||||||||||
| let gpu_name = get_gpu_name(); | ||||||||||||||||||||
|
|
||||||||||||||||||||
| tracing::info!("System Diagnostics:"); | ||||||||||||||||||||
| if let Some(ref version) = kernel_version { | ||||||||||||||||||||
| tracing::info!(" Kernel: {}", version); | ||||||||||||||||||||
| } | ||||||||||||||||||||
| if let Some(ref gpu) = gpu_name { | ||||||||||||||||||||
| tracing::info!(" GPU: {}", gpu); | ||||||||||||||||||||
| } | ||||||||||||||||||||
| tracing::info!(" Encoders: {:?}", available_encoders); | ||||||||||||||||||||
|
|
||||||||||||||||||||
| SystemDiagnostics { | ||||||||||||||||||||
| kernel_version, | ||||||||||||||||||||
| available_encoders, | ||||||||||||||||||||
| screen_capture_supported: true, | ||||||||||||||||||||
| gpu_name, | ||||||||||||||||||||
| } | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
|
Comment on lines
+569
to
+573
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The Linux Prompt To Fix With AIThis is a comment left during a code review.
Path: crates/recording/src/diagnostics.rs
Line: 569-573
Comment:
**`screen_capture_supported` is hardcoded `true`**
The Linux `SystemDiagnostics` struct always sets `screen_capture_supported: true` without probing capture availability. On a pure Wayland compositor without XWayland or a headless CI container, `cap doctor` will incorrectly report `captureReady: true`. Checking the `DISPLAY` env var or attempting a trial capture would make this accurate.
How can I resolve this? If you propose a fix, please make it concise. |
||||||||||||||||||||
| fn get_kernel_version() -> Option<String> { | ||||||||||||||||||||
| std::fs::read_to_string("/proc/sys/kernel/osrelease") | ||||||||||||||||||||
| .ok() | ||||||||||||||||||||
| .map(|value| value.trim().to_string()) | ||||||||||||||||||||
| .filter(|value| !value.is_empty()) | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| fn get_gpu_name() -> Option<String> { | ||||||||||||||||||||
| let output = std::process::Command::new("glxinfo") | ||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||||||
| .arg("-B") | ||||||||||||||||||||
| .output() | ||||||||||||||||||||
| .ok()?; | ||||||||||||||||||||
| if !output.status.success() { | ||||||||||||||||||||
| return None; | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| String::from_utf8_lossy(&output.stdout) | ||||||||||||||||||||
| .lines() | ||||||||||||||||||||
| .find_map(|line| { | ||||||||||||||||||||
| line.trim() | ||||||||||||||||||||
| .strip_prefix("OpenGL renderer string:") | ||||||||||||||||||||
| .map(|name| name.trim().to_string()) | ||||||||||||||||||||
| }) | ||||||||||||||||||||
| .filter(|name| !name.is_empty()) | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| fn get_available_encoders() -> Vec<String> { | ||||||||||||||||||||
| let candidates = [ | ||||||||||||||||||||
| "h264_nvenc", | ||||||||||||||||||||
| "h264_vaapi", | ||||||||||||||||||||
| "h264_qsv", | ||||||||||||||||||||
| "libx264", | ||||||||||||||||||||
| "hevc_nvenc", | ||||||||||||||||||||
| "hevc_vaapi", | ||||||||||||||||||||
| "libx265", | ||||||||||||||||||||
| ]; | ||||||||||||||||||||
|
|
||||||||||||||||||||
| candidates | ||||||||||||||||||||
| .iter() | ||||||||||||||||||||
| .filter(|name| ffmpeg::encoder::find_by_name(name).is_some()) | ||||||||||||||||||||
| .map(|s| s.to_string()) | ||||||||||||||||||||
| .collect() | ||||||||||||||||||||
| } | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| #[cfg(target_os = "windows")] | ||||||||||||||||||||
| pub use windows_impl::*; | ||||||||||||||||||||
|
|
||||||||||||||||||||
| #[cfg(target_os = "macos")] | ||||||||||||||||||||
| pub use macos_impl::*; | ||||||||||||||||||||
|
|
||||||||||||||||||||
| #[cfg(target_os = "linux")] | ||||||||||||||||||||
| pub use linux_impl::*; | ||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
gsettingsqueriesorg.gnome.desktop.interfacewhich only exists on GNOME/libgnome-based desktops. On KDE Plasma, XFCE, Cinnamon, and bare window-manager setups the command will fail and the function silently returnsfalse, forcing light mode for all non-GNOME Linux users. A fallback viaXDG_CURRENT_DESKTOPor the cross-DEorg.freedesktop.portal.SettingsD-Bus portal would widen coverage.Prompt To Fix With AI