Skip to content
Merged
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 Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ workspace = true
[dependencies]
# Wayland
wayland-client = "0.31"
wayland-protocols = { version = "0.32", features = ["client", "unstable"] }
wayland-protocols = { version = "0.32", features = ["client", "staging", "unstable"] }
wayland-protocols-wlr = { version = "0.3", features = ["client"] }
smithay-client-toolkit = { version = "0.20", default-features = false, features = ["calloop", "xkbcommon"] }

Expand Down Expand Up @@ -53,6 +53,7 @@ tokio = { version = "1.0", features = ["rt-multi-thread", "macros", "time", "syn

# Screenshot capture
zbus = { version = "5.0", optional = true, default-features = false, features = ["tokio"] }
getrandom = { version = "0.3", optional = true }
serde_json = "1.0"
png = "0.18"

Expand All @@ -72,7 +73,7 @@ xkbcommon = { version = "0.8", optional = true }
tablet-input = []
# D-Bus dependent functionality (portal capture, notifications, tray)
dbus = ["zbus"]
portal = ["dbus"]
portal = ["dbus", "dep:getrandom"]
tray = ["dbus", "ksni"]
config-schema = ["dep:schemars"]
# GTK4-rendered toolbars on layer-shell compositors; the built-in Cairo
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ The v0.9.23+ prebuilt `wayscriber` packages require glibc 2.39 and GTK 4.12 —
- Presenter mode (<kbd>Ctrl+Shift+M</kbd>): hides UI, forces click highlights
- Input HUD (<kbd>Ctrl+Shift+K</kbd>): on-screen keystroke and click chips for demos and screencasts (opt-in system-wide capture via the `input-monitor` build feature — see [docs/CONFIG.md](docs/CONFIG.md#uiinput_hud---input-hud-keystrokes-and-clicks))
- Light passthrough (layer-shell): draw while input passes through to the app underneath — see [Light passthrough mode](#light-passthrough-mode)
- Screen freeze (<kbd>Ctrl+Shift+F</kbd>): pause the display while apps keep running. On GNOME, this uses the screenshot portal when available
- Screen freeze (<kbd>Ctrl+Shift+F</kbd>): pause the display while apps keep running. Freeze prefers compositor-native `wlr-screencopy` or `ext-image-copy-capture` and falls back to the screenshot portal when available
- Spotlight: drag an ellipse to dim everything around it; stack several to highlight multiple areas. Dim strength and edge softness are configurable under `[spotlight]`

### Callouts and zoom
Expand Down
2 changes: 1 addition & 1 deletion docs/SETUP.md
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ Then use the configurator's Daemon tab, or create a GNOME custom shortcut that r
wayscriber --daemon-toggle
```

Freeze works on GNOME when the screenshot portal is available and responsive; the first use may show a desktop permission prompt. Portal capture can be slower than compositor screencopy, and mixed-DPI or multi-monitor setups may depend on client-side crop behavior.
Freeze prefers compositor-native `wlr-screencopy` or `ext-image-copy-capture` when either protocol is available, then falls back to the screenshot portal. On GNOME, Freeze works when that portal is available and responsive; the first use may show a desktop permission prompt. Portal capture can be slower than direct compositor capture, and mixed-DPI or multi-monitor setups may depend on client-side crop behavior.

Light passthrough mode is not available in the regular app on stock GNOME Wayland. GNOME's xdg-shell fallback does not expose the shell-level overlay behavior needed to keep annotations visible while input goes to apps underneath, so `--light-toggle` is intentionally disabled instead of pretending to pass input through. A GNOME Shell extension companion would be the real path for that workflow.

Expand Down
2 changes: 2 additions & 0 deletions docs/codebase-overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@ Daemon mode therefore provides a persistent background service that reacts to us

`WaylandState` centralizes everything the handlers need: current buffers, Cairo context, mouse positions, capture state, and tokio handle for async work.

Freeze capture waits for the overlay-suppression frame, then selects `wlr-screencopy`, `ext-image-copy-capture`, or the screenshot portal in that order. The two direct protocols capture the active output into shared memory; the portal captures the desktop and the client crops the selected output when needed.

---

## 4. Input Handling & Drawing State
Expand Down
32 changes: 17 additions & 15 deletions src/backend/wayland/backend/event_loop/capture.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,18 +23,29 @@ pub(super) fn poll_portal_captures(state: &mut WaylandState, now: Instant) {
state.apply_capture_completion();
}

pub(super) fn poll_capture_deadlines(state: &mut WaylandState, now: Instant) {
pub(super) fn poll_capture_deadlines(
state: &mut WaylandState,
qh: &wayland_client::QueueHandle<WaylandState>,
now: Instant,
) {
state.poll_overlay_capture_barrier_timeout(now);
if let Some(backend) = state.frozen.take_timed_out_direct_capture(now) {
warn!("{backend:?} frozen capture timed out; trying the next backend");
state.continue_frozen_capture_after_failure(backend, qh);
}
}

pub(super) fn capture_timeout(state: &WaylandState, now: Instant) -> Option<Duration> {
super::min_timeout(
state.overlay_capture_barrier_timeout(now),
super::min_timeout(
state.frozen.portal_timeout(now),
state.frozen.direct_capture_timeout(now),
super::min_timeout(
state.zoom.portal_timeout(now),
state.xdg_frozen_fullscreen_timeout(now),
state.frozen.portal_timeout(now),
super::min_timeout(
state.zoom.portal_timeout(now),
state.xdg_frozen_fullscreen_timeout(now),
),
),
),
)
Expand Down Expand Up @@ -146,7 +157,7 @@ fn handle_frozen_toggle(state: &mut WaylandState) {

if !state.frozen_enabled() {
warn!(
"Frozen mode unavailable: no screencopy backend and no screenshot portal backend; ignoring toggle"
"Frozen mode unavailable: no direct capture backend and no screenshot portal backend; ignoring toggle"
);
state.input_state.push_toast(
ToastPriority::Info,
Expand All @@ -159,12 +170,6 @@ fn handle_frozen_toggle(state: &mut WaylandState) {
state.restore_xdg_after_frozen();
state.frozen.unfreeze(&mut state.input_state);
} else {
let use_fallback = !state.frozen.manager_available();
if use_fallback {
warn!("Frozen mode: screencopy unavailable, using portal fallback");
} else {
info!("Frozen mode: using screencopy fast path");
}
if !state.enter_overlay_suppression(OverlaySuppression::Frozen) {
warn!("Frozen mode requested while overlay is suppressed; ignoring toggle");
state.input_state.push_toast(
Expand All @@ -174,10 +179,7 @@ fn handle_frozen_toggle(state: &mut WaylandState) {
);
return;
}
if let Err(err) = state
.frozen
.start_capture(use_fallback, &state.tokio_handle)
{
if let Err(err) = state.frozen.start_capture() {
warn!("Frozen capture failed to start: {}", err);
state.exit_overlay_suppression(OverlaySuppression::Frozen);
state.frozen.cancel(&mut state.input_state);
Expand Down
2 changes: 1 addition & 1 deletion src/backend/wayland/backend/event_loop/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ pub(super) fn run_event_loop(
// A capture-barrier deadline may be what woke dispatch. Apply its
// recovery before this iteration reaches toolbar synchronization and
// rendering so the restored frame is not delayed by another block.
capture::poll_capture_deadlines(state, Instant::now());
capture::poll_capture_deadlines(state, qh, Instant::now());

if !state.input_state.should_exit {
state.reconcile_live_source_interaction_if_idle(
Expand Down
32 changes: 31 additions & 1 deletion src/backend/wayland/backend/setup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,19 @@ use smithay_client_toolkit::{
shm::Shm,
};
use wayland_client::{Connection, EventQueue, globals::registry_queue_init};
use wayland_protocols::ext::{
image_capture_source::v1::client::ext_output_image_capture_source_manager_v1::ExtOutputImageCaptureSourceManagerV1,
image_copy_capture::v1::client::ext_image_copy_capture_manager_v1::ExtImageCopyCaptureManagerV1,
};
use wayland_protocols::wp::text_input::zv3::client::zwp_text_input_manager_v3::ZwpTextInputManagerV3;
use wayland_protocols_wlr::screencopy::v1::client::zwlr_screencopy_manager_v1::ZwlrScreencopyManagerV1;

use crate::env_vars::{XDG_CURRENT_DESKTOP_ENV, XDG_SESSION_DESKTOP_ENV};

use super::super::state::{WaylandGlobals, WaylandState};
use super::super::{
frozen::ExtImageCopyManagers,
state::{WaylandGlobals, WaylandState},
};

// Freeze/zoom capture currently consumes wl_shm buffer events and ignores linux-dmabuf.
// Version 3 can negotiate linux-dmabuf-only frames on newer wlroots/NVIDIA stacks, so
Expand All @@ -34,6 +41,7 @@ pub(super) struct WaylandSetup {
pub(super) qh: wayland_client::QueueHandle<WaylandState>,
pub(super) state_globals: WaylandGlobals,
pub(super) screencopy_manager: Option<ZwlrScreencopyManagerV1>,
pub(super) ext_image_copy_managers: Option<ExtImageCopyManagers>,
pub(super) text_input_manager: Option<ZwpTextInputManagerV3>,
pub(super) layer_shell_available: bool,
}
Expand Down Expand Up @@ -135,6 +143,27 @@ pub(super) fn setup_wayland() -> Result<WaylandSetup> {
}
};

let ext_image_copy_manager = globals
.bind::<ExtImageCopyCaptureManagerV1, _, _>(&qh, 1..=1, ())
.ok();
let ext_output_source_manager = globals
.bind::<ExtOutputImageCaptureSourceManagerV1, _, _>(&qh, 1..=1, ())
.ok();
let ext_image_copy_managers = match (ext_image_copy_manager, ext_output_source_manager) {
(Some(capture), Some(output_source)) => {
debug!("Bound ext-image-copy-capture output backend");
Some(ExtImageCopyManagers::new(capture, output_source))
}
(capture, output_source) => {
debug!(
"ext-image-copy-capture output backend unavailable: capture_manager={}, output_source_manager={}",
capture.is_some(),
output_source.is_some()
);
None
}
};

// IME / text-input-v3 for the text and sticky-note tools. Optional: when
// the compositor lacks it, editing falls back to the raw keysym path
// (single-key characters only).
Expand Down Expand Up @@ -175,6 +204,7 @@ pub(super) fn setup_wayland() -> Result<WaylandSetup> {
qh,
state_globals,
screencopy_manager,
ext_image_copy_managers,
text_input_manager,
layer_shell_available,
})
Expand Down
9 changes: 7 additions & 2 deletions src/backend/wayland/backend/state_init/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -104,14 +104,17 @@ pub(super) fn init_state(backend: &WaylandBackend, setup: WaylandSetup) -> Resul
};
input_state.set_session_preflight_options(session_options.clone());
let screencopy_supported = setup.screencopy_manager.is_some();
let image_copy_capture_supported = setup.ext_image_copy_managers.is_some();
let portal_freeze_supported = screenshot_portal_available(&backend.tokio_runtime);
let frozen_supported = screencopy_supported || portal_freeze_supported;
let direct_capture_supported = screencopy_supported || image_copy_capture_supported;
let frozen_supported = direct_capture_supported || portal_freeze_supported;
let tokio_handle = backend.tokio_runtime.handle().clone();

// Set compositor capabilities based on detected Wayland protocols
input_state.compositor_capabilities = CompositorCapabilities {
layer_shell: setup.layer_shell_available,
screencopy: screencopy_supported,
image_copy_capture: image_copy_capture_supported,
freeze_capture: frozen_supported,
pointer_constraints: setup
.state_globals
Expand Down Expand Up @@ -190,7 +193,7 @@ pub(super) fn init_state(backend: &WaylandBackend, setup: WaylandSetup) -> Resul

let freeze_on_start = if backend.freeze_on_start && !frozen_supported {
warn!(
"Frozen mode unavailable: no screencopy backend and no screenshot portal backend; ignoring --freeze"
"Frozen mode unavailable: no direct capture backend and no screenshot portal backend; ignoring --freeze"
);
false
} else {
Expand Down Expand Up @@ -218,6 +221,8 @@ pub(super) fn init_state(backend: &WaylandBackend, setup: WaylandSetup) -> Resul
main_surface_uses_overlay_layer: output_prefs.main_surface_uses_overlay_layer,
pending_freeze_on_start: freeze_on_start,
screencopy_manager: setup.screencopy_manager,
ext_image_copy_managers: setup.ext_image_copy_managers,
portal_freeze_supported,
text_input_manager: setup.text_input_manager,
#[cfg(feature = "tablet-input")]
tablet_manager,
Expand Down
Loading
Loading