Files for my website bwc9876.dev
at main 374 lines 12 kB view raw view rendered
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![A notification that I just took a screenshot with the screenshot visible](@assets/blog/wip1_screenshot_notif.webp) 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![A notification that I just took a screen recording with a video camera icon visible](@assets/blog/wip1_screenrec_notif.webp) 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)