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
36 changes: 36 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,42 @@ screenshots remain whole-desktop captures. Repeat while switching outputs and, w
changing scale or transform during capture: the result must either stay exact or fail visibly,
never shift, stretch, or reuse another output's image.

For changes to the native region picker, use the same compositor/scale matrix and record the
following permission-gated evidence (these checks open or foreground the overlay):

- `Ctrl+Shift+C` delivers the selected frozen pixels; the saved PNG dimensions match the picker
readout, including while zoomed or panned.
- With `[capture].include_drawings = true`, committed annotations are already visible over the
frozen desktop while selecting and match the delivered PNG; transient strokes, handles, and UI
stay hidden. In interactive Review, toggling drawings off/on updates that preview immediately.
- Cancelling while pending, armed, or selecting leaves no picker-owned freeze behind, and the next
capture action works.
- Output, scale, transform, zoom-level, and pan changes cancel with one visible message.
- `picker = "slurp"` restores the external selector. Native acquisition failure hands off to it,
while a dismissed portal request does not.
- Pointer, touch, and stylus drags all select when the hardware is available; losing the owning
device during a drag rearms the picker.
- Holding Shift before the first press produces a square crop, and the crosshair remains on the
physical pointer while the squared frame follows the constrained endpoint.
- `Ctrl+A` during a pointer, touch, or stylus drag captures the whole output and consumes the
outstanding release without starting a canvas gesture.
- Deactivating or aborting zoom while the picker waits cancels quietly; a reported zoom failure
produces exactly one message.
- With `picker = "slurp"` and `format = "jpg"`, the legacy path remains JPEG; native selection
remains PNG-only.
- On Hyprland and Sway, `Space` enters window mode only after the provider is ready. Verify pointer,
touch, and stylus selection, plus `Super+Arrow` navigation and `Enter`, on overlapping tiled and
floating windows. The highlighted readout and delivered PNG must have identical pixel bounds.
Also switch workspaces during picker startup: candidates intentionally follow the workspace
visible at provider-query time, while output/layout mismatches still disable window mode.
- On Plasma/KWin, Niri, and GNOME, the window-mode legend and `Space` control stay absent while area
selection continues to work. Provider failure or an empty workspace must also fall back quietly.

At 1080p@1, 4K@2, and a fractional-scale 4K setup, capture the debug-level `Region picker frame`
timings during pointer motion. Also record the process peak-RSS delta while encoding a full-output
`Ctrl+A` crop. Keep these measurements with the change's manual test evidence; they are intentionally
not collected by CI.

These captures now fail when the active output does not advertise a current `wl_output` mode.
Plugging or unplugging any monitor also cancels in-flight Freeze and Zoom captures on unrelated
outputs, because output count is part of the layout identity used to reject stale frames.
Expand Down
51 changes: 50 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -739,12 +739,61 @@ Light passthrough (layer-shell compositors only) lets normal keyboard and pointe
| <kbd>Ctrl+Shift+I</kbd> | Capture selection (respects `capture.copy_to_clipboard`) |
| <kbd>Ctrl+C</kbd> | Copy full screen to clipboard |
| <kbd>Ctrl+S</kbd> | Save full screen as PNG |
| <kbd>Ctrl+Shift+C</kbd> | Select region → clipboard |
| <kbd>Ctrl+Shift+C</kbd> | Select a region, then choose Copy, Save, Both, or Board (`capture_region_interactive`) |
| <kbd>Ctrl+Shift+S</kbd> | Select region → save PNG |
| <kbd>Ctrl+6</kbd> | Region → clipboard (explicit) |
| <kbd>Ctrl+Alt+6</kbd> | Region → save PNG (explicit) |
| Unbound | Select region → clipboard in one step (`capture_clipboard_selection`) |
| Unbound | Measure a logical screen region without capturing it (`measure_mode`) |
| <kbd>Ctrl+Alt+O</kbd> | Open last capture folder |

