The scripts that take gamut's
pictures, and the pictures themselves: one script in scripts/ per picture
in screenshots/, so that a picture can be taken again after the interface
changes and come out the same size, in the same place, showing the same
thing. What each one shows is the script. gamut's README links to the
pictures here rather than carrying them, so taking one again and pushing it
is what changes the picture there.
| script | picture |
|---|---|
main_screenshot |
a photograph zoomed in three steps and panned to the summit, with the minimap |
animated |
an animated WebP opened paused, with the histogram, then played through once with the transport bar |
pixel_grid |
the grid on, the wheel rolled over the stag until the grid is at single pixels, then Space |
pixel_copy |
the grid on and the wheel rolled into the mountain, the dot at the head of the readout hovered and pressed, Hex chosen, and Ctrl+. over the picture |
open_in |
the photograph with the open button in the left strip pressed and the "Open in…" menu of what this desk will open it in standing beside it |
curated_info |
a phone's photo left a moment on its own, then the information toggle in the right strip clicked and the Curated tab wheeled down through its sections, File at the head to Regions at the foot |
compare |
the three rasters of one mountain flipped through, the wheel rolled into the crater on the way |
false_color |
the hillshade held over the crater, the histogram opened from its button and the swatches under its Display band clicked through the four color maps, then the band's black handle dragged three tenths of the way in and its white one a tenth |
fuzzy_finder |
the chooser over a directory of several hundred pictures, opened from the counter at the head of the top bar and one picture chosen by a few letters, then opened again with Ctrl+P and one asked for by its number, typed slowly |
file_list |
the seven pictures of one mountain as their directory, the list down the left: the menu at its head opened and Area chosen, then Descending, which takes the strip to its foot, the wheel rolling it back to its head, and the first row clicked for the elevation model, the wheel turned over the crater, then the third for the shaded relief of the same ground, which comes up where the model was left |
themes |
the showcase's last beat on its own: the whole monitor on an empty workspace, the mountain's directory with the information panel open, Omarchy's theme menu taking the desk and the window to Lupine, Ctrl+P to the relief map under the new palette, and the menu back to the theme it started on |
histogram |
the photograph with the histogram opened from its button, the black and white handles on the band each dragged a fifth of the way in, and the warning triangle at the head of the panel pressed to paint the clipped pixels, then pressed again to take the paint off |
histogram_raw |
a Canon raw zoomed to 200% on the dragon's whisker, the histogram opened and wheeled down past Develop to Photosites, then the raw mosaic laid over the picture from the Photosites eye, the loupe turned on and taken along the whisker's edge, the wheel turned to about 400%, and Shift+M to take the mosaic off |
ui |
the directory of the mountain's four rasters, the pointer held on the counter at the head of the top bar for its tooltip, the help button at the foot of the right strip pressed and the table of keys scrolled to its end over three seconds, Esc, and the button at the end of the top bar pressed to hide the interface |
mandelbrot |
a PNG that a program rewrites every second, one step further into the Mandelbrot set, watched for ten seconds: zoomed four steps into the middle and panned once around it partway, then Space |
region |
a web page's screenshot, x pressed and a box dragged out ten pixels loose around the wordmark, the wheel turned five notches in over the handle on its left edge, the handle clicked and Right pressed until the edge meets the first letter, the picture dragged to bring the top, right and bottom handles into view in turn and each brought in the same way, then Ctrl+C and Ctrl+V, so that the copy is pasted and shown, cut to the pixel |
export |
the mountain's shaded relief under the viridis ramp, a box dragged out a thousand pixels square around the summit and zoomed into with space, and Export chosen from the file menu: the dialog's first size box set to 25%, and the quarter-size crop written, shown as it lands, and put at its own size |
loupe |
a robin, the loupe's toggle in the bottom bar clicked, the pointer glided onto the bird's eye, and Shift+L pressed 300 ms apart round its four magnifications and back to 4×, then the toggle clicked off; and the other way of having it, the pointer taken down to the bird's right foot and the right button held, which holds the loupe up for as long as it is down, and the pointer walked slowly up the toe to the ankle |
showcase |
the whole desk rather than the one window, on an empty workspace at scale 2: six pictures of one mountain opened once and walked through — the summit under the wheel, the loupe on a bird's eye, the sun's clipped pixels painted and the frame thrown away, the crater held up the three rasters, the panel that says where on the mountain it is — and Omarchy's own theme menu retinting the desk and the window with it at the end |
scripts/main_screenshot # screenshots/main_screenshot.jpg
GAMUT=~/build/gamut scripts/compare # with another binary
SCREENSHOTS=/tmp/shots scripts/false_color # to another directory
scripts/all # every one of them, in the order aboveFive things are the environment's to say, each with a default:
| variable | what | default |
|---|---|---|
GAMUT |
the binary to drive | ~/git/gamut/target/release/gamut |
SCREENSHOTS |
where the pictures go | screenshots/ here |
FILMS |
where the recordings go before they are cut | films/ here, ignored by git |
CODEC |
what record encodes in |
h264 |
QUALITY |
what it encodes at | the recorder's own default |
The pictures they open are in images/: seven of one mountain in
images/mora — a photograph, two raw frames, an aerial and the three
12.5 m rasters of the same ground — several hundred bird plates in
images/birds, two spatial photos that carry a depth map in
images/depth, and the animated WebP and the stag beside them. Each script takes
another path as its first argument. mandelbrot opens no picture of ours:
the program that draws its picture is the Rust crate in
mandelbrot-zoom/, which the script builds if cargo build --release has not been run there already, or MANDELBROT names
another binary.
lib.sh is what the scripts share, and it speaks only Hyprland.
The compositor has to do three things the program cannot do for itself:
- Leave the window at the size asked for.
--sizeis a request, and a tiling layout ignores it.openstarts the program throughhl.exec_cmdwith a rule set that floats the window at exactly that size, and at full opacity: Omarchy's default rules make every window slightly translucent, which would blend whatever is behind it into the picture. - Put it on a device pixel.
hyprctl clientsreports the window's position and size in logical pixels, andgrim -gcaptures that rectangle at the monitor's own scale. On a monitor at scale 1.6 a 1200×675 window comes out as a 1920×1080 image, but only every fifth logical position is a whole device pixel, and a window centered under the bar starts at device row 620.8: the first captured row is then Hyprland's border, which is blue while the window has focus, gray while it does not, and a half-second fade between whenever the pointer leaves and comes back. In a film that row flickers.placementcenters the window as thecenterrule would and then moves it to the nearest position that is whole at the monitor's scale, so every captured pixel is the window's own. - Type into it.
wtypepresses keys on a virtual keyboard, and they go to whichever window has focus. Focus follows the pointer, so the pointer is put in the window before anything is typed.
keys presses one key per wtype call, and the reason is worth knowing
before changing it. wtype builds its keymap as it goes: each new keysym is
added and the whole keymap sent to the compositor again, which forwards it to
the focused window. A key pressed under the new keymap before the window has
read it is interpreted under the old one, and wtype -k plus -k Up reaches
the program as something other than + and Up. One keysym per process is
one keymap per process, and every key lands as itself.
The pointer is placed by cursor, which goes out of the window and back in
rather than straight to the point, because a warp within the window sends
the window no motion event: the readout of wherever the pointer last was
over the picture would stay in the bar, and the wheel would turn about the
old place. Leaving and entering are events, and the entering is also what
gives the window the keyboard. cursor checks that it did, and when it did
not — the desk's own mouse moved, a window came up over the spot — falls
back on the compositor's focus dispatch, which warps the pointer to the
window's center, and then leaves and enters again. keys and press make
the same check before they send anything; one run of fuzzy_finder before
that check typed its query into a browser's print dialog. Between shots the
pointer is parked on the middle of the top bar, the one place it shows in
nothing.
close kills the window and then waits for the compositor to forget it.
The next window can be given the same address, and open tells the new
window from the ones already open by address, so a script that opens twice
in a row would otherwise take the second window for the first.
wtype has keys and no pointer, and Hyprland can warp the pointer but not
press its buttons or turn its wheel. What the wheel does — zoom about the
pointer — and what a drag does are half the program, so
device.py is a mouse of its own: a device registered through
/dev/uinput for as long as one command runs, sending wheel notches or a
held button and motion, then taken away again. It uses nothing outside
Python's standard library; the ioctl numbers and the event record are
written out from the kernel's own headers. Omarchy gives the user write
access to /dev/uinput through an ACL, which is what makes this possible
without root. wheel, drag and click in lib.sh call it, and record NAME cursor keeps the pointer in the film for a recording where the pointer
is the point.
glide moves the pointer through it too, for a film where the pointer's
way to a button is part of the picture and cursor's leap out of the
window and back is not, and for a tooltip, which the program shows only
over a pointer that arrived by motion. The compositor accelerates motion
from a mouse, so that a unit of motion carries a distance that depends on
the pace it comes at; device.py has Hyprland give this device alone a
flat profile, through the Lua API's hl.device, at a sensitivity that
makes every unit exactly a quarter of a logical pixel however fast it is
sent. Where the pointer is then is arithmetic: where it was when the
device was made, asked of the compositor once, plus the units sent over
four. A glide is the units that take it to its mark, at a pace of sixteen
pixels a frame taken over the move — eased, so that it sets off and stops
gently as a hand does — and it lands within an eighth of a pixel. drag_to
is the same with the button held.
The pointer's place is a fraction, and hyprctl cursorpos cuts the
fraction off; the Lua API has it whole, and since hyprctl eval prints
nothing a script returns and only what it raises, device.py raises the
position and reads it off the error. It is read once, when the device is
made, and once more at the end, to check the sum; asked after each action
it would be a pause between them.
Where the pointer is and where the window has it are two things. The
compositor tells the window of a motion only when the whole logical pixel
the pointer is in changes, and then tells it the exact place of that
event; a last move of less than a pixel is not heard, and the window has
the pointer wherever it last crossed from one pixel into the next — up to
a pixel from where it stopped, which at a zoom under 100% is most of an
image pixel. So every glide ends on a move the window must hear: its last
two pixels are one step, sent as one event, which crosses a pixel's edge
along whichever axis it mostly goes and lands on the mark. place is the
same motion under a name for where the pixel under the pointer is what
matters, the two ends of the drag that draws region's box.
A drag has its own trouble at the other end. A toolkit takes a press for a
click until the pointer has gone some pixels from it, and the motion of
the frame that decides it is not part of the drag; a stroke that sets off
at speed loses its first several pixels that way. So drag_to creeps the
first ten pixels a couple of units at a time, past the toolkit's distance,
before it moves at its pace, and what the deciding frame loses is meant to
be a fraction of a pixel. Against the program as it is now that is not
quite so: a pan still comes up six or seven logical pixels short, and a box
drawn from a press starts a few image pixels past it. region, which has
to land on given pixels, makes up for both: each of its pan strokes is
drawn six pixels longer than the pan it is counted as, and its box is
measured rather than assumed, as below.
region draws its box once before the film starts and reads where its
edges came out, then counts the presses that tighten each edge from that.
The far corner it asks the program for — Ctrl+Shift+. copies the
coordinate of the pixel under the pointer, and the clipboard is read back
— and the near corner it works out from the size of the box, copied with
Ctrl+C and read off the picture on the clipboard. The box is taken off
again, the coordinate is asked last so that the clipboard holds words and
the paste button is not up, and the toast that says it was copied is gone
before the recorder starts.
Several of the device's actions are done as one hand's gesture, the
device made once and the actions given to it in a row, separated by --:
the glide to a handle, the click that takes it, the drag that brings the
next part of the picture into view, the arrows that bring the edge in, a
rest where a hand would pause. The whole of region's film is one such
gesture, planned on the script's own model of the view, which the exact
motion lets it trust: a device made for each stage was most of a second
of the pointer sitting still between them. A pan too long for one stroke
inside the window is broken into two or three.
What the window makes of a click or a drag is decided a frame at a time, so the device gives it two frames where the order of things matters: after a glide, before the click or press there, since a click on a handle is a click on whichever handle the window last drew the pointer over; and after the button comes up, before the pointer moves on, since a click counts only if the pointer is still on the widget in that frame, and motion in the frame a drag is released is taken as part of the drag.
The desk hides the pointer when a key is pressed, and tells the window it
has left, which takes down whatever was up because the pointer was over
something — a tooltip, a region's measurements. keep_pointer turns that
off for the run, through the compositor's Lua config, and cleanup puts
it back however it was.
It is a keyboard too, for the bindings wtype cannot reach. A binding in
gamut's app/input.rs is matched either by what the key says (Char("+"))
or by where it is (Position(KeyCode::Digit2), which is how Shift+2 is
50% on any layout). wtype types a keysym under a keymap of its own, at a
keycode it chose, so the window sees the right character at the wrong
position; a key from the device is the real keycode read under the real
keymap, and matches either way. press shift+2 in lib.sh is that. keys
stays on wtype for everything else, because a keysym is what a binding by
character wants and does not depend on the desk's layout.
The pointer is placed first with cursor, and fitted W H X Y says where
image pixel X, Y of a W×H image is in the layout while the image is fitted
to the window — the same sum gamut's chrome::content_area and
View::fit_zoom do, redone in awk, so that a script can put the pointer on
a feature of the picture by its own coordinates. A fifth number is what
the file list takes off the left of that area where it is up, which
file_list reads from gamut's own state file: the width it fits a
thumbnail into is the one thing about the list the program keeps between
runs, and where the rows are and where the picture is both follow from it.
record starts gpu-screen-recorder on the same rectangle, at 60 frames a
second, and waits for the .ts file it writes beside the film with its
first frame's wall-clock time. That is the clock the film is cut by:
since_record says how long after the first frame something was done, and
clip takes a start and a length in those seconds. animated
reads the start before it presses Return and the length from the file's
own frame durations, so the clip is one loop of the file from the moment
play was pressed, and loops where the file does. It reads those durations
with ffprobe rather than ImageMagick, whose delay is in whole hundredths
and so reads an 83.33 ms WebP frame as 80. cut stops the recorder with
SIGINT, which is how it is told to finish the file rather than drop it.
clip is ffmpeg cutting the film and encoding it again, at the film's own
1920×1080 and 60 frames a second: x264 at crf 18 with the index moved to
the front, which is the encode showcase delivers. poster keeps one
frame of that clip as a JPEG at the quality shoot writes, a still to
stand in for the film wherever a video cannot be played; its time is in
the clip's own, so animated asks for 9 s of its 12.
mandelbrot is a recording of the file under the window changing and the
window following, first as it was opened and then under a pan and zoom of
its own: three seconds in, + four times and a turn of Shift and the arrows around
the middle, which is where the renderer is heading, and each frame that
lands meanwhile comes up under the same view, since a file of the same
size keeps it; then Space fits the whole picture again. The renderer writes a 1024×1024 frame into
$FILMS every second, each to a temporary file renamed over the last, so
the window's watch — a stat every quarter second, acted on once the file
has held still for one — never reads a half-written frame. It draws its
first frame in milliseconds and the window takes a second or so to open,
so the script holds the renderer with SIGSTOP after that first frame and
lets it go with SIGCONT once the recorder is running, and every take
begins on the whole set. Ten seconds is nine steps of the zoom, at 0.8 a
step.
themes films the whole monitor rather than the window, as showcase
does and with its staging: an empty workspace, so the desk's background
stands behind the window, and scale 2, so the 3840×2160 master comes down
2:1 to the delivered 1920×1080. The window is opened once: gamut watches
the desktop's palette file and retints itself when Omarchy rewrites it.
The script puts the workspace, the scale, the theme and the background
back on whatever they were, however it ends.
They are not tests, and they do not run in CI. They open a window on whatever workspace is active and type into it, so they want a desk with someone at it who is not typing anything else — the keys go to whichever window has focus, and the focus check is a remedy, not a guarantee. The window the screenshot shows wears the desktop's theme, as the program always does, so a picture taken on a different theme is a different picture.