feat(regionSelector): freehand true-shape capture, R/Space keys, single-owner mode state

- fix: capture-mode state desync (6-press C cycle, inverted tab display).
  selectionMode/captureMode now have a single owner (RegionSelection);
  the toolbar receives them as read-only bindings and emits *Selected
  request signals on tab clicks - no more bidirectional Synchronizer +
  currentIndex write-back storms. captureMode is int (same-value writes
  no longer emit change signals).
- feat: freehand (ex-Circle) true-shape capture - a polygon mask makes
  pixels outside the drawn loop transparent (OCR flattens onto white;
  recording stays rectangular). Mask survives the crop editor: confirm
  captures loop intersected with the adjusted rect.
- feat: circle/freehand selection works in Crop mode (loop bounding box
  seeds the editor); Rect/Freehand tabs inert only while editing.
- feat: R toggles Rect/Freehand; Space confirms the crop editor;
  'Circle' tab relabeled Freehand (config keys keep circle names).
- docs: MANUAL, wiki, CONFIG_README, CHANGELOG updated.
This commit is contained in:
DJ committed 2026-07-04 23:47:21 -04:00
1 parent 285a7b4e5f
commit 510446e499
8 files changed
+168 -70

No files matched your search

+3 -1
View File
@@ -9,7 +9,9 @@ and this project adheres to [Conventional Commits](https://www.conventionalcommi
### Added
- **Crop Mode** - Press `C` (or the new Instant/Crop toolbar tabs) to switch the region selector into Crop mode: releasing a selection opens an adjustable editor (8 resize handles + interior move, min-size and screen clamping, live size label) instead of capturing immediately; **Enter** or the confirm button captures, **Escape** returns to drawing. Works for screenshots and recording alike; configurable via `regionSelector.crop.minSize`/`handleSize`
- **Crop Mode** - Press `C` (or the new Instant/Crop toolbar tabs) to switch the region selector into Crop mode: releasing a selection opens an adjustable editor (8 resize handles + interior move, min-size and screen clamping, live size label) instead of capturing immediately; **Enter**/**Space** or the confirm button captures, **Escape** returns to drawing. Works with both selection modes (a circle loop seeds the editor with its bounding box). Works for screenshots and recording alike; configurable via `regionSelector.crop.minSize`/`handleSize`
- **`R` Hotkey** - Toggles Rect/Freehand selection mode inside the overlay (the "Circle" tab is now labeled **Freehand**; config keys keep their `circle` names)
- **Freehand True-Shape Capture** - Freehand selections now capture only the pixels inside the drawn loop (transparent outside, via an ImageMagick polygon mask). OCR flattens onto white; recording remains rectangular. In Crop mode the mask survives the editor - confirm captures the loop intersected with the adjusted rect
- **`regionSelector.dragThreshold` Config Key** - Pixel dead-zone (default `6`) for the region selector's click/drag discriminator; `0` disables it, restoring the previous per-pixel sensitivity (circle mode always uses `0` — every circle gesture is the drawn shape)
### Changed
+6 -4
View File
@@ -25,12 +25,14 @@ Click and drag to select a rectangular region. Features:
- **Dimension display** - Shows width x height as you drag
- **Window snapping** - Detected windows highlight for quick selection
### Circle Selection
### Freehand Selection
Freehand captures the true drawn shape: the output is cropped to the loop's bounding box and everything outside the loop is transparent. OCR flattens the mask onto white (tesseract dislikes alpha); recording always captures the bounding rectangle (wf-recorder is rectangle-only). In Crop mode the mask survives the editor: confirm captures the drawn loop intersected with the adjusted rect.
Used for image search by default. Draw a freeform circle around content:
- Hold and drag to draw
- Release to capture the bounded area
- Release to capture the drawn shape (transparent outside the loop)
- Useful for irregular shapes or specific content
Toggle between modes in config:
@@ -50,10 +52,10 @@ By default captures happen instantly when you release the mouse. Press `C` (or u
- Releasing a selection (or clicking a detected window / monitor button) opens an adjustable editor instead of capturing
- Drag the 8 handles to resize, or drag inside the rect to move it
- `Enter` or the confirm button captures the adjusted region; `Escape` goes back to drawing
- `Enter` or `Space` (or the confirm button) captures the adjusted region; `Escape` goes back to drawing
- Works for all actions, including recording; right-click selections still open the editor and route to Edit on confirm
Crop mode is per monitor, always starts off (the overlay opens in Instant mode), and temporarily forces rectangle selection — your previous selection mode is restored when you leave it. Minimum size and handle size are configurable via `regionSelector.crop` in the config.
Crop mode is per monitor and always starts off (the overlay opens in Instant mode). Freehand selection works in Crop too — the drawn loop's bounding box seeds the editor. Toggle Rect/Freehand with `R`. Minimum size and handle size are configurable via `regionSelector.crop` in the config.
## Screenshot Actions
+2 -2
View File
@@ -145,7 +145,7 @@ Rectangle selection tool settings.
### regionSelector.circle
Circle selection tool settings.
Freehand ("circle") selection tool settings — the config key keeps its historical name.
| Setting | Default | Description |
|---------|---------|-------------|
@@ -164,7 +164,7 @@ With satty, **Enter** (or the copy/save toolbar buttons) keeps the edit — copy
### regionSelector.crop
Crop mode: an adjustable crop editor as an alternative to the default instant capture. Toggle it with the **C** key (hardcoded, per monitor) or the Instant/Crop toolbar tabs. In Crop mode, releasing a selection opens an editor with 8 resize handles and interior move-drag instead of capturing immediately; **Enter** (or the confirm button) captures the adjusted region, **Escape** goes back to drawing. The overlay always opens in Instant mode.
Crop mode: an adjustable crop editor as an alternative to the default instant capture. Toggle it with the **C** key (hardcoded, per monitor) or the Instant/Crop toolbar tabs. In Crop mode, releasing a selection opens an editor with 8 resize handles and interior move-drag instead of capturing immediately; **Enter** or **Space** (or the confirm button) captures the adjusted region, **Escape** goes back to drawing. The overlay always opens in Instant mode.
| Setting | Default | Description |
|---------|---------|-------------|
@@ -18,10 +18,18 @@ import Quickshell.Hyprland
Toolbar {
id: root
// Use a synchronizer on these
// action uses a Synchronizer (display-only here, never written back).
// selectionMode/captureMode are plain read-only bindings from RegionSelection — the
// single owner. Tab clicks *request* a change via the *Selected signals instead of
// writing state, and the tab indices are derived display. (The previous bidirectional
// Synchronizer + currentIndex write-back + the C-key external writer left three copies
// of the mode that desynced under re-entrant sync.)
property var action
property var selectionMode
property var captureMode
property int captureMode
signal selectionModeSelected(var mode)
signal captureModeSelected(int mode)
// True while the adjustable crop editor is open (plain binding from RegionSelection)
property bool cropEditing: false
// Monitor list for full-screen capture buttons
@@ -36,10 +44,12 @@ Toolbar {
readonly property bool showMonitorButtons: actionConfig.allowsMonitorButtons
readonly property bool cropActive: root.captureMode === RegionSelection.CaptureMode.Crop
// Keep tab indices in sync with externally driven state (C-key toggle swaps
// captureMode; entering Crop forces selectionMode back to RectCorners)
// Follow the owner: whenever the bound-in mode changes (C key, forced RectCorners,
// sessionReset, or an accepted tab request), reflect it in the tab index. Reads the
// just-changed property directly — never a derived property that may not have
// re-evaluated yet.
onCaptureModeChanged: {
const idx = root.cropActive ? 1 : 0;
const idx = root.captureMode === RegionSelection.CaptureMode.Crop ? 1 : 0;
if (captureModeTabBar.currentIndex !== idx) captureModeTabBar.setCurrentIndex(idx);
}
onSelectionModeChanged: {
@@ -65,20 +75,25 @@ Toolbar {
}
}
// Selection mode tabs (Rect/Circle) - inert while Crop is active (Crop implies rectangles)
// Selection mode tabs (Rect/Circle) - usable in Crop too (a circle loop seeds the
// editor via its bounding box); inert only while the crop editor is open, same as
// the capture tabs
ToolbarTabBar {
id: tabBar
enabled: !root.cropActive
enabled: !root.cropEditing
opacity: enabled ? 1 : 0.4
tabButtonList: [
{"icon": "activity_zone", "name": Translation.tr("Rect")},
{"icon": "gesture", "name": Translation.tr("Circle")}
{"icon": "gesture", "name": Translation.tr("Freehand")}
]
Component.onCompleted: {
currentIndex = root.selectionMode === RegionSelection.SelectionMode.RectCorners ? 0 : 1
}
onCurrentIndexChanged: {
root.selectionMode = currentIndex === 0 ? RegionSelection.SelectionMode.RectCorners : RegionSelection.SelectionMode.Circle;
// Emit only for genuine user input; index changes that merely follow the
// bound-in state compute the same mode and stay silent (no echo loop).
const mode = currentIndex === 0 ? RegionSelection.SelectionMode.RectCorners : RegionSelection.SelectionMode.Circle;
if (mode !== root.selectionMode) root.selectionModeSelected(mode);
}
}
@@ -97,7 +112,8 @@ Toolbar {
currentIndex = root.cropActive ? 1 : 0
}
onCurrentIndexChanged: {
root.captureMode = currentIndex === 0 ? RegionSelection.CaptureMode.Instant : RegionSelection.CaptureMode.Crop;
const mode = currentIndex === 0 ? RegionSelection.CaptureMode.Instant : RegionSelection.CaptureMode.Crop;
if (mode !== root.captureMode) root.captureModeSelected(mode);
}
}
@@ -103,18 +103,31 @@ QtObject {
clearTargetedRegion();
}
// Compute bounding box from circle selection points
// Compute the padded bounding box of the circle selection points without touching
// any state — safe for crop-mode seeding, where region* must stay binding-driven
// padding: extra space around the bounding box
// fallbackX, fallbackY: coordinates to use if no points recorded
function setRegionFromCirclePoints(padding: real, fallbackX: real, fallbackY: real) {
function circleBoundingBox(padding: real, fallbackX: real, fallbackY: real): var {
const dragPoints = (points.length > 0) ? points : [{ x: fallbackX, y: fallbackY }];
const maxX = Math.max(...dragPoints.map(p => p.x));
const minX = Math.min(...dragPoints.map(p => p.x));
const maxY = Math.max(...dragPoints.map(p => p.y));
const minY = Math.min(...dragPoints.map(p => p.y));
regionX = minX - padding;
regionY = minY - padding;
regionWidth = maxX - minX + padding * 2;
regionHeight = maxY - minY + padding * 2;
return {
x: minX - padding,
y: minY - padding,
width: maxX - minX + padding * 2,
height: maxY - minY + padding * 2
};
}
// Set the selection region to the circle points' bounding box (breaks the region*
// bindings — only safe on paths that end in dispatch/dismissal)
function setRegionFromCirclePoints(padding: real, fallbackX: real, fallbackY: real) {
const box = circleBoundingBox(padding, fallbackX, fallbackY);
regionX = box.x;
regionY = box.y;
regionWidth = box.width;
regionHeight = box.height;
}
}
@@ -32,35 +32,35 @@ PanelWindow {
enum CaptureMode { Instant, Crop }
property var action: RegionSelection.SnipAction.Copy
property var selectionMode: RegionSelection.SelectionMode.RectCorners
property var captureMode: RegionSelection.CaptureMode.Instant
// int, not var: int properties skip the changed signal on same-value writes, so
// redundant assignments (sessionReset, echo paths) can't retrigger onCaptureModeChanged
property int captureMode: RegionSelection.CaptureMode.Instant
// Crop mode: true while the adjustable crop editor is open (post-release, pre-confirm)
property bool cropEditing: false
// Selection mode in effect before Crop forced RectCorners; restored on exit
property var preCropSelectionMode: RegionSelection.SelectionMode.RectCorners
// Freehand true-shape capture: the drawn path, staged by the freehand release branch
// for the immediately following snip() (which masks pixels outside it). Empty for
// every other path — rect drags, target clicks, monitor buttons, crop confirms.
property var pendingSnipPoints: []
signal dismiss()
property bool isCropMode: (root.captureMode === RegionSelection.CaptureMode.Crop)
// Swapping capture modes resets the editor and any in-progress drag — never the frame.
// Crop implies rectangles: force RectCorners while active (toolbar tab bar is disabled
// too), remembering the previous mode so leaving Crop restores it (e.g. OCR's circle
// config). Lives here so both the C key and the toolbar tabs get the same behavior.
// Swapping capture modes resets the editor and any in-progress drag — never the
// frame or the selection mode. Circle works in Crop too: the drawn loop's bounding
// box seeds the editor. Lives here so both the C key and the toolbar tabs get the
// same behavior.
onCaptureModeChanged: {
root.cropEditing = false;
root.pendingSnipPoints = [];
dragState.reset();
if (root.isCropMode) {
root.preCropSelectionMode = root.selectionMode;
root.selectionMode = RegionSelection.SelectionMode.RectCorners;
} else {
root.selectionMode = root.preCropSelectionMode;
}
}
// Restore session defaults when a capture shortcut re-triggers while the overlay is
// already open. Local writes (C key, toolbar Synchronizer) break the captureMode
// binding from RegionSelector, so the reset must be pushed as a call, not a binding.
// already open. Local writes (the C key) break the captureMode binding from
// RegionSelector, so the reset must be pushed as a call, not a binding.
function sessionReset() {
root.cropEditing = false;
root.pendingSnipPoints = [];
dragState.reset();
root.captureMode = RegionSelection.CaptureMode.Instant;
}
@@ -107,6 +107,7 @@ PanelWindow {
// Capture the full region of this monitor
function captureFullMonitorLocal() {
root.pendingSnipPoints = []; // full-monitor capture is never freehand-masked
if (root.isCropMode) {
// Crop: seed the editor with the full-monitor rect instead of snipping
root.openCropEditor(0, 0, root.screen.width, root.screen.height);
@@ -255,17 +256,18 @@ PanelWindow {
// Table of command builders indexed by SnipAction enum value.
// Each builder takes (rx, ry, rw, rh, absX, absY) and returns a command array.
readonly property var commandBuilders: ({
[RegionSelection.SnipAction.Copy]: (rx, ry, rw, rh, absX, absY) =>
SnipCommands.buildCopyCommand(root.screenshotPath, rx, ry, rw, rh, root.saveScreenshotDir, Config.options.screenSnip.copyAlsoSaves),
[RegionSelection.SnipAction.Edit]: (rx, ry, rw, rh, absX, absY) =>
SnipCommands.buildEditCommand(root.screenshotPath, rx, ry, rw, rh, root.saveScreenshotDir, Config.options.screenSnip.copyAlsoSaves),
[RegionSelection.SnipAction.Search]: (rx, ry, rw, rh, absX, absY) =>
SnipCommands.buildSearchCommand(root.screenshotPath, rx, ry, rw, rh, "https://lens.google.com"),
[RegionSelection.SnipAction.CharRecognition]: (rx, ry, rw, rh, absX, absY) =>
SnipCommands.buildOcrCommand(root.screenshotPath, rx, ry, rw, rh),
[RegionSelection.SnipAction.Record]: (rx, ry, rw, rh, absX, absY) =>
[RegionSelection.SnipAction.Copy]: (rx, ry, rw, rh, absX, absY, polygon) =>
SnipCommands.buildCopyCommand(root.screenshotPath, rx, ry, rw, rh, root.saveScreenshotDir, Config.options.screenSnip.copyAlsoSaves, polygon),
[RegionSelection.SnipAction.Edit]: (rx, ry, rw, rh, absX, absY, polygon) =>
SnipCommands.buildEditCommand(root.screenshotPath, rx, ry, rw, rh, root.saveScreenshotDir, Config.options.screenSnip.copyAlsoSaves, polygon),
[RegionSelection.SnipAction.Search]: (rx, ry, rw, rh, absX, absY, polygon) =>
SnipCommands.buildSearchCommand(root.screenshotPath, rx, ry, rw, rh, "https://lens.google.com", polygon),
[RegionSelection.SnipAction.CharRecognition]: (rx, ry, rw, rh, absX, absY, polygon) =>
SnipCommands.buildOcrCommand(root.screenshotPath, rx, ry, rw, rh, polygon),
// Recording is rectangle-only (wf-recorder captures rects); polygon unused
[RegionSelection.SnipAction.Record]: (rx, ry, rw, rh, absX, absY, polygon) =>
SnipCommands.buildRecordCommand(Directories.recordScriptPath, absX, absY, rw, rh, false),
[RegionSelection.SnipAction.RecordWithSound]: (rx, ry, rw, rh, absX, absY) =>
[RegionSelection.SnipAction.RecordWithSound]: (rx, ry, rw, rh, absX, absY, polygon) =>
SnipCommands.buildRecordCommand(Directories.recordScriptPath, absX, absY, rw, rh, true)
})
@@ -304,6 +306,23 @@ PanelWindow {
const absX = rx + Math.round(root.monitorOffsetX * root.monitorScale);
const absY = ry + Math.round(root.monitorOffsetY * root.monitorScale);
// Freehand true-shape mask: transform the staged path to crop-local physical
// pixels, downsampled to <= 150 vertices (a slow scribble records hundreds of
// samples; ImageMagick doesn't need them all). Empty string = plain bbox crop.
let polygon = "";
if (root.pendingSnipPoints.length >= 3) {
const pts = root.pendingSnipPoints;
const stride = Math.max(1, Math.ceil(pts.length / 150));
const parts = [];
for (let i = 0; i < pts.length; i += stride) {
const px = Math.round((pts[i].x - clamped.x) * root.monitorScale);
const py = Math.round((pts[i].y - clamped.y) * root.monitorScale);
parts.push(`${px},${py}`);
}
polygon = parts.join(" ");
}
root.pendingSnipPoints = [];
// Build command using table-driven approach
const builder = commandBuilders[root.action];
if (!builder) {
@@ -311,7 +330,7 @@ PanelWindow {
root.dismiss();
return;
}
snipProc.command = builder(rx, ry, rw, rh, absX, absY);
snipProc.command = builder(rx, ry, rw, rh, absX, absY, polygon);
snipProc.startDetached();
root.dismiss();
@@ -355,7 +374,13 @@ PanelWindow {
root.captureMode = root.isCropMode
? RegionSelection.CaptureMode.Instant
: RegionSelection.CaptureMode.Crop;
} else if ((event.key === Qt.Key_Return || event.key === Qt.Key_Enter) && root.cropEditing && !event.isAutoRepeat) {
} else if (event.key === Qt.Key_R && event.modifiers === Qt.NoModifier && !dragState.dragging && !root.cropEditing) {
// Rect/Freehand toggle — same guards as C (no mid-drag swaps), plus
// inert while the editor is open, matching the toolbar tab gating
root.selectionMode = root.selectionMode === RegionSelection.SelectionMode.RectCorners
? RegionSelection.SelectionMode.Circle
: RegionSelection.SelectionMode.RectCorners;
} else if ((event.key === Qt.Key_Return || event.key === Qt.Key_Enter || event.key === Qt.Key_Space) && root.cropEditing && !event.isAutoRepeat) {
root.confirmCrop();
}
}
@@ -372,6 +397,9 @@ PanelWindow {
// Controls
onPressed: (mouse) => {
// Every new gesture invalidates any freehand mask staged by a previous
// (e.g. Esc-cancelled) selection
root.pendingSnipPoints = [];
dragState.startDrag(mouse.x, mouse.y, mouse.button);
root.updateTargetedRegion(mouse.x, mouse.y);
}
@@ -402,6 +430,22 @@ PanelWindow {
// Circle dragging?
else if (root.selectionMode === RegionSelection.SelectionMode.Circle) {
const padding = Config.options.regionSelector.circle.padding + Config.options.regionSelector.circle.strokeWidth / 2;
// True-shape capture: stage the path for snip()'s polygon mask.
// Degenerate paths (< 3 vertices) fall back to the bounding box.
// Staged in Crop too — the mask is screen-anchored (snip() transforms
// it against the final rect), so confirm captures loop ∩ adjusted rect.
if (dragState.points.length >= 3) {
root.pendingSnipPoints = dragState.points.slice();
}
if (root.isCropMode) {
// Crop: seed the editor from the loop's bounding box WITHOUT
// setRegionFromCirclePoints — that writes region* directly and
// would break the bindings Esc-back-to-draw depends on
const box = dragState.circleBoundingBox(padding, mouseArea.mouseX, mouseArea.mouseY);
dragState.endDrag();
root.openCropEditor(box.x, box.y, box.width, box.height);
return;
}
dragState.setRegionFromCirclePoints(padding, mouseArea.mouseX, mouseArea.mouseY);
}
dragState.endDrag();
@@ -422,7 +466,9 @@ PanelWindow {
Loader {
z: 2
anchors.fill: parent
active: root.selectionMode === RegionSelection.SelectionMode.RectCorners
// Also active while crop-editing regardless of selection mode: the editor
// is always rectangular (a circle seed becomes its bounding box)
active: root.selectionMode === RegionSelection.SelectionMode.RectCorners || root.cropEditing
sourceComponent: RectCornersSelectionDetails {
regionX: dragState.regionX
regionY: dragState.regionY
@@ -455,7 +501,9 @@ PanelWindow {
Loader {
z: 2
anchors.fill: parent
active: root.selectionMode === RegionSelection.SelectionMode.Circle
// Hidden while crop-editing: the drawn loop has been consumed into the
// editor's rect (and dragState.points survive until the next reset)
active: root.selectionMode === RegionSelection.SelectionMode.Circle && !root.cropEditing
sourceComponent: CircleSelectionDetails {
color: root.selectionBorderColor
overlayColor: root.overlayColor
@@ -544,15 +592,21 @@ PanelWindow {
OptionsToolbar {
monitors: root.allMonitors
cropEditing: root.cropEditing
// Single source of truth is root.captureMode/selectionMode: state flows
// down through these bindings, tab clicks flow up as *Selected requests.
// (The old bidirectional Synchronizers + index write-backs desynced once
// the C key became an external writer.)
selectionMode: root.selectionMode
captureMode: root.captureMode
onSelectionModeSelected: (mode) => {
if (root.selectionMode !== mode) root.selectionMode = mode;
}
onCaptureModeSelected: (mode) => {
if (root.captureMode !== mode) root.captureMode = mode;
}
Synchronizer on action {
property alias source: root.action
}
Synchronizer on selectionMode {
property alias source: root.selectionMode
}
Synchronizer on captureMode {
property alias source: root.captureMode
}
onDismiss: root.dismiss();
onCaptureFullMonitor: (monitorName) => root.captureFullMonitor(monitorName)
onEditFullMonitor: (monitorName) => root.editFullMonitor(monitorName)
@@ -27,7 +27,7 @@ Scope {
property var action: RegionSelection.SnipAction.Copy
property var selectionMode: RegionSelection.SelectionMode.RectCorners
property var captureMode: RegionSelection.CaptureMode.Instant
property int captureMode: RegionSelection.CaptureMode.Instant
// Emitted by openWithAction so live RegionSelection instances (whose property
// bindings may have been broken by local writes) reset to session defaults
@@ -20,9 +20,20 @@ Singleton {
return path;
}
// Build ImageMagick crop command base
function buildCropBase(screenshotPath: string, rx: int, ry: int, rw: int, rh: int): string {
return `magick ${StringUtils.shellSingleQuoteEscape(screenshotPath)} -crop ${rw}x${rh}+${rx}+${ry}`;
// Build ImageMagick crop command base.
// polygon: optional "x,y x,y ..." vertex list in crop-local physical pixels (freehand
// true-shape capture) — pixels outside it become transparent via a CopyOpacity mask.
// flattenWhite: replace that transparency with white (tesseract dislikes alpha).
function buildCropBase(screenshotPath: string, rx: int, ry: int, rw: int, rh: int, polygon: string, flattenWhite: bool): string {
let cmd = `magick ${StringUtils.shellSingleQuoteEscape(screenshotPath)} -crop ${rw}x${rh}+${rx}+${ry}`;
if (polygon !== "") {
// +repage zeroes the virtual canvas offset so the mask composites aligned
cmd += ` +repage \\( -size ${rw}x${rh} xc:black -fill white -draw 'polygon ${polygon}' \\) -alpha off -compose CopyOpacity -composite`;
if (flattenWhite) {
cmd += ` -background white -alpha remove -alpha off`;
}
}
return cmd;
}
// Build cleanup command
@@ -53,8 +64,8 @@ Singleton {
}
// Copy to clipboard (optionally also saves to disk if copyAlsoSaves is true)
function buildCopyCommand(screenshotPath: string, rx: int, ry: int, rw: int, rh: int, saveDir: string, alsoSave: bool): list<string> {
const cropBase = buildCropBase(screenshotPath, rx, ry, rw, rh);
function buildCopyCommand(screenshotPath: string, rx: int, ry: int, rw: int, rh: int, saveDir: string, alsoSave: bool, polygon: string): list<string> {
const cropBase = buildCropBase(screenshotPath, rx, ry, rw, rh, polygon, false);
const cropToStdout = `${cropBase} -`;
const cleanup = buildCleanup(screenshotPath);
@@ -84,8 +95,8 @@ Singleton {
// with NO XDG_CONFIG_HOME override, so GTK theming stays native. satty shows its own
// notifications. Falls back to a bare swappy invocation (end-4 style) when satty is disabled
// or not installed.
function buildEditCommand(screenshotPath: string, rx: int, ry: int, rw: int, rh: int, saveDir: string, alsoSave: bool): list<string> {
const cropBase = buildCropBase(screenshotPath, rx, ry, rw, rh);
function buildEditCommand(screenshotPath: string, rx: int, ry: int, rw: int, rh: int, saveDir: string, alsoSave: bool, polygon: string): list<string> {
const cropBase = buildCropBase(screenshotPath, rx, ry, rw, rh, polygon, false);
const cropToStdout = `${cropBase} -`;
const cleanup = buildCleanup(screenshotPath);
@@ -118,8 +129,8 @@ Singleton {
// Image search (clipboard-based: copy to clipboard + open search page)
function buildSearchCommand(screenshotPath: string, rx: int, ry: int, rw: int, rh: int,
searchPageUrl: string): list<string> {
const cropBase = buildCropBase(screenshotPath, rx, ry, rw, rh);
searchPageUrl: string, polygon: string): list<string> {
const cropBase = buildCropBase(screenshotPath, rx, ry, rw, rh, polygon, false);
const cropToStdout = `${cropBase} -`;
const cleanup = buildCleanup(screenshotPath);
// Copy to clipboard AND open search page - user pastes with Ctrl+V
@@ -127,8 +138,8 @@ Singleton {
}
// OCR with tesseract
function buildOcrCommand(screenshotPath: string, rx: int, ry: int, rw: int, rh: int): list<string> {
const cropBase = buildCropBase(screenshotPath, rx, ry, rw, rh);
function buildOcrCommand(screenshotPath: string, rx: int, ry: int, rw: int, rh: int, polygon: string): list<string> {
const cropBase = buildCropBase(screenshotPath, rx, ry, rw, rh, polygon, true);
const cropInPlace = `${cropBase} '${StringUtils.shellSingleQuoteEscape(screenshotPath)}'`;
const cleanup = buildCleanup(screenshotPath);
const tesseractLangs = `$(tesseract --list-langs | awk 'NR>1{print $1}' | tr '\\n' '+' | sed 's/\\+$/\\n/')`;