Region shortcuts use Wayscriber's native frozen-image picker by default. Set
`capture.region.picker = "slurp"` for the external selector; native selection
also falls back to `slurp` when no screen capture backend is available. The
readout, hotkey legend, and optional magnified pixel loupe are configurable
under `[capture.region]`. Region captures are encoded and
named as PNG even when the general screenshot format is set to JPEG. An
explicit `picker = "slurp"` keeps the configured legacy screenshot format.
On Hyprland and Sway, press <kbd>Space</kbd> in the native picker to switch
between free-area selection and current-workspace window selection. Point and
click a window, or use <kbd>Super</kbd>+Arrow and <kbd>Enter</kbd>. The window
control is omitted when the compositor cannot provide reliable geometry.
It is also omitted when the picker reuses an older user Freeze or Zoom image;
window bounds are offered only with the fresh auto-freeze created for that
picker. The provider result is checked against that source's output and layout
identity before candidates are shown; a mismatch simply leaves window mode
unavailable. Wayland has no portable workspace identity at the freeze boundary,
so candidates reflect the workspace visible when the compositor query runs.
`capture_region_interactive` owns <kbd>Ctrl+Shift+C</kbd> by default and is
also in the command palette. That chord previously ran the one-step
`capture_clipboard_selection`, which now ships unbound; Review's **Copy**
(<kbd>Ctrl+C</kbd>) reaches the same clipboard result, and binding
`capture_clipboard_selection` explicitly brings the one-step copy back. It
always uses the native picker so Review cannot be bypassed by an external
selector. In Review, **Both** (or <kbd>Enter</kbd>) always copies the PNG and
saves it to a file.

Full-screen and region captures include the active board's committed drawings
by default. Set `[capture].include_drawings = false` for raw desktop pixels.
Provisional strokes, selection handles, previews, toolbars, and other
Wayscriber UI are never included. In interactive Review, **Include drawings in
exports** (or <kbd>D</kbd>) starts from the configured default and can override
it for that one capture. Copy, Save, Both, and Board all honour it. Adding an
annotated crop to the board it came from bakes a second, flattened copy of
those annotations into the image, so turn the toggle off first when you want
the raw crop on the board.
The native region picker shows those committed drawings over its frozen desktop
while you select, so the visible preview matches the annotated export; toggling
drawings off in Review immediately returns the preview to raw desktop pixels.
On a transparent board, full-screen and legacy `slurp` captures retain the
desktop behind the annotations; a solid board retains its canvas background.
The native region picker always composites committed drawings over its frozen
desktop crop.

`measure_mode` is also command-palette-first and unbound by default. It shows a
crosshair and logical-pixel size readout without freezing or capturing the
screen; press <kbd>Esc</kbd> or run the action again to leave it.

Shortcuts marked "respects `capture.copy_to_clipboard`" send the capture to the clipboard or a file according to that `config.toml` setting; the other shortcuts always use the destination shown. Captures need the [screenshot tools](#screenshot-tools) and fall back to xdg-desktop-portal if they are missing.

Use `--exit-after-capture` / `--no-exit-after-capture` to override whether the overlay closes after a capture.
Expand Down
46 changes: 43 additions & 3 deletions config.example.toml
Original file line number Diff line number Diff line change
Expand Up @@ -271,10 +271,18 @@ capture_selection = ["Ctrl+Shift+I"]
# Clipboard/File capture variants
capture_clipboard_full = ["Ctrl+C"]
capture_file_full = ["Ctrl+S"]
capture_clipboard_selection = ["Ctrl+Shift+C"]
# Unbound by default: Ctrl+Shift+C now opens the interactive picker below,
# whose Copy action reaches the same clipboard result. Bind a chord here to get
# the one-step region copy back.
capture_clipboard_selection = []
capture_file_selection = ["Ctrl+Shift+S"]
capture_clipboard_region = ["Ctrl+6"]
capture_file_region = ["Ctrl+Alt+6"]
# Open the region review/action bar. Also available from the command palette.
capture_region_interactive = ["Ctrl+Shift+C"]
# Show a logical-pixel screen ruler without capturing anything. Available from
# the command palette and unbound by default.
measure_mode = []
export_canvas_file = []
export_canvas_clipboard = []
export_canvas_clipboard_and_file = []
Expand Down Expand Up @@ -457,6 +465,10 @@ show_capabilities_warning = true
# The guided tour remains available manually when this is false.
show_onboarding_hints = true

# Show rectangle and ellipse preview dimensions in logical board pixels.
# This is separate from capture.region.show_size_readout.
show_shape_size_readout = true

# Command palette action toast duration (ms)
command_palette_toast_duration_ms = 1500

Expand Down Expand Up @@ -1403,10 +1415,17 @@ filename_template = "screenshot_%Y-%m-%d_%H%M%S"
# Image format for saved screenshots: png, jpg, or jpeg
format = "png"

# Copy screenshots to clipboard by default
# Shortcut-specific actions may override this
# Copy screenshots to clipboard by default. Image writes smaller than 64 MiB
# are read back and retried once unless the PNG bytes match; larger writes
# trust wl-copy.
# Shortcut-specific actions may override this.
copy_to_clipboard = true

# Composite the active board's committed annotations into full-screen and
# region screenshots. Set to false for raw desktop pixels by default; the
# interactive region Review toggle can still override this per capture.
include_drawings = true

# Exit overlay after any capture completes (forces exit for all capture types)
# When false, clipboard-only captures still auto-exit by default.
# Use --no-exit-after-capture to keep the overlay open for a run.
Expand All @@ -1417,6 +1436,27 @@ exit_after_capture = false
# language packages must be installed. OCR also needs `enabled = true` above.
ocr_languages = "eng"

[capture.region]
# "native" draws Wayscriber's picker over a frozen desktop image.
# "slurp" keeps the external selector. Native falls back to slurp when no
# screen capture backend is available.
# Native region selections are encoded and named as PNG. The explicit "slurp"
# picker keeps [capture].format for compatibility with the legacy grim path.
# On Hyprland and Sway, native selection also offers Space-triggered window
# snapping for visible windows on the active output and workspace when the
# picker creates a fresh auto-freeze (not when reusing an older Freeze/Zoom).
# Candidates reflect the workspace visible when the compositor query runs.
picker = "native"

# Pointer-side readout: position while idle, W x H in export pixels while dragging.
show_size_readout = true

# Magnified pixel loupe next to the pointer while dragging.
show_loupe = false

# Short hotkey legend at the top centre until the first drag.
show_legend = true

# ═══════════════════════════════════════════════════════════════════════════════
# EXPORT SETTINGS
# ═══════════════════════════════════════════════════════════════════════════════
Expand Down
36 changes: 35 additions & 1 deletion configurator/src/app/pages/capture.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ use crate::models::util::format_float;
use crate::models::{
ColorPickerId, ColorQuadInput, PdfFitModeOption, PdfLabelContentModeOption,
PdfLabelPositionOption, PdfOrientationOption, PdfPageSizeOption,
PdfTransparentBackgroundOption, TabId, TextField, ToggleField,
PdfTransparentBackgroundOption, RegionPickerOption, TabId, TextField, ToggleField,
};

use wayscriber::config::validate_ocr_languages;
Expand Down Expand Up @@ -53,6 +53,12 @@ pub(super) fn build(sender: &ComponentSender<ConfiguratorApp>) -> BuiltPage {
|app| app.draft.capture_copy_to_clipboard,
|value| Message::ToggleChanged(ToggleField::CaptureCopyToClipboard, value),
)
.switch_row(
"Include drawings in screenshots",
"Composites the active board's committed annotations into full-screen and region captures by default. Interactive region capture can override this per screenshot.",
|app| app.draft.capture_include_drawings,
|value| Message::ToggleChanged(ToggleField::CaptureIncludeDrawings, value),
)
.switch_row(
"Always exit overlay after capture",
"",
Expand All @@ -72,6 +78,34 @@ pub(super) fn build(sender: &ComponentSender<ConfiguratorApp>) -> BuiltPage {
},
);

page.group_in_area("Region picker", SearchArea::CaptureRegion)
.combo_row(
"Selection frontend",
"Native draws the picker over a frozen desktop image. Slurp uses the external selector.",
RegionPickerOption::list(),
labels(RegionPickerOption::list(), RegionPickerOption::label),
|app| app.draft.capture_region_picker,
Message::CaptureRegionPickerChanged,
)
.switch_row(
"Show pointer position and selection size",
"Shows coordinates while idle and the selected export size while dragging.",
|app| app.draft.capture_region_show_size_readout,
|value| Message::ToggleChanged(ToggleField::CaptureRegionShowSizeReadout, value),
)
.switch_row(
"Show magnified pixel loupe",
"Shows a magnified pixel grid beside the pointer while selecting or reviewing.",
|app| app.draft.capture_region_show_loupe,
|value| Message::ToggleChanged(ToggleField::CaptureRegionShowLoupe, value),
)
.switch_row(
"Show hotkey legend",
"Shows a short picker hotkey guide until the first drag.",
|app| app.draft.capture_region_show_legend,
|value| Message::ToggleChanged(ToggleField::CaptureRegionShowLegend, value),
);

page.group_in_area("PDF export", SearchArea::CapturePdf)
.entry_row(
"PDF filename template (blank = capture template)",
Expand Down
6 changes: 6 additions & 0 deletions configurator/src/app/pages/ui/general.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,12 @@ pub(super) fn build(sender: &ComponentSender<ConfiguratorApp>) -> BuiltPage {
|app| app.draft.ui_show_onboarding_hints,
|value| Message::ToggleChanged(ToggleField::UiShowOnboardingHints, value),
)
.switch_row(
"Show shape size readout",
"Shows rectangle and ellipse preview dimensions in logical board pixels. Separate from the region-capture size readout.",
|app| app.draft.ui_show_shape_size_readout,
|value| Message::ToggleChanged(ToggleField::UiShowShapeSizeReadout, value),
)
.entry_row(
"Command palette toast (ms)",
|app| app.draft.ui_command_palette_toast_duration_ms.clone(),
Expand Down
7 changes: 7 additions & 0 deletions configurator/src/app/search/summary.rs
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,13 @@ fn capture_matches(query: &SearchQuery, summary: &mut TabSearchSummary) {
SearchArea::CaptureFiles,
CAPTURE_FILE_TERMS,
);
add_area_if(
query,
summary,
TabId::Capture,
SearchArea::CaptureRegion,
CAPTURE_REGION_TERMS,
);
if query.matches_parts(CAPTURE_PDF_TERMS.iter().copied())
|| (query.matches_any_raw_text(CAPTURE_PDF_IDENTITY_TERMS)
&& query.matches_parts_scoped_to_tab(TabId::Capture, CAPTURE_PDF_TERMS.iter().copied()))
Expand Down
23 changes: 23 additions & 0 deletions configurator/src/app/search/terms.rs
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,12 @@ pub(super) const UI_GENERAL_TERMS: &[&str] = &[
"tutorial",
"guidance",
"tips",
"show shape size readout",
"shape size readout",
"show_shape_size_readout",
"shape dimensions",
"rectangle ellipse preview",
"logical board pixels",
"command palette toast",
];
pub(super) const UI_TOOLBAR_TERMS: &[&str] = &[
Expand Down Expand Up @@ -373,9 +379,26 @@ pub(super) const CAPTURE_FILE_TERMS: &[&str] = &[
"capture filename template",
"clipboard",
"copy to clipboard",
"include drawings in screenshots",
"include drawings",
"capture annotations",
"screenshot annotations",
"include_drawings",
"always exit overlay after capture",
"format",
];
pub(super) const CAPTURE_REGION_TERMS: &[&str] = &[
"region picker",
"selection frontend",
"native",
"slurp",
"show pointer position and selection size",
"size readout",
"show magnified pixel loupe",
"pixel loupe",
"show hotkey legend",
"hotkey legend",
];
pub(super) const CAPTURE_PDF_TERMS: &[&str] = &[
"pdf",
"export",
Expand Down
40 changes: 40 additions & 0 deletions configurator/src/app/search/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,28 @@ fn exact_static_section_labels_match_their_sections() {
}
}

#[test]
fn region_picker_labels_match_the_region_section() {
for query in [
"selection frontend",
"show pointer position and selection size",
"show magnified pixel loupe",
"show hotkey legend",
"slurp",
] {
let (mut app, _effects) = ConfiguratorApp::new_app();
app.search_query = SearchQuery::new(query);

let summary = app.search_summary();
let capture = summary.tab(TabId::Capture).expect("capture match");

assert!(
capture.area_matches(SearchArea::CaptureRegion),
"query should show Region picker: {query}",
);
}
}

#[cfg(feature = "tablet-input")]
#[test]
fn exact_tablet_labels_match_tablet_section() {
Expand Down Expand Up @@ -592,6 +614,11 @@ fn exact_general_ui_field_labels_match_general_ui_section() {
"hints",
"onboarding hints",
"show_onboarding_hints",
"show shape size readout",
"shape size readout",
"show_shape_size_readout",
"rectangle ellipse preview",
"logical board pixels",
] {
let (mut app, _effects) = ConfiguratorApp::new_app();
app.search_query = SearchQuery::new(query);
Expand Down Expand Up @@ -619,6 +646,19 @@ fn exact_capture_file_labels_match_capture_file_section() {
assert!(capture.area_matches(SearchArea::CaptureFiles));
}

#[test]
fn capture_drawing_preference_matches_capture_file_section() {
for query in ["include drawings in screenshots", "capture annotations"] {
let (mut app, _effects) = ConfiguratorApp::new_app();
app.search_query = SearchQuery::new(query);

let summary = app.search_summary();
let capture = summary.tab(TabId::Capture).expect("capture match");

assert!(capture.area_matches(SearchArea::CaptureFiles));
}
}

#[test]
fn exact_capture_filename_labels_match_capture_sections() {
let cases = [
Expand Down
1 change: 1 addition & 0 deletions configurator/src/app/search/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ pub(crate) enum SearchArea {
BoardsGeneral,
RenderProfilesGeneral,
CaptureFiles,
CaptureRegion,
CapturePdf,
DaemonStatus,
DaemonService,
Expand Down
Loading
Loading