@specfork/screenshot
Local desktop screenshots of a region, display, window or all displays, with delay, HiDPI output, annotation, undo, copy, save, pin, hotkeys, tray and CLI; no network by default.
Screenshot
One person takes screenshots on their own computer. A hotkey, the tray icon or a command starts a capture of a region, a whole display, all displays or one window, optionally after a delay. In the interactive mode the frozen screen is shown, the person selects an area, marks it up with arrows, shapes, text, highlights, counters and redactions, then copies, saves or pins the result. Everything happens on the device; nothing is uploaded unless the person sets up an upload target themselves. Behaviour derived from Flameshot's open-source code (GPL-3.0-or-later), written fresh and stack-free. Not affiliated with or endorsed by the Flameshot project (flameshot-org). The name is used only to credit the source.
| Area | Prefix | Covers |
|---|---|---|
| capture.md | CAP | Capture modes, request fields, delay, freeze, last region, cursor, failures |
| displays.md | DSP | Multi-monitor layout, HiDPI and mixed scaling, X11, Wayland, Windows, macOS |
| selection.md | SEL | Overlay, selecting and adjusting the area, toolbar, accept, cancel |
| annotate.md | ANN | Arrow, line, rectangle, ellipse, pencil, marker, text, redact, counter, colour, size |
| editing.md | EDT | Undo/redo, selecting, moving, restyling and ordering annotations |
| output.md | OUT | Clipboard, save, filename pattern, formats, notifications, open with, recent captures |
| pin.md | PIN | Images pinned on top of the desktop |
| app.md | APP | Background instance, tray, global hotkeys, autostart, settings |
| cli.md | CLI | Commands, options, exit codes, local command interface |
| privacy.md | PRIV | No network by default, opt-in upload, redaction safety, local caches, metadata |
Single user, no account, no server. Runs on Windows, macOS and Linux (X11 and Wayland); platform
differences are stated as behaviour. Shortcuts are defaults and configurable; Ctrl means Cmd on
macOS. Coordinates are logical pixels in the global desktop space unless a rule says physical pixels;
setting and field names are snake_case.
This spec is licensed under Creative Commons Attribution 4.0 International (CC BY 4.0): https://creativecommons.org/licenses/by/4.0/legalcode
It describes behaviour of Flameshot (https://github.com/flameshot-org/flameshot, GPL-3.0-or-later)
in new words; no code or text was copied.
See README.md front matter derived_from.
Annotate
Status: Draft · Prefix: ANN
Purpose
Drawing tools used on the selection before it is exported, and the colour and size every tool shares.
Requirements
| ID | Requirement | Verified by |
|---|---|---|
| ANN-1 | Annotation: {id, tool, color (#RRGGBB), opacity 0..255, size 1..100, points[], fill: none|solid, text, font, z}. Annotations stay editable objects until export; the exported image is the frozen image plus all annotations drawn in z order, clipped to the selection. |
draw arrow, move it, export → arrow at its new place |
| ANN-2 | Tools and default keys: arrow A, line L, rectangle R, ellipse E, pencil P, marker M, text T, redact B, counter N, pointer V (EDT-5). Tools are usable only while a selection exists; a key with no selection does nothing. |
no selection, press R → no tool change |
| ANN-3 | Line: a straight segment from press to release, size = stroke width in logical px; with Shift the angle snaps to multiples of 45°. |
Shift-drag at 40° → drawn at 45° |
| ANN-4 | Arrow: a line with a filled head at the release point; head length = max(10, 3 × size) logical px; arrow_heads: end|both (end). Shift snaps as ANN-3. |
size 5 → head length 15 px |
| ANN-5 | Rectangle: from press to release corner, corner radius 0..50 px (0). Ellipse: inscribed in the dragged box. Both are an outline of width size, or filled when fill is solid; Shift keeps a square or circle. |
Shift-drag 100×60 → 100×100 square |
| ANN-6 | Pencil: a smoothed freehand stroke following the pointer, width size. |
drag a loop → loop drawn |
| ANN-7 | Marker: a highlighter stroke at 40% opacity whose overlapping parts within one stroke do not darken; freehand, or straight with Shift; default size 14. |
scribble over the same spot twice in one stroke → uniform tint |
| ANN-8 | Text: a click opens a text box at that point; typing inserts text, Enter adds a line, Ctrl+Enter or a click outside commits. Fields: font family (system UI font), size 6..200 pt (16), bold, italic, underline, strike-through, align left|center|right. A box committed empty is discarded. |
type nothing, click outside → no object |
| ANN-9 | Redact: a dragged rectangle obscured by redact_mode: pixelate|blur|solid (pixelate) at strength 1..10 (5). Output follows PRIV-8: by default nothing inside the rectangle influences the result. |
redact a word, export twice with different words underneath → identical pixels |
| ANN-10 | Counter: a click places a round badge with the next number in this capture, starting at 1; diameter = 16 + 4 × size px; digits black or white, whichever contrasts more with the badge colour; outline ring when counter_outline is true (default true). Deleting a counter renumbers all higher ones down by 1; undoing the delete restores every number; a new badge takes the highest number + 1. |
3 clicks → 1, 2, 3; delete 2 → 1, 2; undo → 1, 2, 3 |
| ANN-11 | Colour: default #FF0000; a palette of palette_colors (default 10 presets) plus a custom picker; a secondary click opens the palette at the pointer; G then a click sets it to the pixel under the pointer. The last colour is kept across captures. |
pick blue, new capture → blue selected; G, click a green pixel → green |
| ANN-12 | Size: mouse wheel or +/- changes the current tool's size by 1 within 1..100 and shows the value briefly. Each tool keeps its own last size across captures; defaults: line, arrow, rectangle, ellipse, pencil 3; marker 14; text 16 pt; redact 5; counter 2. |
pencil 3, wheel up twice → 5; marker still 14 |
Out of scope
- Image stickers, emoji, shapes beyond those listed, colour inversion, crop or rotate inside the overlay.
- Per-point pressure from pen tablets.
Decisions
- Default keys are our own (line
L, ellipseE, counterN, pointerV) to read as the tool's initial where possible. - Constraining uses
Shifteverywhere (ANN-3..7), the common convention in drawing apps.
App
Status: Draft · Prefix: APP
Purpose
The background instance, tray icon, global hotkeys, start at login and the settings that persist between captures.
Requirements
| ID | Requirement | Verified by |
|---|---|---|
| APP-1 | Starting the app with no arguments runs one background instance per user session; starting it again with no arguments exits at once with code 0 and leaves the first running. | start twice → one process, second exits 0 |
| APP-2 | Tray menu: capture region, capture full, capture display ▸ (one row per display: index, name, physical size), launcher, recent captures, open save folder, close all pins, settings, about, quit. tray_click: capture|menu (capture) sets what a primary click on the icon does; a secondary click always opens the menu. |
2 displays → submenu with 2 rows; click icon → region overlay |
| APP-3 | tray_icon (default true): when false there is no tray icon and the instance keeps running, reachable by hotkeys and CLI. When the desktop has no tray area, the app behaves as if false and says so once. |
tray off → no icon, hotkey still captures |
| APP-4 | Global hotkeys, configurable, defaults: region capture PrintScreen (Windows, Linux) or Cmd+Shift+2 (macOS); full capture Ctrl+PrintScreen (macOS Cmd+Shift+1); recent captures Shift+PrintScreen (macOS Cmd+Shift+0). A hotkey another app or the OS holds (e.g. an OS screenshot tool on PrintScreen) is not registered: settings mark it not registered, explain how the user can free it, and the others still work; the app never changes OS settings. |
another app holds PrintScreen → marked, full hotkey works |
| APP-5 | Wayland: the app cannot grab keys itself; it registers hotkeys through the desktop's global-shortcut service when present (the desktop may ask the user to confirm), otherwise settings show the CLI command to bind in the desktop's own shortcut settings. | Wayland without service → settings show gui command |
| APP-6 | start_at_login (default false) adds or removes a per-user login item; no administrator rights are needed. |
turn on, log in again → instance running |
| APP-7 | Settings live in one per-user text file in the OS configuration folder. Unknown keys are ignored with a warning; an invalid value falls back to that key's default with a warning; the rest still load. | dim_opacity=999 → default used, warning logged |
| APP-8 | Settings window sections: general, capture, tools, shortcuts, output, integrations (PRIV-3). Changes apply from the next capture with no restart. | change save_format to jpg → next save is jpg |
| APP-9 | Overlay shortcuts (SEL, ANN-2, EDT) are configurable; assigning a key already used by another overlay action is refused with a message naming that action; an empty key leaves the action unbound. | assign R to arrow → refused, names rectangle |
| APP-10 | ui_language (auto = OS language, falls back to English); accent_color (#RRGGBB) colours selection border, handles and toolbar. |
accent #00AA00 → green handles |
Out of scope
- Themes beyond the accent colour; per-display settings.
- Self-update or installing packages.
Decisions
- macOS defaults use
Cmd+Shift+0/1/2, which the OS does not reserve (it uses 3, 4, 5 and 6). - No idle auto-exit: the background instance stays until quit, so hotkeys always work.
Capture
Status: Draft · Prefix: CAP
Purpose
What can be captured, how a capture request is described, and what happens between the request and the image.
Requirements
| ID | Requirement | Verified by |
|---|---|---|
| CAP-1 | Modes: region (interactive overlay, SEL), full (all displays in one image), screen (one display), window (one top-level window). Every mode can be started from a global hotkey, the tray menu, the CLI or the launcher, a small window to choose mode, delay (0..600 s), display and actions that hides itself before the delay starts. |
start each mode from the CLI → image bounds equal region, desktop, display, window |
| CAP-2 | Capture request: {mode, delay_ms 0..600000 (0), display (index or current), region (optional rectangle), use_last_region (false), include_cursor (false), actions: set of copy|save|pin|print_raw|print_geometry, save_path (optional), accept_on_select (false)}. include_cursor composites the pointer image where the platform provides it, otherwise is ignored. A request with an invalid field is refused before anything is grabbed and the message names the field. |
delay_ms -5 → refused, message names delay_ms, no image |
| CAP-3 | Delay: the screen is grabbed delay_ms after the request. Nothing from the app is visible on screen during the delay except a countdown in the tray icon tooltip; the request can be cancelled from the tray menu. |
delay 3000, open a menu in another app → the menu is in the image |
| CAP-4 | Freeze: region and interactive window grab every display once when the overlay opens; the overlay and the result show that frozen image, not later screen changes. |
video playing, start region → result shows the frame from the start |
| CAP-5 | full returns one image covering the bounding box of all displays (DSP-1); parts of the box no display covers are transparent, or black in formats without alpha. |
two displays offset vertically → empty corner is transparent in PNG |
| CAP-6 | screen captures the display given by display (index per DSP-1) or, with current (default), the display containing the pointer. An index with no display → no_such_display, nothing captured. |
2 displays, display 5 → no_such_display |
| CAP-7 | Window selection: in the overlay, hovering highlights the window under the pointer and a click without dragging selects its bounds, including title bar and frame when window_frame is true (default true). Non-interactive window captures the active window. Where the platform does not expose windows (DSP-8), window opens the region overlay with a notice instead. |
X11, click a window → selection equals its outer bounds; Wayland → region overlay + notice |
| CAP-8 | Last region: every accepted region capture remembers its rectangle in global coordinates, in memory only unless remember_last_region is true (default false), then also across restarts. use_last_region preselects it; with none, or one that no longer intersects any display, the overlay opens with no selection. |
capture 200×100 at 50,50, request again with use_last_region → same rectangle preselected |
| CAP-9 | accept_on_select: the capture completes when the first selection drag is released, with no annotation step; with no actions it saves (OUT-2). |
drag 100×100 with the flag → file saved, overlay closed |
| CAP-10 | The app's own transient UI (tray menu, launcher, notifications, countdown) is hidden before the grab and never appears in the image. Pinned images (PIN) stay visible unless hide_pins_on_capture is true (default false). |
start capture from the tray menu → menu absent from the image |
| CAP-11 | At most one interactive overlay at a time unless allow_multiple_captures is true (default false); another interactive request while one is open focuses the open one and the new request ends as cancelled. Non-interactive requests are never blocked. |
overlay open, press the hotkey again → still one overlay |
| CAP-12 | If the platform denies or cannot provide the screen image, no overlay opens, a notification gives the reason, and the request ends as capture_denied (permission refused) or capture_unavailable (no capture service). |
permission refused → notification, CLI exit code 3 |
Out of scope
- Scrolling or long-page capture, video or GIF recording, OCR.
- Capturing windows hidden behind others or minimised.
Decisions
- Window capture (CAP-7) is new: the source has region, display and full capture only; hover-to-pick and active-window capture were designed here.
- The pointer can be included (
include_cursor, CAP-2); the source never includes it.
CLI
Status: Draft · Prefix: CLI
Purpose
Starting captures and changing settings from a terminal or script, and the minimal local command interface behind it.
Requirements
| ID | Requirement | Verified by |
|---|---|---|
| CLI-1 | Commands: gui (region overlay), full, screen, window, launcher, config, plus --help and --version. An unknown command or option prints usage to stderr and exits 2. |
screenshot frobnicate → usage, exit 2 |
| CLI-2 | Capture options: -p/--path <dir or file>, -c/--clipboard, -d/--delay <ms>, --region <WxH+X+Y|screenN|all>, --last-region, -r/--raw, -g/--print-geometry, --pin, -s/--accept-on-select, -n/--display <n>, --cursor, -e/--edit, and --upload <target> only while an upload target exists (PRIV-4). Each maps to a CAP-2 field. |
gui -d 2000 -c → overlay after 2 s; accept → copied |
| CLI-3 | Which command takes which option: gui — path, clipboard, delay, region, last-region, raw, print-geometry, pin, accept-on-select, cursor; full — path, clipboard, delay, raw, cursor; screen — display, path, clipboard, delay, region, raw, pin, edit, cursor; window — path, clipboard, delay, raw, pin, cursor. Others → exit 2. |
full --pin → exit 2 |
| CLI-4 | Validation before any capture: delay integer 0..600000; path names an existing folder, or a file whose parent folder exists; region per DSP-4; display ≥ 0. A failure names the option and exits 2. | -p /no/such/dir → exit 2, names --path |
| CLI-5 | full, screen and window with none of -c, -p, -r, --pin save to save_dir with filename_pattern, no dialog. -e opens the overlay on the chosen display instead. |
full → new file in save_dir |
| CLI-6 | -r writes the PNG bytes to stdout and nothing else there; -g writes WxH+X+Y and a newline (logical, global). With both, only -r output is written. |
gui -g, select 10×10 at 5,5 → 10x10+5+5 |
| CLI-7 | The CLI hands the request to the running instance, starting a temporary one when none runs, waits for the result and exits: 0 done, 1 cancelled, 2 usage error, 3 capture_denied/capture_unavailable/no_such_display, 4 output_failed. Output and exit code reach the calling terminal on every OS, Windows included. |
Esc in overlay → exit 1 |
| CLI-8 | config with no option opens the settings window. Options: --autostart <bool>, --tray <bool>, --notifications <bool>, --filename-pattern <pattern> (prints a sample name for now), --accent <colour>, --check (validates the settings file, lists problems, exits 1 if any). <bool> is exactly true or false; <colour> is #RGB, #RRGGBB or a CSS colour name. |
config --filename-pattern 'a_%Y' → prints a_2026 |
| CLI-9 | Command interface: the instance accepts the same requests over a local channel reachable only by the same OS user (per-user socket, named pipe or session bus). It never opens a network port. | other OS user sends a request → refused; port scan → no listener |
Out of scope
- A scripting or plugin language; requests beyond CAP-2 fields and
config.
Decisions
- One exit-code table (CLI-7) for every command so scripts can tell user cancel from failure.
windowis a CLI command of its own, added with CAP-7.
Displays
Status: Draft · Prefix: DSP
Purpose
How several displays, display scaling and the different desktop platforms shape what is captured and at what resolution.
Requirements
| ID | Requirement | Verified by |
|---|---|---|
| DSP-1 | Each display: {index, name, x, y, width, height (logical px), scale (≥ 1.0, e.g. 1.25, 2.0), primary}. The desktop is the bounding box of all displays in one global logical coordinate space. Indexes are 0-based, ordered by x then y of the top-left corner, and stay the same while the layout does. |
1920×1080 at 0,0 and 2560×1440 at 1920,0 → desktop 4480×1440; display at x=-1920 → index 0 |
| DSP-2 | Images use physical pixels: an area of w×h logical px on a display with scale s yields round(w·s)×round(h·s) pixels, never downscaled to logical size. |
100×50 on a 2.0 display → 200×100 image |
| DSP-3 | An area spanning displays with different scales is produced at the highest scale among the displays it touches; parts from lower-scale displays are upscaled. The saved image records DPI = 96 × that scale. | span 1.0 and 2.0 displays → whole image at 2.0, 192 DPI |
| DSP-4 | Region values are logical: WxH+X+Y in global coordinates, screenN for display N's bounds, all for the desktop. A region outside every display → invalid_region. |
400x300+10+10 on a 2.0 display → 800×600 image |
| DSP-5 | The overlay covers every display at once, each drawn at its own scale so UI stays sharp, and a selection may cross display edges. | drag from display 0 into display 1 → one selection |
| DSP-6 | The display layout is read when each capture starts; a display added, removed or rescaled while the overlay is open cancels the capture with notice display_changed. |
unplug a display during selection → overlay closes, notice shown |
| DSP-7 | Wayland: the grab goes through the desktop's screenshot permission service. The desktop may show its own consent dialog, first time or every time; the app never bypasses it. No such service → capture_unavailable (CAP-12). |
Wayland desktop without the service → no overlay, capture_unavailable |
| DSP-8 | Wayland: when window lists, global pointer position or the active window are not available, current means the primary display, interactive window falls back to region (CAP-7) and non-interactive window → capture_unavailable. |
Wayland, screen with current → primary display |
| DSP-9 | macOS: capturing needs the OS screen-recording permission. Without it the app captures nothing (never a blank image), explains how to grant it, and the request ends as capture_denied. X11 and Windows: displays are read directly with no consent dialog, also on X11 without a compositor or screenshot service. |
macOS permission off → no file, capture_denied; X11 minimal window manager → capture works |
Out of scope
- Per-display colour profiles and HDR tone mapping; images are 8-bit sRGB.
- Virtual desktops or workspaces other than the visible one.
Decisions
- Display order is defined by position (DSP-1) rather than by the platform's list order, so
display 1means the same screen on every OS. - Mixed-scale areas use the highest scale (DSP-3) so no detail is lost; the source behaves this way on macOS only.
Editing
Status: Draft · Prefix: EDT
Purpose
Changing annotations after they are drawn: undo and redo, selecting, moving, restyling, deleting and ordering.
Requirements
| ID | Requirement | Verified by |
|---|---|---|
| EDT-1 | Each completed change is one undo step: add, delete, move, resize, restyle, commit of a text edit, z-order change. Selection rectangle changes are not undo steps. Undo Ctrl+Z; redo Ctrl+Shift+Z or Ctrl+Y. |
draw 2 rectangles, Ctrl+Z → 1 left; Ctrl+Y → 2 |
| EDT-2 | undo_limit 1..1000 (default 100): beyond it the oldest step is dropped and can no longer be undone. |
limit 2, draw 3, undo 3 times → first drawing remains |
| EDT-3 | Any new change after an undo discards the steps that could have been redone. | draw A, B; undo; draw C; redo → nothing happens, A and C shown |
| EDT-4 | The history belongs to one overlay and is gone when the capture is accepted or cancelled. | accept, new capture, Ctrl+Z → nothing to undo |
| EDT-5 | Pointer tool (V): a click on an annotation selects it and shows its bounds; drag moves it; arrows nudge it 1 px (Shift 10 px); Delete or Backspace removes it; double-clicking a text annotation reopens it for editing. A click on empty space deselects. |
select arrow, Delete → arrow gone, one undo step |
| EDT-6 | With an annotation selected, changing colour, opacity, size, fill or font applies to that annotation only, as one undo step, and also becomes the tool's current setting. | select red rectangle, pick green → that rectangle green |
| EDT-7 | Annotation list in the side panel (SEL-8): one row per annotation, topmost first, showing tool and counter number or first text characters; selecting a row selects the annotation; move up/down changes z. |
move bottom rectangle up → it draws over the arrow |
| EDT-8 | Moving or resizing the selection does not move annotations: they keep their place on the frozen image and are clipped to the new selection on export. | annotate, move selection 50 px right → annotation stays, partly clipped |
Out of scope
- Grouping, aligning or copying annotations; editing annotations after export.
Decisions
- Selection changes stay out of the undo history (EDT-1) so undo never makes the area jump unexpectedly.
Output
Status: Draft · Prefix: OUT
Purpose
What happens to a finished image: clipboard, file, file names and formats, notifications, handing to another app, and the list of recent captures.
Requirements
| ID | Requirement | Verified by |
|---|---|---|
| OUT-1 | Copy puts the image on the system clipboard as lossless PNG, plus the platform's native bitmap format where one exists. clipboard_format: png|jpeg (png). |
copy, paste into an image editor → pixel-identical |
| OUT-2 | Save with save_path_fixed false (default) opens a save dialog in save_dir (default: the user's Pictures folder, else home) with a name from filename_pattern; with true it writes there at once, no dialog. A cancelled dialog is a cancelled save, not an error. |
fixed on → file appears with no dialog |
| OUT-3 | filename_pattern (default screenshot_%Y-%m-%d_%H-%M-%S) expands %Y %m %d %H %M %S %j %a %b %% from the local capture time. Characters invalid on any supported OS (/ \ : * ? " < > | and control characters) become -; a trailing lone % is dropped; an empty result uses the default. |
shot %H:%M at 09:05 → shot 09-05.png |
| OUT-4 | save_format: png|jpg|webp|bmp (png) sets the extension. A path given to a file keeps its extension when it is one of these, otherwise the request fails with unsupported_format. Quality 1..100 for jpg and webp (jpeg_quality, default 90); png and bmp are lossless. The file carries DPI per DSP-3 and no other metadata (PRIV-9). |
-p ~/a.tiff → unsupported_format |
| OUT-5 | Saving never overwrites silently: without a dialog, an existing name gets _1, _2, … (first free) before the extension; in the dialog the OS overwrite confirmation applies. |
a.png exists → a_1.png written |
| OUT-6 | copy_path_after_save (false): after a save, the clipboard holds the absolute path as text instead of the image. save_after_copy (false): every copy also saves to save_dir without a dialog. |
save_after_copy on, Ctrl+C → clipboard image and file both exist |
| OUT-7 | Several actions on one request run in the order copy, save, pin, print; a failing one (no permission, disk full) does not stop the rest, is reported, and the request ends as output_failed. |
read-only save_dir, copy+save → clipboard filled, save error reported |
| OUT-8 | Notifications (notifications, default true): after save, the full path, and clicking opens the containing folder; after copy, a short confirmation; after a failure, the reason. Notifications never show the image or its text content. |
save → notification with path, no preview image |
| OUT-9 | Open with (Ctrl+O): the OS chooser of apps that open images receives a temporary PNG; that file lives in the user's private temp folder and is deleted when the app quits or after 24 h, whichever comes first. |
open with viewer, quit app → temp file gone |
| OUT-10 | Clipboard ownership: where clipboard data disappears when its owner exits (X11, Wayland), the app keeps serving the copied image until another app takes the clipboard, even when the copy came from a CLI call. | full -c, CLI returns, paste → image pastes |
| OUT-11 | Recent captures: a list of the last recent_max saved images (0..100, default 10; 0 = off), each with path, time and a thumbnail (PRIV-7); opened from the tray or Shift+PrintScreen. Selecting one copies that file's image again; a missing file is shown as missing. |
save 3, open recent → 3 newest first |
Out of scope
- Printing to a printer, emailing, sharing sheets beyond the OS "open with".
- Editing settings of image formats beyond quality.
Decisions
- The default file name uses full date and time with seconds, so two quick captures rarely need OUT-5 numbering.
- Recent captures list only saved files (OUT-11); images that were only copied are never retained.
Pin
Status: Draft · Prefix: PIN
Purpose
Keeping a captured image floating above other windows as a reference.
Requirements
| ID | Requirement | Verified by |
|---|---|---|
| PIN-1 | Pin opens a borderless, always-on-top window showing the image at the selection's place and logical size; it has no taskbar or app-switcher entry. | pin 300×200 at 100,100 → window there, same size |
| PIN-2 | Dragging with the primary button moves the pin, across displays too; on a display with another scale it keeps its logical size. | drag from 1.0 to 2.0 display → same apparent size |
| PIN-3 | Zoom: wheel or pinch scales 10%..800% in 10% steps around the pointer; smooth scaling when pin_smooth_zoom is true (default), otherwise nearest-neighbour. |
wheel up 3 times → 130% |
| PIN-4 | Opacity 10%..100% (default 100%): keys 1..9 set 10%..90%, 0 sets 100%; menu items raise or lower by 10% within that range. A pin can never become fully transparent. |
press 3 → 30%; lower twice → 10% |
| PIN-5 | Secondary click opens a menu: copy, save, rotate left or right by 90° (the window takes the rotated size), raise opacity, lower opacity, close. Copy and save follow OUT using the original pixels, not the zoomed view. | zoom 200%, copy → original size image |
| PIN-6 | Close with Esc while the pin has focus, the menu, or "close all pins" in the tray menu. |
3 pins, close all → none left |
| PIN-7 | Pins live in memory only: quitting the app closes them and they are not restored on next start. | pin, quit, start → no pins |
| PIN-8 | Wayland: when the compositor does not honour placement or always-on-top, the pin opens where the compositor puts it and the app does not fake either. | Wayland → pin opens, may not stay on top |
Out of scope
- Drawing on a pin; pinning images that were not captured (from file or clipboard).
Decisions
- Zoom stops at 800% and opacity at 10% (PIN-3, PIN-4) so a pin can always be found and closed.
Privacy
Status: Draft · Prefix: PRIV
Purpose
Screenshots often hold private data. These rules keep images on the device unless the person sends them, make redaction safe, and bound what is kept.
Requirements
| ID | Requirement | Verified by |
|---|---|---|
| PRIV-1 | A fresh install makes no network request at all: no telemetry, crash reports, update checks, or downloads. | fresh install, capture/copy/save with traffic monitor → zero requests |
| PRIV-2 | check_for_updates (default false): when on, at most one request a day, fetching only the latest version number and sending nothing but the app version. |
turn on → one request per day, no image data |
| PRIV-3 | Upload is an opt-in integration: there is no upload button, shortcut or CLI flag until the person adds an upload target {name, endpoint, auth} in settings. No target is built in or preconfigured; Enter never uploads. |
fresh install → no upload control anywhere |
| PRIV-4 | Each upload shows a confirmation with a preview and the destination host unless that target's upload_without_confirmation is true (default false). The CLI uploads only with an explicit --upload <target name>. The returned link, and the deletion link when the service gives one, are shown and kept in a local upload list that can be cleared at once. |
upload → dialog shows host; cancel → no request; clear list → empty |
| PRIV-5 | Upload credentials are stored in the OS credential store; without one, in a separate file readable only by the user, with a warning. They never appear in the settings file or logs. | add target → settings file has no token |
| PRIV-6 | Images reach disk only through save, save_after_copy, open with (OUT-9) or recent-capture thumbnails; the frozen screen and annotations are held in memory only and dropped when the overlay closes. |
copy only → no new image file anywhere |
| PRIV-7 | Recent-capture thumbnails are at most 256 px on the longest side, kept only for saved images, capped at recent_max, stored in the user cache folder readable only by the user; "clear recent" deletes all of them, and recent_max 0 deletes them and stores no more. |
set 0 → thumbnail folder empty |
| PRIV-8 | Redaction is safe by default: pixels inside a redact rectangle never influence its output; pixelate and blur derive colour only from pixels just outside its edges, solid uses the draw colour. insecure_redaction (default false) allows true pixelate or blur of the inside. |
two captures differing only inside the rectangle → identical output pixels |
| PRIV-9 | Saved files contain pixels, DPI and an sRGB tag only: no user name, host name, window titles, app version, location or capture time in metadata. | save, read metadata → only DPI and colour profile |
| PRIV-10 | mark_copy_sensitive (default false): when true, copies carry the platform's "do not keep in clipboard history" marker so clipboard managers skip them. |
on, copy → clipboard history tool ignores it |
| PRIV-11 | Logs hold errors and warnings only, never image data, clipboard content, window titles or saved file names. | trigger a save error → log has reason, no file name |
Out of scope
- Built-in upload providers, cloud accounts, sync of settings.
- Detecting sensitive content (faces, cards, passwords) automatically.
Decisions
- No upload target ships preconfigured (PRIV-3); the source ships one image host with a built-in client key and binds a default key to it.
- Update checks are off by default (PRIV-2); the source turns them on.
- Safe redaction (PRIV-8) follows the source's default of sampling only the area's border.
Selection
Status: Draft · Prefix: SEL
Purpose
The interactive overlay: choosing and adjusting the area, the toolbar, and finishing or abandoning a capture.
Requirements
| ID | Requirement | Verified by |
|---|---|---|
| SEL-1 | The overlay shows the frozen image on every display. Outside the selection the image is dimmed by dim_opacity 0..255 (default 180); before any selection the whole image is dimmed and, when show_help is true (default), a short list of the main shortcuts is shown. |
open overlay → help visible; select → only the outside dimmed |
| SEL-2 | Dragging with the primary button draws the selection; releasing finishes it. A drag smaller than 2×2 logical px counts as a click (CAP-7). | drag 1 px → no selection |
| SEL-3 | A selection has 8 handles (corners and edge midpoints) to resize and an inside area to move; with Shift held a handle resizes symmetrically around the centre. The selection always stays inside the desktop. |
drag the right handle with Shift +10 px → left edge moves −10 px |
| SEL-4 | Keyboard: arrows move the selection 1 logical px; Shift+arrow moves the right or bottom edge 1 px; Ctrl+Shift+arrow grows or shrinks both opposite edges 1 px each; Ctrl+A selects the display under the pointer, a second Ctrl+A the whole desktop. |
Ctrl+A twice on 2 displays → desktop selected |
| SEL-5 | While a selection is drawn, moved or resized its size (physical px) and position are shown next to it and hidden geometry_hide_ms after the last change (0..10000, default 2000; 0 = always shown). |
resize, wait 2 s → size label gone |
| SEL-6 | Magnifier, off by default (show_magnifier): a zoomed view of about 15×15 px around the pointer at 8× with a crosshair and the hex colour of the centre pixel. |
turn on, hover a red pixel → shows #FF0000 |
| SEL-7 | Once a selection exists, a toolbar of tools (ANN) and actions (OUT) appears beside it, outside when there is room, otherwise inside along an edge, always fully on one display. Which buttons appear and in what order is toolbar_buttons. |
selection touching all display edges → toolbar inside the selection |
| SEL-8 | Space toggles a side panel with the current tool's options (colour, size, font) and the annotation list (EDT-7). |
press Space → panel shown; again → hidden |
| SEL-9 | Esc ends text editing when a text box is active; otherwise it cancels the capture. Ctrl+Backspace removes the selection and its annotations and keeps the overlay open. A cancelled capture writes, copies and pins nothing; a notification says so when notify_on_cancel is true (default false). |
Esc with no text box → overlay closes, clipboard unchanged |
| SEL-10 | Enter accepts: runs the request's actions, or default_accept_action: copy|save|copy_and_save|pin (default copy) when it has none. Enter with no selection accepts the whole display under the pointer. Ctrl+C, Ctrl+S and the pin action accept with that one action; with accept_on_double_click (default false) a double-click inside the selection accepts too. |
no selection, Enter → clipboard holds that display |
Out of scope
- Free-form (lasso) or multiple disjoint selections.
- Grid snapping and rulers.
Decisions
Ctrl+Aescalates display → desktop (SEL-4) so one key covers both, instead of the source's single whole-desktop step.- Upload is never a default accept action; see PRIV-3.
From Specfork/screenshot at 280450a, licence CC-BY-4.0.