Files for my website bwc9876.dev

WIP Fridays Day 1

bwc9876.dev 61b9b90c 94048d09

verified
+362 -463
src/assets/blog/wip1_screenrec_notif.png

This is a binary file and will not be displayed.

src/assets/blog/wip1_screenshot_notif.png

This is a binary file and will not be displayed.

-463
src/content/posts/advent_adventures_0.md
··· 1 - --- 2 - title: Advent Adventures - Prologue 3 - date: 2023-12-19 4 - summary: A preparation post for when I begin going through Advent of Code year-by-year 5 - cowsay: Happy Holidays! 6 - --- 7 - 8 - Over the past few years I've done [Advent of Code](https://adventofcode.com) on and off. At the time of writing, I'm on [day 19 of this year's challenge](https://adventofcode.com/2023/day/19) and will (hopefully) complete this year. After that, I want to go back and do all the previous years. I'll be writing a post for each year that I do, with a few highlights for the days I enjoyed and/or struggled with. I'll be using [Rust](https://www.rust-lang.org/) for all of my solutions, as it's a language I really want to learn in-depth. 9 - 10 - Of course, I can't call myself a programmer if I don't needlessly over-complicate things. In addition to solving each day I want to keep everything organized, I want to make a repo for all of my Advent solutions. In addition to each year, I want to make a central utils crate that I can use across all years, I want this to be a sort of "swiss-army-knife" of competitive programming tactics/algorithms-- something I can look back on later for reference and also use next year. 11 - 12 - I also want this repo to be easy to use. That is, I want it to be able to generate most of the boilerplate for me through macros. I want to be able to generate a year with a single command and have all the days of that year ready to be implemented. 13 - 14 - This repo should also be able to make a single binary I can use to run any of my solutions across all of the years I've done. 15 - 16 - Lets get started! 17 - 18 - ## The Project Structure 19 - 20 - I'm going to be using [cargo workspaces](https://doc.rust-lang.org/book/ch14-03-cargo-workspaces.html) to organize everything. The structure will look something like this: 21 - 22 - ```dir 23 - advent/ 24 - ├─ src/ 25 - ├─ core/ 26 - │ ├─ Cargo.toml 27 - │ ├─ src/ 28 - ├─ macros/ 29 - │ ├─ Cargo.toml 30 - │ ├─ src/ 31 - ├─ util/ 32 - │ ├─ Cargo.toml 33 - │ ├─ src/ 34 - ├─ years/ 35 - │ ├─ 20XX/ 36 - │ │ ├─ Cargo.toml 37 - │ │ ├─ src/ 38 - │ │ │ ├─ day_X.rs 39 - ├─ Cargo.toml 40 - ``` 41 - 42 - My top-level package is `advent`, this will be the main workspace. The `util` package will be a library that I can use across all years. Each year will be its own package, with its own `Cargo.toml` file. This will allow me to have a separate `main.rs` for each year, and also allow me to use the `util` package as a dependency. 43 - 44 - ### Core Package 45 - 46 - This package will contain everything needed to work with days and parts. This should export a `Year` and `Day` trait that all years and days will implement. It will handle parsing arguments, getting the input from stdin, and timing the solutions. This package will be used by the year packages. 47 - 48 - ### Macros Package 49 - 50 - This package will contain any macros I need. It's a proc macro crate. 51 - 52 - ### Year Packages 53 - 54 - Each year will be a binary that I can run with `cargo run -p y_[YEAR] [DAY]:[PART]`. For example, to run the solution for day 1 part 2 of 2020 I would run `cargo run -p y_2022 1:2`. In addition, the binary can take a wild card to mean "run all days". For example, `cargo run -p y_2022 *` would run all days of 2020. 55 - 56 - The binary will take input through stdin. 57 - 58 - ### Main Package 59 - 60 - The main package will contain a binary that acts as a runner for all days. It will take a year, day, and part as arguments and run the corresponding binary. This package includes all the day packages as dependencies and simply acts as a runner for them. 61 - 62 - ### Utils Package 63 - 64 - The utils package is pretty self-explanatory. It will contain a library that I can use over the years. This will only include things used in _solving the problems_ not running the days. Ideally, this will require no dependencies and would be easily transferred to other projects. 65 - 66 - ## The `Day` trait 67 - 68 - The `Day` trait will be the main trait that all days will implement. It will have a few methods that will be used by the `Year` trait to run the days. Here's what it looks like: 69 - 70 - ```rs 71 - pub trait Day { 72 - 73 - type Input; 74 - 75 - const EXAMPLE_INPUT_1: &'static str = ""; 76 - const EXAMPLE_INPUT_2: &'static str = ""; 77 - 78 - const EXPECTED_1: &'static str = ""; 79 - const EXPECTED_2: &'static str = ""; 80 - 81 - fn get_example_input(part: usize) -> &'static str { 82 - match part { 83 - 1 => Self::EXAMPLE_INPUT_1, 84 - 2 => Self::EXAMPLE_INPUT_2, 85 - _ => panic!("Invalid part number"), 86 - } 87 - } 88 - 89 - fn run_part(part: usize, input: Option<&str>) -> Option<String> { 90 - let input = input.unwrap_or_else(|| Self::get_example_input(part)); 91 - let input = Self::parse_input(input); 92 - match part { 93 - 1 => Self::part_1(input), 94 - 2 => Self::part_2(input), 95 - _ => panic!("Invalid part number"), 96 - } 97 - } 98 - 99 - // ... 100 - 101 - fn parse_input(input: &str) -> Self::Input; 102 - 103 - fn part_1(_input: Self::Input) -> Option<String> { None } 104 - fn part_2(_input: Self::Input) -> Option<String> { None } 105 - 106 - } 107 - ``` 108 - 109 - The general idea here is we have a few constants that contain the example input for each part. Then we have a few methods that will be used to run the days. The `get_example_input` method will be used to get the example input for a given part. The `run_part` method will be used to run a given part. The `parse_input` method will be used to parse the input into the type used by the day. Finally, the `part_1` and `part_2` methods will be used to run the parts. 110 - 111 - Here we expect `part_1` and `part_2` to be implemented. However, they return `None` by default. This is so we can generate all 25 days of a year without having to implement all of them. If a day is not implemented, the runner will simply print a message saying that the day is not implemented. 112 - 113 - Sadly `parse_input` will always have to be implemented, as there's no way to implement it by default. `I` here is a `String` by default, and since `parse_input` returns `I` one might think that for a default implementation, we could just return the input as-is. However, rust won't allow this as in the event `I` is changed to another type, the default implementation would no longer work. So we have to implement `parse_input` for each day. Implementing it isn't too bad, and could be part of a derive macro later on. 114 - 115 - ## The `Year` trait 116 - 117 - The `Year` trait will be the main trait that all years will implement. It will have a few methods that will be used by the `core` package to run the days. Here's what it looks like: 118 - 119 - ```rs 120 - pub trait Year { 121 - const YEAR: usize; 122 - 123 - fn solve_day(day: usize, part: usize, input: Option<&str>) -> Option<String>; 124 - 125 - fn solve_day_both_parts(day: usize, extra_indent: &str); 126 - 127 - fn solve_all_days() { 128 - println!("Year {}:", Self::YEAR); 129 - for day in 1..=MAX_DAY { 130 - Self::solve_day_both_parts(day, " "); 131 - } 132 - } 133 - 134 - fn run_dp(input: Option<&str>, dp: DP) { 135 - match dp.day { 136 - Selection::All => { 137 - Self::solve_all_days(); 138 - }, 139 - Selection::Single(day) => { 140 - match dp.part { 141 - Selection::All => { 142 - Self::solve_day_both_parts(day, ""); 143 - }, 144 - Selection::Single(part) => { 145 - Self::solve_day(day, part, input); 146 - }, 147 - } 148 - }, 149 - } 150 - } 151 - } 152 - ``` 153 - 154 - The general idea here is we have a few methods that will be used to run the days. The `solve_day` method will be used to run a given day. The `solve_day_both_parts` method will be used to run both parts of a given day. Finally, the `solve_all_days` method will be used to run all days. The extra indent is used to indent the output of the day so that it lines up with the year. 155 - 156 - ## Trying to implement a year 157 - 158 - Now that we have a basic skeleton of what a year should look like, let's try to implement it! I'm going to be using the most recent year, 2023, as my testing grounds. 159 - 160 - The layout of each year's package will look like this: 161 - 162 - ```dir 163 - 20XX/ 164 - ├─ src/ 165 - │ ├─ main.rs 166 - │ ├─ lib.rs 167 - │ ├─ day_X.rs 168 - ├─ Cargo.toml 169 - ``` 170 - 171 - The `main.rs` file will be used to run the days. The `lib.rs` file will be used to implement the `Year` trait. The `day_X.rs` files will be used to implement the `Day` trait for each day. 172 - 173 - Starting out I'm going to manually implement one day of 2023 and get my traits implemented. Then, I'm going to see where I can use macros, derive macros, and other things to make the process easier for subsequent days/years. 174 - 175 - ### Day 1 176 - 177 - ```rs 178 - use core::Day; 179 - 180 - pub struct Day1; 181 - 182 - // Ideally most of this could be handled by a proc macro of some kind 183 - impl Day for Day1 { 184 - 185 - type Input = String; 186 - 187 - const EXAMPLE_INPUT_1: &'static str = "..."; 188 - // Defining examples and such... 189 - 190 - fn parse_input(input: &str) -> Self::Input { 191 - input.to_string() 192 - } 193 - 194 - fn part_1(input: Self::Input) -> Option<String> { 195 - //... 196 - Some(answer.to_string()) 197 - } 198 - 199 - fn part_2(input: Self::Input) -> Option<String> { 200 - //... 201 - Some(answer.to_string()) 202 - } 203 - } 204 - ``` 205 - 206 - I still don't know how I entirely feel about making my Days return `String`s. It's fine for now and will come in handy if for some reason a problem needs a string as an answer. However, I think I might change it to return an `i64` instead. I'll have to see how it goes. 207 - 208 - Now that I have a day implemented, I want to try and simplify defining it via a macro. I'm going to try and make a macro that will generate the `Day` trait implementation (or most of it) for me. 209 - 210 - ```rs 211 - #[macro_export] 212 - macro_rules! ex_for_day { 213 - ($day:literal, $part:literal) => { 214 - include_str!(concat!("examples/day_", stringify!($day), "/", stringify!($part), ".txt")) 215 - }; 216 - } 217 - 218 - #[macro_export] 219 - macro_rules! day_stuff { 220 - ($day:literal, $e_1:literal, $e_2:literal) => { 221 - day_stuff!($day, $e_1, $e_2, String); 222 - 223 - fn parse_input(input: &str) -> Self::Input { 224 - input.to_string() 225 - } 226 - }; 227 - 228 - ($day:literal, $e_1:literal, $e_2:literal, $i: ty) => { 229 - type Input = $i; 230 - 231 - const DAY: usize = $day; 232 - const EXAMPLE_INPUT_1: &'static str = ex_for_day!($day, 1); 233 - const EXAMPLE_INPUT_2: &'static str = ex_for_day!($day, 2); 234 - const EXPECTED_1: &'static str = $e_1; 235 - const EXPECTED_2: &'static str = $e_2; 236 - } 237 - } 238 - ``` 239 - 240 - Here we can see I have two macros. The first one, `ex_for_day`, is used to get the example input for a given day and part. The second one, `day_stuff`, is used to generate part of the `Day` trait implementation. It takes the day number, the expected answers, and the type of the input. It then generates the `Input` type, the example inputs, and the expected answers. Finally, it generates the `parse_input` method. 241 - 242 - If no input type is given, it defaults to `String`. This is because I want to be able to use this macro for all days, and I don't want to have to specify the input type for each day. 243 - 244 - In theory, I could use proc-macros here to generate more of the file. However, proc macros tend to mess with debugging output / hide what scopes things are defined in. I want to be able to debug my code, so I'm going to stick with these macros for now. 245 - 246 - Now, I need to be able to generate a `Year` trait implementation. This will be a bit more complicated than the `Day` trait implementation, as I need to generate a `match` statement for each day. I'm going to try and make a macro that will generate the `Year` trait implementation for me. 247 - 248 - ### Year 2023 249 - 250 - After getting the `Year` trait implemented for 2023, here's what it looks like: 251 - 252 - ```rs 253 - mod day_1; 254 - 255 - use core::{Day, Year}; 256 - 257 - use day_1::Day1; 258 - 259 - pub struct Year2023; 260 - 261 - impl Year for Year2023 { 262 - 263 - const YEAR: usize = 2023; 264 - 265 - fn solve_day(day: usize, part: usize, input: Option<&str>) -> Option<String> { 266 - match day { 267 - 1 => Day1::run_part(part, input), 268 - _ => None, 269 - } 270 - } 271 - 272 - fn solve_day_both_parts(day: usize, extra_indent: &str) { 273 - match day { 274 - 1 => Day1::run_all_parts(extra_indent), 275 - _ => (), 276 - } 277 - } 278 - 279 - } 280 - ``` 281 - 282 - This looks like it can be placed in a proc macro as all we're _really changing_ here is the year number, the rest should be carbon copy for all years. So I created a simple proc macro that basically has the `pub struct` and `impl` blocks as a template, and will replace the year number with the proper year: 283 - 284 - ```rs 285 - extern crate proc_macro; 286 - 287 - use proc_macro::TokenStream; 288 - 289 - const YEAR_TEMPLATE: &str = include_str!("template_year.rs"); 290 - 291 - #[proc_macro] 292 - pub fn year(item: TokenStream) -> TokenStream { 293 - let year = item.to_string(); 294 - 295 - YEAR_TEMPLATE.replace("__YEAR__", &year).parse().unwrap() 296 - } 297 - ``` 298 - 299 - After expanding this to include mod statements, use statements, and tests, I get the ability to simply do: 300 - 301 - ```rs 302 - use macros::year; 303 - 304 - year!(2023); 305 - ``` 306 - 307 - to automatically make a runner and tester for all the days in 2023. 308 - 309 - #### Adding the Year Runner 310 - 311 - Up until now, I haven't shown _how_ running problems is going to work. The year binaries will parse the arguments into a struct called `DP` (day, part). This struct will then be used to find the correct day and part to run. Here's what it looks like: 312 - 313 - ```rs 314 - #[derive(Clone, Debug)] 315 - pub enum Selection { 316 - All, 317 - Single(usize), // TODO: Add range maybe? 318 - } 319 - 320 - pub struct DP { 321 - pub day: Selection, 322 - pub part: Selection, 323 - } 324 - ``` 325 - 326 - The parsing for this is pretty simple, split by `:`, then parse each part. If the part is `*` then it's `Selection::All`, otherwise it's `Selection::Single`. 327 - 328 - Now I can simply pass this to the `Year` trait's `run_dp` method and it will run the correct day and part. 329 - 330 - For the final bit, we need a way to get the `input` we want to run. To do this, I accept the input as a second argument to the binary. If no input is given, we'll use the example input. If the user passes `-`, we will read from stdin. Otherwise, we will simply use the input given. 331 - 332 - Combining parsing the DP and the input we get this handy utility method: 333 - 334 - ```rs 335 - pub fn get_dp_and_input() -> (DP, Option<String>) { 336 - let mut args = args().skip(1); 337 - 338 - let dp = args.next().map(|s| DP::parse(&s.trim())).unwrap_or(DP_ALL); 339 - 340 - let input = args.next().map(|s| s.trim().to_string()).map(|i| { 341 - if i == "-" { 342 - let mut input = String::new(); 343 - stdin().read_to_string(&mut input).expect("Failed to read input"); 344 - input.trim().to_string() 345 - } else { 346 - i 347 - } 348 - }); 349 - 350 - (dp, input) 351 - } 352 - ``` 353 - 354 - All we need in `main.rs` for our years now is some glue code: 355 - 356 - ```rs 357 - use core::{Year, get_dp_and_input}; 358 - 359 - use y_2023::Year2023; 360 - 361 - fn main() { 362 - let (dp, input) = get_dp_and_input(); 363 - Year2023::run_dp(input.as_deref(), dp); 364 - } 365 - ``` 366 - 367 - We can run our year with `cargo run -p y_2023 1:1` and it will run day 1 part 1 of 2023. We can also run `cargo run -p y_2023 1:*` to run both parts of day 1, or `cargo run -p y_2023 *` to run all days of 2023. 368 - 369 - It's a bit overkill, but I made a proc macro to generate this glue code for me: 370 - 371 - ```rs 372 - #[proc_macro] 373 - pub fn year_runner(item: TokenStream) -> TokenStream { 374 - let year = item.to_string(); 375 - 376 - format!(" 377 - use core::{{Year, get_dp_and_input}}; 378 - 379 - use y_{year}::Year{year}; 380 - 381 - fn main() {{ 382 - let (dp, input) = get_dp_and_input(); 383 - Year{year}::run_dp(input.as_deref(), dp); 384 - }}").parse::<TokenStream>().unwrap() 385 - } 386 - ``` 387 - 388 - ### Generating Days / Years 389 - 390 - Now that I've got a basic skeleton for a day and year, I want to be able to generate them. I'm going to make a function in my `core` package that will let me generate a day to a given file given the day's number. Then I'm gonna write a function that can generate an entire year (and its rust project) given the year's number. 391 - 392 - So to generate an entire year crate I need to: 393 - 394 - 1. Create a directory for the year in `years/` 395 - 2. Initialize a `Cargo.toml` file 396 - 1. Name it `y_[YEAR]` 397 - 2. Add the `core` package as a dependency 398 - 3. Add the `util` package as a dependency 399 - 4. Add the `macros` package as a dependency 400 - 3. Make a `src/` folder 401 - 1. Make example files for every day in the year in `examples/day_[DAY]/[PART].txt` 402 - 2. Make a `day_[DAY].rs` file for each day, with a definition for the Day and a call to `day_stuff!` 403 - 3. Make a `lib.rs` file that has a single macro call to `year!([YEAR]);` 404 - 4. Make a `main.rs` file that has a single macro call to `year_runner!([YEAR]);` 405 - 4. Add the year as a dependency to the `Cargo.toml` file in the root of the project 406 - 407 - The logic for this is pretty rudimentary so I'm going to not include it here. Basically, the main binary will take a year number and generate the year crate for that year. It will also add the year crate as a dependency to the root `Cargo.toml` file. 408 - 409 - ### The Main Binary 410 - 411 - The main binary is used for two things: 412 - 413 - 1. It's the thing that you run to actually generate years 414 - 2. It can run any year, day, and part. It does this by compiling all years into itself 415 - 416 - The first step is making a simple CLI parser. I want it to be able to take 2 commands `new` and `solve`. `new` is used to generate a new year, and `solve` is used to run a year, day, and part. Here's what the CLI parser looks like: 417 - 418 - ```rs 419 - let args = std::env::args().skip(1).collect::<Vec<_>>(); 420 - 421 - let command = args.get(0); 422 - 423 - match command { 424 - Some(command) => { 425 - match command.as_str() { 426 - "new" => { 427 - let year = args.get(1).expect("No year provided"); 428 - make_year(year); 429 - }, 430 - "solve" | "run" => { 431 - let (ydp, input) = get_ydp_and_input(args[1..].to_vec()); 432 - run_ydp(ydp, input); 433 - } 434 - _ => { 435 - println!("Unknown command: {}", command); 436 - println!("Available commands: new, solve"); 437 - } 438 - } 439 - }, 440 - None => { 441 - println!("No command provided"); 442 - println!("Available commands: new, solve"); 443 - } 444 - } 445 - ``` 446 - 447 - After that, we need to connect `new` to `core::make_year` and then `solve` to a match statement that will run the correct year, day, and part. 448 - 449 - To generate this match statement (and use statements) I once again turn to a proc macro. These macros are pretty much the same for days, they have a match that takes a year number and will run the corresponding year. 450 - 451 - ### Testing 452 - 453 - Now that we have a project generation command we're done! This allows us to generate a new year crate, auto-updating the main binary to include it as a dependency. The crate will have all 25 days generated, with the `Day` trait implemented for each day. The `Year` trait will also be implemented for the year. Now we can run the year's binary through `cargo run -p y_[YEAR] [DAY]:[PART]` and it will run the correct day and part. Or we can run `cargo run solve [YEAR]:[DAY]:[PART]` to get the same result. 454 - 455 - I'll start by generating all 25 days for 2023. Up until now, I've only been generating one day per year for simplicity. 456 - 457 - So now my y_2023 crate has a `day_x.rs` file for every day until the 25th. I can now run `cargo run solve 2023:*` to run every single day of the year. Although they're not implemented yet, it will just print that the day is not implemented. 458 - 459 - ### Conclusion 460 - 461 - So now I have an automated repo for solving Advent of Code. I can generate a new year, and it will generate all 25 days for that year. I then implement the days and run them through the main binary. I'm going to hold off on transferring my 2023 solutions to this repo until it's over. After which I want to take a look at all of the days in 2023 and see what I can extract into the `utils` crate and `macros` crate. 462 - 463 - And with that, I'm done with this post! Stay tuned for a "Prologue part 2" / "2023" post where I go over some of the highlights of 2023 for me and describe what logic I'm extracting into the `utils` crate and `macros` crate.
+353
src/content/posts/wip_screen_captures.md
··· 1 + --- 2 + title: Work In Progress Friday - Screen Captures 3 + date: 2024-07-25 4 + summary: An adventure in making scripts to capture screen shots and recordings 5 + cowsay: A picture is worth a thousand words 6 + --- 7 + 8 + I've recently been going down the path of madness known as customizing my desktop and 9 + I figured I'd share some neat scripts and setups I've done along the way. 10 + 11 + Today I'll go into some scripts I've been working on to capture screen shots and recordings. 12 + They allow selecting regions of the screen and specific windows, and I also made it so you 13 + can edit them afterwards. 14 + 15 + ## Background 16 + 17 + My 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 19 + can be found, I'll reference them throughout this post and provide links to the current version of each so you 20 + can see if I've updated them since this post. 21 + 22 + Currently I use [Hyprland](https://hyprland.org) as my window manager, and have been duct-taping components together to make my own little Desktop Environment around it. 23 + 24 + I also like to use [NuShell](https://nushell.sh) as my shell, and these scripts 25 + will be written in it. If you haven't checked out NuShell yet, I highly recommend it! 26 + 27 + ## Screenshots 28 + 29 + First is the script to take screenshots. This is a relatively simple script as it simply builds 30 + on top of [grimblast](https://github.com/hyprwm/contrib/tree/main/grimblast) with some nice QoL 31 + features. 32 + 33 + To install grimblast, all I have to do is add it to my `environment.systemPackages`: 34 + 35 + ```nix 36 + environment.systemPackages = with pkgs; [ 37 + # ... 38 + grimblast 39 + libnotify # For notifications 40 + xdg-utils # For opening files 41 + # ... 42 + ]; 43 + ``` 44 + 45 + Grimblast will automatically save screenshots to `XDG_SCREENSHOTS_DIR`, I 46 + manually set this in my _home manager_ config with: 47 + 48 + ```nix 49 + xdg.userDirs.extraConfig.XDG_SCREENSHOTS_DIR = "${config.home.homeDirectory}/Pictures/Screenshots"; 50 + ``` 51 + 52 + Grimblast will name the screenshots with the current date and time, which works for me. 53 + 54 + Now along to actually using grimblast, I'll create a new script and put in in my config somewhere, we'll 55 + call it `screenshot.nu`. I usually like to place any non-nix files in a folder called `res` at the root 56 + of my config, we'll get to actually calling this script once we're done writing it. 57 + 58 + To start out we need to call grimblast, I like to use `copysave` as the action as I like having it 59 + immediately in my clipboard, and having it saved for later. I'll also add `--freeze` which simply 60 + freezes the screen while I select the region to capture. 61 + 62 + ```nushell 63 + let file_path = grimblast --freeze copysave 64 + ``` 65 + 66 + grimblast will then return the path to the saved screenshot, which we save in `file_path`. 67 + If the user were to cancel the selection (press escape), `file_path` would be empty, so we want to make sure 68 + to check for that so we're not trying to open a non-existent file. 69 + 70 + ```nushell 71 + if $file_path == "" { 72 + exit 1 73 + } 74 + ``` 75 + 76 + Now the main part, we'll send a notification that the screenshot was saved, and have options for it. 77 + 78 + I want four actions for the screenshot: 79 + 80 + - Open 81 + - Open Folder 82 + - Edit 83 + - Delete 84 + 85 + Also since grimblast saves the screenshot as a png, I can pass it as the icon of the notification. 86 + 87 + ```nushell 88 + let 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" 89 + ``` 90 + 91 + A long command here, `notify-send` allows us to send a notification to the currently running notification daemon. 92 + In my case I'm using [swaync](https://github.com/ErikReider/SwayNotificationCenter). 93 + 94 + - `--app-name` is the name of the application that sent the notification, I say screengrab here so swaync will show an icon in addition to the image, also so I can play a camera shutter sound when the notification is sent. 95 + - `-i` is the icon to display in the notification, in this case the screenshot we just took. 96 + - `-t` is the time in milliseconds to show the notification 97 + - `--action` is actions to display in the notification, `name=Text` 98 + - First position argument is the notification title, and second is the body. 99 + 100 + With that we get a neat notification when we screenshot. 101 + 102 + ![A notification that I just took a screenshot with the screenshot visible](@assets/blog/wip1_screenshot_notif.png) 103 + 104 + Now we need to handle each action, the chosen action is returned by notify-send, so we can match on that. 105 + 106 + - "Open" and "Open Folder" are pretty simple, just pass `$file_path` and `$file_path | path dirname` to `xdg-open` 107 + - "Edit" I'll simply pass the file path to my editor, I chose [swappy](https://github.com/jtheoof/swappy) because of it's simplicity and ease of use. 108 + - "Delete" I'll just remove the file. 109 + 110 + ```nushell 111 + match $choice { 112 + "open" => { 113 + xdg-open $file_path 114 + } 115 + "folder" => { 116 + xdg-open ($file_path | path dirname) 117 + } 118 + "edit" => { 119 + swappy -f $file_path 120 + } 121 + "delete" => { 122 + rm $file_path 123 + } 124 + } 125 + ``` 126 + 127 + And that's it! I now have a fairly robust screenshot script. 128 + 129 + ### Calling the Screenshot script 130 + 131 + Now in terms of actually calling it I'll be binding it to `Win` + `Shift` + `S` in Hyprland, as 132 + well as `PrintScreen`. 133 + 134 + In home manager i simply have to add these strings to my `wayland.windowManager.hyprland.settings.bind` 135 + array: 136 + 137 + ```nix 138 + wayland.windowManager.hyprland.settings.bind = [ 139 + # ... 140 + ",Print,exec,nu ${../res/screenshot.nu}" 141 + "SUPER SHIFT,S,exec,nu ${../res/screenshot.nu}" 142 + # ... 143 + ]; 144 + ``` 145 + 146 + Now by switching to my new config (and making sure to stage `screenshot.nu` of course), 147 + I can take screenshots with a keybind! 148 + 149 + ## Screen Recordings 150 + 151 + This will be a bit more involved mainly because something like grimblast doesn't exist for screen recordings. 152 + Looking at existing solutions I couldn't find any that I really liked, mostly because they involved 153 + some additional UI. To be clear this script will be for _simple_, _short_ recordings, long-term stuff 154 + I'll still prefer to use something like OBS. 155 + 156 + For the actual screen recording I'll be using [wf-recorder](https://github.com/ammen99/wf-recorder). 157 + 158 + ```nix 159 + environment.systemPackages = with pkgs; [ 160 + # ... 161 + wf-recorder 162 + libnotify # For notifications 163 + xdg-utils # For opening files 164 + slurp # Will explain this later 165 + # ... 166 + ]; 167 + ``` 168 + 169 + First and foremost location, I chose to use `~/Videos/Captures` for my recordings. I didn't 170 + set an environment variable for this, it'll be hardcoded in the script. 171 + 172 + ```nushell 173 + let date_format = "%Y-%m-%d_%H-%M-%S" 174 + 175 + let captures_folder = $"($env.HOME)/Videos/Captures" 176 + 177 + if not ($captures_folder | path exists) { 178 + mkdir $captures_folder 179 + } 180 + 181 + let out_name = date now | format date $"($captures_folder)/($date_format).mp4" 182 + ``` 183 + 184 + This will handle determining the folder and name for the recordings, 185 + and creating the folder if it doesn't exist. 186 + 187 + Next up I want to have a similar selection process to the screenshot script, to do this I'll use 188 + [slurp](https://github.com/emersion/slurp) to select areas of the screen, 189 + which is what grimblast uses under the hood. 190 + 191 + In addition, grimblast does some communication with Hyprland to get window information such as 192 + position and size, this lets you select a window to take a screenshot of. I'll be getting that info manually from 193 + Hyprland using NuShell instead: 194 + 195 + ```nushell 196 + let workspaces = hyprctl monitors -j | from json | get activeWorkspace.id 197 + let windows = hyprctl clients -j | from json | where workspace.id in $workspaces 198 + let geom = $windows | each { |w| $"($w.at.0),($w.at.1) ($w.size.0)x($w.size.1)" } | str join "\n" 199 + ``` 200 + 201 + This gets all the geometry in a format `slurp` will be able to parse and use. 202 + 203 + ```nushell 204 + let stat = do { echo $geom | slurp -d } | complete 205 + 206 + if $stat.exit_code == 1 { 207 + echo "No selection made" 208 + exit 209 + } 210 + ``` 211 + 212 + I do `complete` here to get the exit code of the slurp command, if it's 1 then the user cancelled the selection 213 + and similar to the screenshot script I'll exit. 214 + 215 + Now it's time to actually record, the stdout of `slurp` contains the geometry that we want to capture, 216 + so we'll pass that to `wf-recorder` with the `-g` flag: 217 + 218 + ```nushell 219 + wf-recorder -g ($stat.stdout) -F fps=30 -f $out_name 220 + ``` 221 + 222 + Pretty simple command, `-g` is the geometry to record, `-F` is the format options, and `-f` is the output file. 223 + 224 + Now we'll run into an issue if we run this script and start recording, there's no way to stop it! 225 + I'll cover how we're going to get around that when it comes to setting up keybinds. 226 + 227 + Assuming `wf-recorder` stops, we'll send a notification to the user that the recording is done: 228 + 229 + ```nushell 230 + let 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)" 231 + ``` 232 + 233 + ![A notification that I just took a screen recording with a video camera icon visible](@assets/blog/wip1_screenrec_notif.png) 234 + 235 + Most arguments are the same here as the screenshot script, the only difference is the icon and app name. 236 + The actions are also basically the same, so I'll leave out the explanation and just show the handler: 237 + 238 + ```nushell 239 + match $action { 240 + "open" => { 241 + xdg-open $out_name 242 + } 243 + "folder" => { 244 + xdg-open $captures_folder 245 + } 246 + "delete" => { 247 + rm $out_name 248 + } 249 + } 250 + ``` 251 + 252 + ### Calling the Recording script 253 + 254 + Now to actually call the script, I'll bind it to `Win` + `Shift` + `R` in Hyprland. 255 + 256 + However, we're going to do something special with the `exec` line here. Instead of simply calling the script 257 + we're going to check if `wf-recorder` is already running, if this is the case we can send 258 + `SIGINT` to it to make it stop recording, meaning our script will continue and show the notification. 259 + 260 + ```nix 261 + wayland.windowManager.hyprland.settings.bindr = [ 262 + # ... 263 + "SUPER SHIFT,R,exec,pkill wf-recorder --signal SIGINT || nu ${../res/screenrec.nu}" 264 + # ... 265 + ]; 266 + ``` 267 + 268 + `pkill` here will exit with code `1` if it doesn't find any processes to kill, so the `||` will run our script if `pkill` fails. 269 + 270 + Note that I did this on `bindr`, this means the keybind will only happen once the R key is _released_ rather than 271 + pressed. This is to prevent a weird issue I ran into where the recording would stop immediately after starting. 272 + 273 + And that's it! We can now screen record with ease. It won't record audio 274 + (might do an additional keybind in the future) and it also doesn't copy the recording to the clipboard, but 275 + it works pretty well for short videos. 276 + 277 + ## Full Scripts 278 + 279 + ### Screenshot Script 280 + 281 + ```nushell 282 + #!/usr/bin/env nu 283 + 284 + let file_path = grimblast --freeze copysave area 285 + 286 + if $file_path == "" { 287 + exit 1; 288 + } 289 + 290 + let 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" 291 + 292 + match $choice { 293 + "open" => { 294 + xdg-open $file_path 295 + } 296 + "folder" => { 297 + xdg-open ($file_path | path dirname) 298 + } 299 + "edit" => { 300 + swappy -f $file_path 301 + } 302 + "delete" => { 303 + rm $file_path 304 + } 305 + } 306 + ``` 307 + 308 + [Most recent version on GitHub](https://github.com/Bwc9876/nix-conf/blob/main/res/screenshot.nu) 309 + 310 + ### Recording Script 311 + 312 + ```nushell 313 + #!/usr/bin/env nu 314 + 315 + let date_format = "%Y-%m-%d_%H-%M-%S" 316 + 317 + let captures_folder = $"($env.HOME)/Videos/Captures" 318 + 319 + if not ($captures_folder | path exists) { 320 + mkdir $captures_folder 321 + } 322 + 323 + let out_name = date now | format date $"($captures_folder)/($date_format).mp4" 324 + 325 + let workspaces = hyprctl monitors -j | from json | get activeWorkspace.id 326 + let windows = hyprctl clients -j | from json | where workspace.id in $workspaces 327 + let geom = $windows | each { |w| $"($w.at.0),($w.at.1) ($w.size.0)x($w.size.1)" } | str join "\n" 328 + 329 + let stat = do { echo $geom | slurp -d } | complete 330 + 331 + if $stat.exit_code == 1 { 332 + echo "No selection made" 333 + exit 334 + } 335 + 336 + wf-recorder -g ($stat.stdout) -F fps=30 -f $out_name 337 + 338 + let 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)" 339 + 340 + match $action { 341 + "open" => { 342 + xdg-open $out_name 343 + } 344 + "folder" => { 345 + xdg-open $captures_folder 346 + } 347 + "delete" => { 348 + rm $out_name 349 + } 350 + } 351 + ``` 352 + 353 + [Most recent version on GitHub](https://github.com/Bwc9876/nix-conf/blob/main/res/screenrec.nu)
+9
src/pages/blog/posts/[...slug].astro
··· 45 45 gap: 1rem; 46 46 } 47 47 48 + div.wrapper > * { 49 + min-width: 0; 50 + } 51 + 48 52 @media (min-width: 1200px) { 49 53 div.wrapper { 50 54 flex-direction: row; ··· 71 75 img { 72 76 border: solid 1px var(--text) !important; 73 77 border-radius: 5px; 78 + } 79 + 80 + pre { 81 + overflow-x: auto; 82 + width: 100%; 74 83 } 75 84 </style>