Skip to content

Commit 96ccdbd

Browse files
authored
Merge pull request #250 from devmobasa/gnome-improvements
Add GNOME freeze fallback and improve fallback-mode messaging
2 parents 86bce2d + e33463a commit 96ccdbd

26 files changed

Lines changed: 536 additions & 123 deletions

File tree

README.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,7 @@ For distro-specific package details, see [Installation](#installation). For keyb
178178
| Platform | Status | Notes |
179179
|----------|--------|-------|
180180
| Wayland (layer-shell) | ✅ Supported | Hyprland, Sway, River, Wayfire, Niri/Cosmic, Plasma/KWin |
181-
| GNOME | ⚠️ Partial | Portal fallback; overlay windowed |
181+
| GNOME | ⚠️ Partial | Normal overlay and Freeze via portal when available; Light Mode passthrough unavailable |
182182
| X11 || Not supported |
183183

184184
<details>
@@ -256,8 +256,8 @@ For distro-specific package details, see [Installation](#installation). For keyb
256256
- Click highlights with configurable colors/radius/duration
257257
- Persistent ring while click highlight tool is active
258258
- Presenter mode (<kbd>Ctrl+Shift+M</kbd>): hides UI, forces click highlights
259-
- Light passthrough mode (layer-shell): <kbd>Ctrl+Shift+L</kbd> enters from the focused overlay; compositor/global shortcuts such as `wayscriber --light-toggle` keep control reliable while input is passed through
260-
- Screen freeze (<kbd>Ctrl+Shift+F</kbd>): pause display while apps run
259+
- Light passthrough mode (layer-shell): <kbd>Ctrl+Shift+L</kbd> enters from the focused overlay; compositor/global shortcuts such as `wayscriber --light-toggle` keep control reliable while input is passed through. Stock GNOME Wayland does not expose the overlay behavior this mode needs.
260+
- Screen freeze (<kbd>Ctrl+Shift+F</kbd>): pause display while apps run. On GNOME, this uses the screenshot portal when available.
261261

262262
### Callouts & Zoom
263263
- **Numbered callouts:** auto-numbered arrow labels, step markers
@@ -510,6 +510,7 @@ Light passthrough controls:
510510
- <kbd>Ctrl+Shift+L</kbd> is a Wayscriber in-overlay shortcut, not an OS/global shortcut. It works while the overlay is focused.
511511
- Once light passthrough is active, normal keyboard and pointer input goes to the app underneath. Bind compositor/global shortcuts to `wayscriber --light-toggle` and `wayscriber --light-draw-toggle` for reliable control.
512512
- Use `wayscriber --light-draw-on` on press and `wayscriber --light-draw-off` on release for draw-while-held shortcuts.
513+
- Stock GNOME Wayland does not support this regular-app passthrough mode. Freeze may still work for still-image capture, but it is not a live passthrough replacement. A GNOME Shell extension approach would be needed for true shell-level passthrough.
513514

514515
Use `--no-tray` or `WAYSCRIBER_NO_TRAY=1` if you don't have a system tray; otherwise right-click the tray icon for options:
515516
- Toggle overlay visibility
@@ -735,7 +736,7 @@ The polygon tools are available from the toolbar picker; their default keybindin
735736

736737
</details>
737738

738-
For light passthrough, <kbd>Ctrl+Shift+L</kbd> is the default Wayscriber-level binding only while the overlay has focus. Use compositor/global shortcuts that run `wayscriber --light-toggle` and related light-draw commands once passthrough is active.
739+
For light passthrough, <kbd>Ctrl+Shift+L</kbd> is the default Wayscriber-level binding only while the overlay has focus. Use compositor/global shortcuts that run `wayscriber --light-toggle` and related light-draw commands once passthrough is active. On stock GNOME Wayland, regular app windows cannot provide the required click-through shell overlay. Freeze may still work for still-image capture, but it is not a live passthrough replacement.
739740

740741
Arrow labels can auto-number when enabled in the arrow toolbar; reset with <kbd>Ctrl+Shift+R</kbd>. Step markers auto-increment and reset from the toolbar (or bind `reset_step_markers` in `config.toml`). Preset slots can be saved/cleared from the toolbar; edit names and advanced fields in `config.toml`. Blur has no default keyboard shortcut; bind `select_blur_tool` in `config.toml` if you want direct keyboard access.
741742

docs/CONFIG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -431,7 +431,7 @@ show_toast = true
431431

432432
Light mode hides UI chrome and sets the overlay to click-through passthrough until drawing is explicitly enabled. `toggle_light_mode` defaults to <kbd>Ctrl+Shift+L</kbd>, but that is a Wayscriber in-overlay shortcut: it works while the overlay still has focus. Once passthrough is active, normal keyboard and pointer input goes to the app underneath, so compositor/global shortcuts should call the daemon commands below for reliable control.
433433

434-
This mode requires layer-shell support; it is disabled on the xdg fallback because keyboard input cannot be passed through reliably there.
434+
This mode requires compositor overlay support through layer-shell. It is disabled on the xdg fallback because regular app windows cannot reliably stay visible as click-through shell overlays while keyboard and pointer input go to apps underneath. On stock GNOME Wayland, Freeze may still work for still-image capture when portal capture is available, but it is not a live passthrough replacement. True passthrough would require a GNOME Shell extension companion.
435435

436436
For compositor/global shortcuts while passthrough is active, run:
437437

docs/SETUP.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ wayscriber --light-draw-off
137137

138138
Use `--light-toggle` for passthrough on/off and `--light-draw-toggle` for sticky drawing. Draw-while-held needs a shortcut system that can run one command on press and another on release; if your KDE shortcut UI only supports activation commands, use the sticky draw toggle instead.
139139

140-
Light passthrough still requires layer-shell support. If Wayscriber reports that light mode requires layer-shell, your session is using a fallback path where passthrough is not available.
140+
Light passthrough requires compositor overlay support through layer-shell. If Wayscriber reports that passthrough is unavailable, your session is using a fallback path where regular app windows cannot provide a reliable click-through overlay.
141141

142142
### GNOME: Fedora Workstation and Ubuntu GNOME
143143

@@ -153,7 +153,9 @@ Then use the configurator's Daemon tab, or create a GNOME custom shortcut that r
153153
wayscriber --daemon-toggle
154154
```
155155

156-
Light passthrough mode is not currently available on GNOME sessions where Wayscriber uses the xdg-shell fallback. In that fallback, keyboard passthrough cannot be made reliable, so `--light-toggle` is intentionally disabled instead of pretending to pass input through.
156+
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.
157+
158+
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.
157159

158160
### Method 3: One-Shot Mode (Alternative)
159161

src/backend/wayland/backend/event_loop/capture.rs

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,40 @@ use crate::notification;
1212
pub(super) fn poll_portal_captures(state: &mut WaylandState) {
1313
// Apply any completed portal fallback captures without blocking.
1414
state.frozen.poll_portal_capture(&mut state.input_state);
15+
handle_pending_frozen_image(state);
1516
state.zoom.poll_portal_capture(&mut state.input_state);
1617
}
1718

19+
fn handle_pending_frozen_image(state: &mut WaylandState) {
20+
if !state.frozen.has_pending_image() {
21+
return;
22+
}
23+
if state.surface.is_xdg_window() {
24+
if state.xdg_fullscreen() {
25+
state.activate_pending_frozen_image_for_current_surface();
26+
return;
27+
}
28+
if !state.xdg_frozen_fullscreen_requested() && state.begin_xdg_frozen_fullscreen() {
29+
return;
30+
}
31+
if state.xdg_frozen_fullscreen_pending_configure() {
32+
if state.xdg_frozen_fullscreen_timed_out() {
33+
warn!("Frozen xdg fullscreen configure timed out; cancelling freeze");
34+
state.input_state.set_ui_toast(
35+
UiToastKind::Error,
36+
"Freeze failed because fullscreen was not confirmed",
37+
);
38+
state.restore_xdg_after_frozen();
39+
state.frozen.cancel(&mut state.input_state);
40+
}
41+
return;
42+
}
43+
state.activate_pending_frozen_image_for_current_surface();
44+
return;
45+
}
46+
state.activate_pending_frozen_image_for_current_surface();
47+
}
48+
1849
pub(super) fn flush_if_capture_active(conn: &Connection, capture_active: bool) {
1950
if capture_active {
2051
let _ = conn.flush();
@@ -61,10 +92,17 @@ fn handle_frozen_toggle(state: &mut WaylandState) {
6192
}
6293

6394
if !state.frozen_enabled() {
64-
warn!("Frozen mode disabled on this compositor (xdg fallback); ignoring toggle");
95+
warn!(
96+
"Frozen mode unavailable: no screencopy backend and no screenshot portal backend; ignoring toggle"
97+
);
98+
state.input_state.set_ui_toast(
99+
UiToastKind::Warning,
100+
"Freeze is unavailable because screen capture is not available.",
101+
);
65102
} else if state.frozen.is_in_progress() {
66103
warn!("Frozen capture already in progress; ignoring toggle");
67104
} else if state.input_state.frozen_active() {
105+
state.restore_xdg_after_frozen();
68106
state.frozen.unfreeze(&mut state.input_state);
69107
} else {
70108
let use_fallback = !state.frozen.manager_available();
@@ -73,7 +111,14 @@ fn handle_frozen_toggle(state: &mut WaylandState) {
73111
} else {
74112
info!("Frozen mode: using screencopy fast path");
75113
}
76-
state.enter_overlay_suppression(OverlaySuppression::Frozen);
114+
if !state.enter_overlay_suppression(OverlaySuppression::Frozen) {
115+
warn!("Frozen mode requested while overlay is suppressed; ignoring toggle");
116+
state.input_state.set_ui_toast(
117+
UiToastKind::Warning,
118+
"Freeze is already preparing another overlay operation.",
119+
);
120+
return;
121+
}
77122
if let Err(err) = state
78123
.frozen
79124
.start_capture(use_fallback, &state.tokio_handle)

src/backend/wayland/backend/event_loop/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@ pub(super) fn run_event_loop(
167167
notification::send_notification_async(
168168
&state.tokio_handle,
169169
"Wayscriber lost focus".to_string(),
170-
"GNOME could not keep the overlay focused; closing fallback window."
170+
"The desktop could not keep the overlay focused, so Wayscriber closed it."
171171
.to_string(),
172172
Some("dialog-warning".to_string()),
173173
);

src/backend/wayland/backend/helpers.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ pub(super) fn friendly_capture_error(error: &str) -> String {
2626
} else if lower.contains("permission") {
2727
"Permission denied. Enable screen sharing in system settings.".to_string()
2828
} else if lower.contains("portal returned error code") {
29-
"Portal screenshot failed. If you use wlroots/Hyprland/Niri, install grim + slurp. Otherwise check xdg-desktop-portal."
29+
"Screen capture failed. If you use Hyprland, Niri, or another wlroots desktop, install grim + slurp. Otherwise check the desktop screen capture service."
3030
.to_string()
3131
} else if lower.contains("busy") {
3232
"Screen capture in progress. Try again in a moment.".to_string()
@@ -207,7 +207,7 @@ mod tests {
207207
);
208208
assert_eq!(
209209
friendly_capture_error("portal returned error code 2"),
210-
"Portal screenshot failed. If you use wlroots/Hyprland/Niri, install grim + slurp. Otherwise check xdg-desktop-portal."
210+
"Screen capture failed. If you use Hyprland, Niri, or another wlroots desktop, install grim + slurp. Otherwise check the desktop screen capture service."
211211
);
212212
assert_eq!(
213213
friendly_capture_error("resource busy"),

src/backend/wayland/backend/setup.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ pub(super) fn setup_wayland() -> Result<WaylandSetup> {
124124
}
125125
Err(err) => {
126126
warn!(
127-
"zwlr_screencopy_manager_v1 not available (frozen mode disabled): {}",
127+
"zwlr_screencopy_manager_v1 not available; frozen mode may use portal fallback: {}",
128128
err
129129
);
130130
None

src/backend/wayland/backend/state_init/mod.rs

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,18 @@
11
use anyhow::Result;
22
use log::{info, warn};
33
use smithay_client_toolkit::globals::ProvidesBoundGlobal;
4+
use std::env;
45

56
use super::super::state::{WaylandState, WaylandStateInit};
67
use super::WaylandBackend;
78
use super::setup::WaylandSetup;
89
use super::tray::process_tray_action;
10+
use crate::backend::wayland::portal_capture::screenshot_portal_available;
911
use crate::{
1012
capture::CaptureManager,
1113
config::Config,
12-
input::{InputState, state::CompositorCapabilities},
14+
input::InputState,
15+
input::state::{CompositorCapabilities, DesktopEnvironment, ShellMode},
1316
onboarding::{DEFERRED_HINT_REPEAT_MAX, OnboardingStore},
1417
};
1518

@@ -43,16 +46,29 @@ pub(super) fn init_state(backend: &WaylandBackend, setup: WaylandSetup) -> Resul
4346

4447
let mut input_state = input_state::build_input_state(&config);
4548
input_state.set_session_preflight_options(session_options.clone());
49+
let screencopy_supported = setup.screencopy_manager.is_some();
50+
let portal_freeze_supported = screenshot_portal_available(&backend.tokio_runtime);
51+
let frozen_supported = screencopy_supported || portal_freeze_supported;
52+
let tokio_handle = backend.tokio_runtime.handle().clone();
4653

4754
// Set compositor capabilities based on detected Wayland protocols
4855
input_state.compositor_capabilities = CompositorCapabilities {
4956
layer_shell: setup.layer_shell_available,
50-
screencopy: setup.screencopy_manager.is_some(),
57+
screencopy: screencopy_supported,
58+
freeze_capture: frozen_supported,
5159
pointer_constraints: setup
5260
.state_globals
5361
.pointer_constraints_state
5462
.bound_global()
5563
.is_ok(),
64+
desktop_environment: desktop_environment_from_env(),
65+
shell_mode: if setup.layer_shell_available {
66+
ShellMode::LayerShell
67+
} else if setup.state_globals.xdg_shell.is_some() {
68+
ShellMode::XdgFallback
69+
} else {
70+
ShellMode::Unknown
71+
},
5672
};
5773

5874
let mut onboarding = OnboardingStore::load();
@@ -91,11 +107,10 @@ pub(super) fn init_state(backend: &WaylandBackend, setup: WaylandSetup) -> Resul
91107
let capture_manager = CaptureManager::new(backend.tokio_runtime.handle());
92108
info!("Capture manager initialized");
93109

94-
let tokio_handle = backend.tokio_runtime.handle().clone();
95-
96-
let frozen_supported = setup.layer_shell_available;
97110
let freeze_on_start = if backend.freeze_on_start && !frozen_supported {
98-
warn!("Frozen mode is not supported on GNOME xdg fallback; ignoring --freeze");
111+
warn!(
112+
"Frozen mode unavailable: no screencopy backend and no screenshot portal backend; ignoring --freeze"
113+
);
99114
false
100115
} else {
101116
backend.freeze_on_start
@@ -144,3 +159,19 @@ fn apply_initial_mode(backend: &WaylandBackend, _config: &Config, input_state: &
144159
}
145160
}
146161
}
162+
163+
fn desktop_environment_from_env() -> DesktopEnvironment {
164+
let values = [
165+
env::var("XDG_CURRENT_DESKTOP").unwrap_or_default(),
166+
env::var("XDG_SESSION_DESKTOP").unwrap_or_default(),
167+
env::var("DESKTOP_SESSION").unwrap_or_default(),
168+
];
169+
if values
170+
.iter()
171+
.any(|value| value.to_ascii_uppercase().contains("GNOME"))
172+
{
173+
DesktopEnvironment::Gnome
174+
} else {
175+
DesktopEnvironment::Unknown
176+
}
177+
}

src/backend/wayland/frozen/capture.rs

Lines changed: 26 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,7 @@ use smithay_client_toolkit::shm::{
44
Shm,
55
slot::{Buffer, SlotPool},
66
};
7-
use wayland_client::{
8-
Dispatch, QueueHandle, WEnum,
9-
protocol::{wl_output, wl_shm},
10-
};
7+
use wayland_client::{Dispatch, QueueHandle, WEnum, protocol::wl_shm};
118
use wayland_protocols_wlr::screencopy::v1::client::zwlr_screencopy_frame_v1::{
129
Event as FrameEvent, Flags, ZwlrScreencopyFrameV1,
1310
};
@@ -75,22 +72,36 @@ impl FrozenState {
7572
pub fn start_capture(
7673
&mut self,
7774
use_fallback: bool,
78-
tokio_handle: &tokio::runtime::Handle,
75+
_tokio_handle: &tokio::runtime::Handle,
7976
) -> Result<()> {
8077
if self.capture.is_some() || self.portal_in_progress || self.preflight_pending {
8178
warn!("Frozen-mode capture already in progress; ignoring toggle");
8279
return Ok(());
8380
}
8481

8582
self.capture_done = false;
83+
self.preflight_use_fallback = use_fallback || self.manager.is_none();
84+
self.preflight_pending = true;
85+
Ok(())
86+
}
8687

88+
pub fn begin_preflight_capture<State>(
89+
&mut self,
90+
use_fallback: bool,
91+
shm: &Shm,
92+
qh: &QueueHandle<State>,
93+
tokio_handle: &tokio::runtime::Handle,
94+
) -> Result<()>
95+
where
96+
State:
97+
Dispatch<ZwlrScreencopyFrameV1, ()> + Dispatch<ZwlrScreencopyManagerV1, ()> + 'static,
98+
{
8799
if use_fallback || self.manager.is_none() {
88-
info!("Screencopy unavailable; using fallback portal capture for frozen mode");
89-
return self.capture_via_portal(tokio_handle);
100+
info!("Suppression frame committed; using fallback portal capture for frozen mode");
101+
self.capture_via_portal(tokio_handle)
102+
} else {
103+
self.begin_screencopy(shm, qh)
90104
}
91-
92-
self.preflight_pending = true;
93-
Ok(())
94105
}
95106

96107
pub fn begin_screencopy<State>(&mut self, shm: &Shm, qh: &QueueHandle<State>) -> Result<()>
@@ -168,10 +179,7 @@ impl FrozenState {
168179
return;
169180
}
170181

171-
input_state.set_frozen_active(true);
172-
input_state.dirty_tracker.mark_full();
173182
input_state.needs_redraw = true;
174-
self.capture_done = true;
175183
}
176184
FrameEvent::Failed => {
177185
warn!("Frozen capture failed");
@@ -268,20 +276,16 @@ impl FrozenState {
268276

269277
capture.frame.destroy();
270278

271-
let output_transform = self
272-
.active_geometry
273-
.as_ref()
274-
.map(|geo| geo.transform)
275-
.unwrap_or(wl_output::Transform::Normal);
276-
277-
self.set_image(
279+
let source_geometry = self.active_geometry.clone();
280+
self.set_pending_image(
278281
FrozenImage {
279282
width: capture.width,
280283
height: capture.height,
281284
stride: (capture.width * 4) as i32,
282285
data,
283-
}
284-
.with_output_transform(output_transform),
286+
},
287+
source_geometry,
288+
true,
285289
);
286290

287291
Ok(())

src/backend/wayland/frozen/mod.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,12 @@ mod state;
66
pub use image::FrozenImage;
77
pub use state::FrozenState;
88

9-
type PortalCaptureResult = Result<(Option<u32>, self::image::FrozenImage), String>;
9+
type PortalCaptureResult = Result<
10+
(
11+
Option<u32>,
12+
Option<crate::backend::wayland::frozen_geometry::OutputGeometry>,
13+
self::image::FrozenImage,
14+
),
15+
String,
16+
>;
1017
type PortalCaptureRx = std::sync::mpsc::Receiver<PortalCaptureResult>;

0 commit comments

Comments
 (0)