Files for my website
bwc9876.dev
1---
2title: Custom Screen Capture Flow
3date: 2024-07-25
4summary: An adventure in making scripts to capture screen shots and recordings.
5cowsay: A picture is worth a thousand words
6---
7
8I've recently been going down the path of madness known as customizing my desktop and I figured I'd
9share some neat scripts and setups I've done along the way.
10
11Today I'll go into some scripts I've been working on to capture screen shots and recordings. They
12allow selecting regions of the screen and specific windows, and I also made it so you can edit them
13afterwards.
14
15## Background
16
17My entire system and home folder is managed by [NixOS](https://nixos.org), so I have a
18[configuration repository](https://github.com/Bwc9876/nix-conf) where all my scripts and configs can
19be found, I'll reference them throughout this post and provide links to the current version of each
20so you can see if I've updated them since this post.
21
22Currently I use [Hyprland](https://hyprland.org) as my window manager, and have been duct-taping
23components together to make my own little Desktop Environment around it.
24
25I also like to use [NuShell](https://nushell.sh) as my shell, and these scripts will be written in
26it. If you haven't checked out NuShell yet, I highly recommend it!
27
28## Screenshots
29
30First is the script to take screenshots. This is a relatively simple script as it simply builds on
31top of [grimblast](https://github.com/hyprwm/contrib/tree/main/grimblast) with some nice QoL
32features.
33
34To install grimblast, all I have to do is add it to my `environment.systemPackages`:
35
36```nix
37{
38 environment.systemPackages = with pkgs; [
39 # ...
40 grimblast
41 libnotify # For notifications
42 xdg-utils # For opening files
43 # ...
44 ];
45}
46```
47
48Grimblast will automatically save screenshots to `XDG_SCREENSHOTS_DIR`, I manually set this in my
49_home manager_ config with:
50
51```nix
52{
53 xdg.userDirs.extraConfig.XDG_SCREENSHOTS_DIR = "${config.home.homeDirectory}/Pictures/Screenshots";
54}
55```
56
57Grimblast will name the screenshots with the current date and time, which works for me.
58
59Now along to actually using grimblast, I'll create a new script and put in in my config somewhere,
60we'll call it `screenshot.nu`. I usually like to place any non-nix files in a folder called `res` at
61the root of my config, we'll get to actually calling this script once we're done writing it.
62
63To start out we need to call grimblast, I like to use `copysave` as the action as I like having it
64immediately in my clipboard, and having it saved for later. I'll also add `--freeze` which simply
65freezes the screen while I select the region to capture.
66
67```nushell
68let file_path = grimblast --freeze copysave
69```
70
71grimblast will then return the path to the saved screenshot, which we save in `file_path`. If the
72user were to cancel the selection (press escape), `file_path` would be empty, so we want to make
73sure to check for that so we're not trying to open a non-existent file.
74
75```nushell
76if $file_path == "" {
77 exit 1
78}
79```
80
81Now the main part, we'll send a notification that the screenshot was saved, and have options for it.
82
83I want four actions for the screenshot:
84
85- Open
86- Open Folder
87- Edit
88- Delete
89
90Also since grimblast saves the screenshot as a png, I can pass it as the icon of the notification.
91
92```nushell
93let choice = notify-send --app-name=screengrab -i $file_path -t 7500 --action=open=Open --action=folder="Show In Folder" --action=edit=Edit --action=delete=Delete "Screenshot taken" $"Screenshot saved to ($file_path) and copied to clipboard"
94```
95
96A long command here, `notify-send` allows us to send a notification to the currently running
97notification daemon. In my case I'm using
98[swaync](https://github.com/ErikReider/SwayNotificationCenter).
99
100- `--app-name` is the name of the application that sent the notification, I say screengrab here so
101 swaync will show an icon in addition to the image, also so I can play a camera shutter sound when
102 the notification is sent.
103- `-i` is the icon to display in the notification, in this case the screenshot we just took.
104- `-t` is the time in milliseconds to show the notification
105- `--action` is actions to display in the notification, `name=Text`
106- First position argument is the notification title, and second is the body.
107
108With that we get a neat notification when we screenshot.
109
110
111
112Now we need to handle each action, the chosen action is returned by notify-send, so we can match on
113that.
114
115- "Open" and "Open Folder" are pretty simple, just pass `$file_path` and `$file_path | path dirname`
116 to `xdg-open`
117- "Edit" I'll simply pass the file path to my editor, I chose
118 [swappy](https://github.com/jtheoof/swappy) because of it's simplicity and ease of use.
119- "Delete" I'll just remove the file.
120
121```nushell
122match $choice {
123 "open" => {
124 xdg-open $file_path
125 }
126 "folder" => {
127 xdg-open ($file_path | path dirname)
128 }
129 "edit" => {
130 swappy -f $file_path
131 }
132 "delete" => {
133 rm $file_path
134 }
135}
136```
137
138And that's it! I now have a fairly robust screenshot script.
139
140### Screenshot Invocation
141
142Now in terms of actually calling it I'll be binding it to `Win` + `Shift` + `S` in Hyprland, as well
143as `PrintScreen`.
144
145In home manager I simply have to add these strings to my Hyprland binds
146
147```nix
148{
149 wayland.windowManager.hyprland.settings.bind = [
150 # ...
151 ",Print,exec,nu ${../res/screenshot.nu}"
152 "SUPER SHIFT,S,exec,nu ${../res/screenshot.nu}"
153 # ...
154 ];
155}
156```
157
158Now by switching to my new config (and making sure to stage `screenshot.nu` of course), I can take
159screenshots with a keybind!
160
161## Screen Recordings
162
163This will be a bit more involved mainly because something like grimblast doesn't exist for screen
164recordings. Looking at existing solutions I couldn't find any that I really liked, mostly because
165they involved some additional UI. To be clear this script will be for _simple_, _short_ recordings,
166long-term stuff I'll still prefer to use something like OBS.
167
168For the actual screen recording I'll be using [wf-recorder](https://github.com/ammen99/wf-recorder).
169
170```nix
171{
172 environment.systemPackages = with pkgs; [
173 # ...
174 wf-recorder
175 libnotify # For notifications
176 xdg-utils # For opening files
177 slurp # Will explain this later
178 # ...
179 ];
180}
181```
182
183First and foremost location, I chose to use `~/Videos/Captures` for my recordings. I didn't set an
184environment variable for this, it'll be hardcoded in the script.
185
186```nushell
187let date_format = "%Y-%m-%d_%H-%M-%S"
188
189let captures_folder = $"($env.HOME)/Videos/Captures"
190
191if not ($captures_folder | path exists) {
192 mkdir $captures_folder
193}
194
195let out_name = date now | format date $"($captures_folder)/($date_format).mp4"
196```
197
198This will handle determining the folder and name for the recordings, and creating the folder if it
199doesn't exist.
200
201Next up I want to have a similar selection process to the screenshot script, to do this I'll use
202[slurp](https://github.com/emersion/slurp) to select areas of the screen, which is what grimblast
203uses under the hood.
204
205In addition, grimblast does some communication with Hyprland to get window information such as
206position and size, this lets you select a window to take a screenshot of. I'll be getting that info
207manually from Hyprland using NuShell instead:
208
209```nushell
210let workspaces = hyprctl monitors -j | from json | get activeWorkspace.id
211let windows = hyprctl clients -j | from json | where workspace.id in $workspaces
212let geom = $windows | each { |w| $"($w.at.0),($w.at.1) ($w.size.0)x($w.size.1)" } | str join "\n"
213```
214
215This gets all the geometry in a format `slurp` will be able to parse and use.
216
217```nushell
218let stat = do { echo $geom | slurp -d } | complete
219
220if $stat.exit_code == 1 {
221 echo "No selection made"
222 exit
223}
224```
225
226I do `complete` here to get the exit code of the slurp command, if it's 1 then the user cancelled
227the selection and similar to the screenshot script I'll exit.
228
229Now it's time to actually record, the stdout of `slurp` contains the geometry that we want to
230capture, so we'll pass that to `wf-recorder` with the `-g` flag:
231
232```nushell
233wf-recorder -g ($stat.stdout) -F fps=30 -f $out_name
234```
235
236Pretty simple command, `-g` is the geometry to record, `-F` is the format options, and `-f` is the
237output file.
238
239Now we'll run into an issue if we run this script and start recording, there's no way to stop it!
240I'll cover how we're going to get around that when it comes to setting up keybinds.
241
242Assuming `wf-recorder` stops, we'll send a notification to the user that the recording is done:
243
244```nushell
245let action = notify-send --app-name=simplescreenrecorder --icon=simplescreenrecorder -t 7500 --action=open=Open --action=folder="Show In Folder" --action=delete=Delete "Recording finished" $"File saved to ($out_name)"
246```
247
248
249
250Most arguments are the same here as the screenshot script, the only difference is the icon and app
251name. The actions are also basically the same, so I'll leave out the explanation and just show the
252handler:
253
254```nushell
255match $action {
256 "open" => {
257 xdg-open $out_name
258 }
259 "folder" => {
260 xdg-open $captures_folder
261 }
262 "delete" => {
263 rm $out_name
264 }
265}
266```
267
268### Start/Stop Recording
269
270Now to actually call the script, I'll bind it to `Win` + `Shift` + `R` in Hyprland.
271
272However, we're going to do something special with the `exec` line here. Instead of simply calling
273the script we're going to check if `wf-recorder` is already running, if this is the case we can send
274`SIGINT` to it to make it stop recording, meaning our script will continue and show the
275notification.
276
277```nix
278{
279 wayland.windowManager.hyprland.settings.bindr = [
280 # ...
281 "SUPER SHIFT,R,exec,pkill wf-recorder --signal SIGINT || nu ${../res/screenrec.nu}"
282 # ...
283 ];
284}
285```
286
287`pkill` here will exit with code `1` if it doesn't find any processes to kill, so the `||` will run
288our script if `pkill` fails.
289
290Note that I did this on `bindr`, this means the keybind will only happen once the R key is
291_released_ rather than pressed. This is to prevent a weird issue I ran into where the recording
292would stop immediately after starting.
293
294And that's it! We can now screen record with ease. It won't record audio (might do an additional
295keybind in the future) and it also doesn't copy the recording to the clipboard, but it works pretty
296well for short videos.
297
298## Full Scripts
299
300### Screenshot Script
301
302```nushell
303#!/usr/bin/env nu
304
305let file_path = grimblast --freeze copysave area
306
307if $file_path == "" {
308 exit 1;
309}
310
311let choice = notify-send --app-name=screengrab -i $file_path -t 7500 --action=open=Open --action=folder="Show In Folder" --action=edit=Edit --action=delete=Delete "Screenshot taken" $"Screenshot saved to ($file_path) and copied to clipboard"
312
313match $choice {
314 "open" => {
315 xdg-open $file_path
316 }
317 "folder" => {
318 xdg-open ($file_path | path dirname)
319 }
320 "edit" => {
321 swappy -f $file_path
322 }
323 "delete" => {
324 rm $file_path
325 }
326}
327```
328
329[Most recent version on GitHub](https://github.com/Bwc9876/nix-conf/blob/main/res/screenshot.nu)
330
331### Recording Script
332
333```nushell
334#!/usr/bin/env nu
335
336let date_format = "%Y-%m-%d_%H-%M-%S"
337
338let captures_folder = $"($env.HOME)/Videos/Captures"
339
340if not ($captures_folder | path exists) {
341 mkdir $captures_folder
342}
343
344let out_name = date now | format date $"($captures_folder)/($date_format).mp4"
345
346let workspaces = hyprctl monitors -j | from json | get activeWorkspace.id
347let windows = hyprctl clients -j | from json | where workspace.id in $workspaces
348let geom = $windows | each { |w| $"($w.at.0),($w.at.1) ($w.size.0)x($w.size.1)" } | str join "\n"
349
350let stat = do { echo $geom | slurp -d } | complete
351
352if $stat.exit_code == 1 {
353 echo "No selection made"
354 exit
355}
356
357wf-recorder -g ($stat.stdout) -F fps=30 -f $out_name
358
359let action = notify-send --app-name=simplescreenrecorder --icon=simplescreenrecorder -t 7500 --action=open=Open --action=folder="Show In Folder" --action=delete=Delete "Recording finished" $"File saved to ($out_name)"
360
361match $action {
362 "open" => {
363 xdg-open $out_name
364 }
365 "folder" => {
366 xdg-open $captures_folder
367 }
368 "delete" => {
369 rm $out_name
370 }
371}
372```
373
374[Most recent version on GitHub](https://github.com/Bwc9876/nix-conf/blob/main/res/screenrec.nu)