Skip to content

fix: run Cap desktop + CLI on Linux (Studio Mode + CLI E2E)#1923

Merged
richiemcilroy merged 5 commits into
mainfrom
cursor/linux-desktop-cli-e2e-3c50
Jun 19, 2026
Merged

fix: run Cap desktop + CLI on Linux (Studio Mode + CLI E2E)#1923
richiemcilroy merged 5 commits into
mainfrom
cursor/linux-desktop-cli-e2e-3c50

Conversation

@richiemcilroy

@richiemcilroy richiemcilroy commented Jun 18, 2026

Copy link
Copy Markdown
Member

Overview

Gets the Linux build of Cap Desktop and the cap CLI compiling and running end-to-end, and validates Studio Mode recording → editor → export plus the full CLI record/export flow on Linux (X11 capture via x11grab, encode via system libav, rendering via wgpu on llvmpipe).

The Linux target had several gaps that prevented cap-desktop from compiling/running (it had only ever been built on macOS/Windows). This PR fills those gaps with platform-gated code only — no behavior change on macOS/Windows.

Changes

  • crates/recording/src/diagnostics.rs — add a Linux SystemDiagnostics + collect_diagnostics() (kernel version, available ffmpeg encoders, GPU name) so the desktop's diagnostics command compiles and works on Linux.
  • apps/desktop/src-tauri/src/windows.rs — add Linux branches for the window-capture occluder position, the in-progress-recording window builder, MonitorExt::intersects, and is_system_dark_mode() (via gsettings).
  • apps/desktop/src-tauri/src/thumbnails/ — add a Linux thumbnail module that reuses the existing public cap_recording::screenshot::capture_screenshot (X11) to produce display/window thumbnails.
  • apps/desktop/src-tauri/src/lib.rs — skip tauri-plugin-single-instance on Linux; its blocking zbus init panics ("Cannot start a runtime from within a runtime") when initialized inside the Tokio runtime.
  • apps/desktop/src/App.tsxsrc/app.tsx — rename so SolidStart's #start/app import resolves on case-sensitive filesystems. It only ever worked on macOS due to case-insensitive matching; on Linux the dev server failed with Failed to resolve import "#start/app".

Note: building the workspace on Linux also needs a linker workaround for libavdevice → libcaca → libncursesw (force-link ncursesw/tinfo, since the default linker won't follow libcaca's DT_NEEDED). This was applied as host cargo config during testing rather than committed, to avoid clashing with scripts/setup.js, which manages .cargo/config.toml.

Testing

Cap CLI — record + export (X11)

cap doctorcaptureReady: true; ✅ cap targets; ✅ cap record start --detach / record stop (recordingMetaExists: true); ✅ cap project validate (valid, studio); ✅ cap export → valid 18.5s H.264+AAC MP4; ✅ cap screenshot.

CLI studio export rendered frame

Cap Desktop — Studio Mode end-to-end

Record → editor opens automatically → export to a valid MP4.

cap_linux_studio_mode_record_to_editor.mp4

Main recording window and the editor (preview composited via wgpu/llvmpipe, timeline, background sidebar):

Main recording window
Cap editor with studio recording

Settings (all tabs render; Feedback tab shows the new Linux diagnostics encoders) and the desktop export completing:

Settings - General
Editor export complete

✅ Desktop editor export produced /home/ubuntu/Cap 2026-06-18 at 21.53.54.mp4 (H.264 1152x720 + AAC, 21.2s), confirmed with ffprobe.

To show artifacts inline, enable in settings.

Open in Web Open in Cursor 

Greptile Summary

This PR brings the Cap desktop app and CLI to Linux end-to-end by adding platform-gated code for X11 capture, window management, thumbnails, and diagnostics — with no behavior change on macOS or Windows.

  • Window management (windows.rs): Adds Linux branches for overlay positioning, in-progress-recording window builder, MonitorExt::intersects, and dark-mode detection via gsettings; the post-build size-setting blocks pass physical pixel values into LogicalSize, which will produce over-sized windows on HiDPI displays. Dark-mode detection is also GNOME-only and falls back silently to light mode on other DEs.
  • Thumbnails & diagnostics: New thumbnails/linux.rs reuses capture_screenshot with graceful error handling; diagnostics.rs adds a linux_impl for kernel/GPU/encoder info with screen_capture_supported hardcoded true.
  • Infrastructure fixes: Single-instance plugin skipped on Linux to avoid a zbus panic; App.tsxapp.tsx rename for case-sensitive filesystems; vendored tao unrealized-GDK-window crash replaced with a safe None check.

Confidence Score: 4/5

Safe to merge as a platform enablement PR; all non-Linux paths are unchanged and the Linux-specific code is cfg-gated throughout.

The window sizing code passes physical pixel values as LogicalSize in the Linux overlay and capture-target blocks, producing incorrectly-sized windows on HiDPI Linux setups. At 1x X11 scale (the tested configuration) this is invisible, but it is a latent issue for the next HiDPI user. Everything else is clean and well-tested.

apps/desktop/src-tauri/src/windows.rs — the two Linux post-build blocks that call set_size(LogicalSize::new(physical_width, physical_height))

Important Files Changed

Filename Overview
apps/desktop/src-tauri/src/windows.rs Adds Linux branches for overlay positioning, in-progress-recording window builder, MonitorExt::intersects, and dark-mode detection; physical values are passed as LogicalSize in the post-build blocks which will missize windows on HiDPI displays, and dark-mode detection only covers GNOME.
apps/desktop/src-tauri/src/thumbnails/linux.rs New Linux thumbnail module using cap_recording::screenshot::capture_screenshot; gracefully returns None on capture or encode errors; well-structured.
apps/desktop/src-tauri/src/thumbnails/mod.rs Adds cfg-gated linux module re-export alongside the existing macos/windows modules; no issues.
apps/desktop/src-tauri/src/lib.rs Single-instance plugin skipped on Linux with a clear comment explaining the zbus blocking call panic; safe and well-documented trade-off.
crates/recording/src/diagnostics.rs Adds Linux SystemDiagnostics with kernel version, available encoders, and GPU name; screen_capture_supported is hardcoded true without probing actual capture availability.
vendor/tao/src/platform_impl/linux/event_loop.rs Replaces unwrap() on GDK window with a graceful None check to prevent panic when set_ignore_cursor_events is called before the window is realized.
apps/desktop/src/app.tsx Lowercase rename from App.tsx to app.tsx to fix case-sensitive filesystem import resolution on Linux; no logic changes.
apps/desktop/src/routes/(window-chrome).tsx Adds Linux detection to render CaptionControlsMacOS (close-only) for Linux, matching decorations: false window style; straightforward.
apps/desktop/src/routes/editor/Header.tsx Adds CaptionControlsMacOS for Linux in the editor header in place of the macOS spacer div; no issues.
apps/desktop/src/routes/screenshot-editor/Header.tsx Adds CaptionControlsMacOS for Linux in the screenshot-editor header; mirrors the editor change consistently.
Prompt To Fix All With AI
Fix the following 3 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 3
apps/desktop/src-tauri/src/windows.rs:1610-1617
**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).

### Issue 2 of 3
apps/desktop/src-tauri/src/windows.rs:91-101
**Dark-mode detection is GNOME-only**

`gsettings` queries `org.gnome.desktop.interface` which 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 returns `false`, forcing light mode for all non-GNOME Linux users. A fallback via `XDG_CURRENT_DESKTOP` or the cross-DE `org.freedesktop.portal.Settings` D-Bus portal would widen coverage.

### Issue 3 of 3
crates/recording/src/diagnostics.rs:569-573
**`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.

Reviews (1): Last reviewed commit: "style: rustfmt windows.rs Linux capture-..." | Re-trigger Greptile

Greptile also left 3 inline comments on this PR.

- Add Linux branches for occluder/in-progress-recording window builders,
  intersects(), and is_system_dark_mode() in windows.rs
- Add Linux SystemDiagnostics + collect_diagnostics() to cap-recording
- Add Linux thumbnail capture reusing cap_recording::screenshot
…entry case

- Gate tauri-plugin-single-instance off on Linux (blocking zbus init panics
  inside the Tokio runtime)
- Rename src/App.tsx -> src/app.tsx so SolidStart's #start/app resolves on
  case-sensitive filesystems (worked on macOS only due to case-insensitivity)
… Linux

- Drop WM decorations on Linux and draw macOS-style HTML traffic lights in
  the window-chrome, editor, and screenshot-editor headers (removes the
  native titlebar and stray window-manager cog button)
- Size/position the target-select overlay and capture-area window to the full
  target display on Linux (were defaulting to a small partial window)
…inux

set_ignore_cursor_events(true) on a not-yet-realized window aborted the app
(non-unwinding panic in the GTK event loop) during window-capture recording.
Skip applying the input shape until the GDK window exists.
@richiemcilroy richiemcilroy force-pushed the cursor/linux-desktop-cli-e2e-3c50 branch from d07f778 to 9db6c54 Compare June 19, 2026 06:47
@richiemcilroy richiemcilroy marked this pull request as ready for review June 19, 2026 06:55
@superagent-security

Copy link
Copy Markdown

Superagent didn't find any vulnerabilities or security issues in this PR.

Comment on lines +1610 to +1617
#[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()));
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 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).

Prompt To Fix With AI
This 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.

Comment on lines +91 to +101
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
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Dark-mode detection is GNOME-only

gsettings queries org.gnome.desktop.interface which 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 returns false, forcing light mode for all non-GNOME Linux users. A fallback via XDG_CURRENT_DESKTOP or the cross-DE org.freedesktop.portal.Settings D-Bus portal would widen coverage.

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop/src-tauri/src/windows.rs
Line: 91-101

Comment:
**Dark-mode detection is GNOME-only**

`gsettings` queries `org.gnome.desktop.interface` which 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 returns `false`, forcing light mode for all non-GNOME Linux users. A fallback via `XDG_CURRENT_DESKTOP` or the cross-DE `org.freedesktop.portal.Settings` D-Bus portal would widen coverage.

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +569 to +573
screen_capture_supported: true,
gpu_name,
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 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.

Prompt To Fix With AI
This 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.

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor nit: this is using PhysicalPosition but then sets size via LogicalSize with physical dimensions. If you intend physical coordinates here, consider pairing it with PhysicalSize for clarity/consistency.

Suggested change
let _ = window.set_size(LogicalSize::new(size.width(), size.height()));
use tauri::{PhysicalPosition, PhysicalSize};
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(PhysicalSize::new(size.width(), size.height()));

}

fn get_gpu_name() -> Option<String> {
let output = std::process::Command::new("glxinfo")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

glxinfo -B can be surprisingly slow / noisy in headless contexts (no session/display). Might be worth short-circuiting if there’s no display env so cap doctor stays snappy.

Suggested change
let output = std::process::Command::new("glxinfo")
if std::env::var_os("DISPLAY").is_none() && std::env::var_os("WAYLAND_DISPLAY").is_none() {
return None;
}
let output = std::process::Command::new("glxinfo")
.arg("-B")
.output()
.ok()?;

@richiemcilroy richiemcilroy merged commit a8d6788 into main Jun 19, 2026
20 of 21 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant