Skip to content

About

Gamut scripts for capturing screenshots/recordings.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

36 Commits

Folders and files

Repository files navigation

gamut-scripting

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 above

Five 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.

What a script does

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. --size is a request, and a tiling layout ignores it. open starts the program through hl.exec_cmd with 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 clients reports the window's position and size in logical pixels, and grim -g captures 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. placement centers the window as the center rule 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. wtype presses 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.

Closing

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.

A device of our own

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.

A recording

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.

What the scripts are not

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.

About

Gamut scripts for capturing screenshots/recordings.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages