Arjen Wiersma

A blog on Emacs, self-hosting, programming and other nerdy things

This post is just a small note for those of you who also run Microsoft Teams on Linux through their browser and now receive a note “your browser does not meet the requirements for the new Teams”. It turns out that the client is looking at the user-agent string to determine which browsers it accepts, and which not.

So, if you have the message, install an user-agent switcher and select a common browser on a common OS (from the MS perspective) and you will suddenly meet the requirements.

This is a longer form article. I is relevant as of February 18th 2023. If the circumstances of my environment changes I will try to update this article to reflect the situation. You can find the full source code of my dotfiles on Github.

I like consistency and simplicity. I do not like to use many different tools to do different things, I rather spend my time learning to use a few tools very well then to follow the hype on the latest trend of tools for something we have been doing forever.

This philosophy I transfer to pretty much everything in life. I have been using the same laptop bag for ages, I have a small mechanical keyboard, and I run the same version of my OS on all my devices. One device for on the go, the other for at home. They look the same and act the same, courtesy of an Linux distribution called NixOS.

Below you will find 2 screenshots, one from my laptop, the other from my desktop. The only difference is the size of the screen.

{{< figure src=“/ox-hugo/desktop.png” caption=”Figure 1: My Linux desktop on my laptop” >}}

{{< figure src=“/ox-hugo/desktop-large.png” caption=”Figure 2: My Linux desktop on my desktop” >}}

NixOS {#nixos}

I use the NixOS distribution of Linux. NixOS is a wonderful operating system that works by declaring what you want your environment to be and then applying that declaration to the current version of the environment. That sounds difficult, but let me explain.

Suppose you have just installed a Linux distribution and you want to install the wonderful Emacs editor. In most distributions you will go to the package manager, search for Emacs and click on install. A few seconds later, Emacs is installed. With NixOS you edit a file that describes your environment, you will add a line to it saying that Emacs is part of your environment. When you have saved the file you will ask NixOS to create a new version of your environment, to do so it will install Emacs for you.

I say it will create a new version of your environment. This means there is an old version as well, right? Yes! NixOS has a concept of Generations. This means every change happens in its own version of the environment. So, if a change goes wrong, you just revert back to the previous version.

This sounds like a great deal of work, and it is. It is not for the new Linux user, that is for sure. If you spend some time learning NixOS I am sure you will be grateful for it. Just the other day I tried to use the wayland system on Linux, my configuration went horribly wrong and I was left with an unusable system. I rebooted the machine, selected the previous generation, and I was back where I started before the change. It is that useful!

As I share my configuration over multiple machines I split up the configuration into a machine specific version to my desktop, laptop, and the things that should run on both:

The shared configuration contains all the juice, it sets up the graphical user interface, creates users and assigns to groups. This means that when you run this configuration you will end up in a very barren i3 tiling window manager. More on that later.

Most of my applications are courtesy of something called home-manager. This is a user-space application that allows for easy changes to the environment. As none of these changes can actually wreck the environment I kept them outside of the default NixOS configuration.

My home-manager configuration takes care of installing all the user-space tools that I use. It also sets up my shell and configures the Emacs daemon.

You might wonder, do you create a configuration file every time you need a tool? No! When I just need a one-off tool I use something called nix-shell. In the screenshots above you will notice that I run neo-fetch. This program is not part of my normal system as I only use it for screenshots as the one above. Within a terminal I run it as follows: nix-shell -p neofetch --run neofetch. This will temporarily install neo-fetch and run it. Afterwards it can be cleaned up. I also do this for most of the tools, such as unzip. I only install then when I need them. This keeps everything that is installed very clean.

You might also notice that there are not programming language toolchains in my configuration. That is correct. When I have a programming project I use something called direnv, see the direnv webpage for some background.

Whenever I start a new programming project I run the following command in the project root: nix --extra-experimental-features "nix-command flakes" flake new -t github:nix-community/nix-direnv .. This will create a flake.nix file in which I can declare what this project needs as dependencies. As the rest of my environment is extremely clean, I will need to specify precisely what is needed. Take the listing below, it is part of a programming project in which I use Rust, Golang, Python and Java. Whenever I move into this project, all the tools will be installed. This also means that it works exactly the same on every single system where I use this setup.

{
  description = "A basic flake with a shell";
  inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
  inputs.flake-utils.url = "github:numtide/flake-utils";

  outputs = { self, nixpkgs, flake-utils }:
    flake-utils.lib.eachDefaultSystem (system: let
      pkgs = nixpkgs.legacyPackages.${system};
    in {
      devShells.default = pkgs.mkShell {
        packages = with pkgs; [
          pkg-config
          openssl.dev
          cargo
          rustc
          rustfmt
          clippy
          rust-analyzer
          aoc-cli
          go
          gopls
          gotools
          govulncheck
          pkgs.jdk
          pkgs.jdt-language-server
          pkgs.python311
        ];
        # Environment variable specifying the plugin directory of
        # the language server 'jdtls'.
        JDTLS_PATH = "${pkgs.jdt-language-server}/share/java";
      };
    });
}
Code Snippet 1: A nix-direnv declaration for a polyglot programming project

This might seem like a hassle. It is true, it is more work then just installing Golang on Ubuntu and “just having it”. But once you use multiple systems or work together in groups you will start appreciating it, trust me.

i3 {#i3}

As I like simplicity I tend to not use elaborate windowing environments, such as Gnome or KDE. I try them out every once in a while, but I also go back to i3. Back in the day I ran enlightenment, but now I have been using i3 WM for quite some years. My configuration is quite mature and I generally only change it when I want to add a new tool to my daily use, or when tools get good updates such as polybar. The configuration is part of my dotfiles.

When I boot my system all I have is a top bar that contains the following information:

  • 💻 Active workspaces (each has its own icon and use)
  • 💾 Current fill state of my disks
  • 🛡️ VPN status
  • 🔊 Sound and its volume percentage
  • 🛜 Wifi state (laptop only)
  • 🔋 Battery state (laptop only)
  • ⏰ Time
  • 📥 Tray icons (flameshot, bluetooth and nextcloud)

That is it. After all those years working with computers, that is all I really need. If I could I would write a toggle for the bar as well, to only show up when needed. The very appealing thing about i3 is its tiling feature. I will never have windows that overlap. Everything is neatly ordered in workspaces and within workspaces in columns or rows. As I create dedicated workspaces everything has a specific place:

  1. Terminal (alacritty with tmux)
  2. Emacs
  3. Virtual Machines
  4. Firefox
  5. Chrome

From workspace 6 on I consider them “throw-away” workspaces. The things I will store there will be used only shortly. The exception is workspace 10 (or 0). This contains my Spotify.

To launch applications I use something called Rofi. It is a window switcher, application launcher and menu replacement tool. It is very easy to customize and you can make it exactly what you want. My configuration is available on github.

{{< figure src=“/ox-hugo/rofi.png” caption=”Figure 3: Rofi launching applications in i3” >}}

You can configure your environment exactly as you want. Take a look at r/unixporn for some more extreme versions of customized desktops.

#emacs #development #writing

It has been a little while. I have been swamped with work and the work on my thesis, leaving no room to finish the Advent of Code or much of anything else.

Yesterday I gave my practice presentation for my thesis. This means I am one more step closer to the finish line. During the day there were many interactions with fellow students. One of the topics has been the templates to use at Open Universiteit. So, I thought I would just create a repository of the templates that I use, so that anyone can learn from them.

The repository is here: https://github.com/credmp/ou-templates

The weekend generally is a place to find hard puzzles again, this time not so much. A simple quest to find the next number in a sequence with a fully written out algorithm to follow. They key here is to use recursion.

package main

import (
	"fmt"
	"time"

	"arjenwiersma.nl/aoc/internal/aoc"
)

func NextStep(in []int) int {
	allZero := true

	for _, v := range in {
		if v != 0 {
			allZero = false
		}
	}

	if allZero {
		return 0
	}

	var diffs []int
	for i := 1; i < len(in); i++ {
		diffs = append(diffs, in[i]-in[i-1])
	}

	p := NextStep(diffs)
	return in[len(in)-1] + p
}

func main() {
	content := aoc.AsLines("2023/Day09/input.txt")

	var lines [][]int
	for _, v := range content {
		lines = append(lines, aoc.AsNumbers(v))
	}

	startTime := time.Now()

	var res []int
	for _, v := range lines {
		res = append(res, NextStep(v))
	}

	r := aoc.SumArray(res)
	endTime := time.Now()
	elapsed := endTime.Sub(startTime)
	fmt.Printf("Part 1: %d (%v)\n", r, elapsed)

	startTime = time.Now()

	for _, v := range lines {
		aoc.Reverse(v)
	}

	res = []int{}
	for _, v := range lines {
		res = append(res, NextStep(v))
	}

	r = aoc.SumArray(res)
	endTime = time.Now()
	elapsed = endTime.Sub(startTime)
	fmt.Printf("Part 2: %d (%v)\n", r, elapsed)
}

Somewhat suspicious of 2 easy days we end up at Day 8. A simple map to follow again, from one key follow the instructions until we hit ZZZ. Part 2 had us do it for several keys at once, with the goal to find the spot where they all converge. This can take forever, erhm, a long time.

So there has to be a math type solution to this problem. It turns out to be a Least Common Multiple problem. It is the smallest positive integer that is divisible by two or more numbers without leaving a remainder. To find the LCM of two or more numbers, you can use a method called prime factorization or a simpler approach involving multiples. We can also use the Greatest Common Divisor (GCD) to find the LCM.

LCM(a, b) = (a * b) / GCD(a, b)

Example: Find the LCM of 12 and 18 using their GCD.

Step 1: Find the GCD of 12 and 18.

  • You can use methods like prime factorization or the Euclidean algorithm to find the GCD.
  • GCD(12, 18) = 6

Step 2: Use the formula to find the LCM.

LCM(12, 18) = (12 * 18) / 6 = 216 / 6 = 36

So, the LCM of 12 and 18 is 36.

package main

import (
	"fmt"
	"strings"
	"time"

	"arjenwiersma.nl/aoc/internal/aoc"
)

func LCM(numbers []int) int {
	result := numbers[0]
	for i := 1; i < len(numbers); i++ {
		result = (result * numbers[i]) / GCD(result, numbers[i])
	}
	return result
}

func GCD(a, b int) int {
	if b == 0 {
		return a
	}
	return GCD(b, a%b)
}

func solve(s string, instr string, m map[string][]string, p2 bool) int {
	steps := 0
	for {
		d := 0
		if string(instr[steps%len(instr)]) == "R" {
			d = 1
		}

		if !p2 && s == "ZZZ" {
			break
		}
		if p2 && s[2] == 'Z' {
			break
		}

		s = m[s][d]

		steps += 1
	}
	return steps
}
func main() {
	content := aoc.AsLines("2023/Day08/input.txt")

	instr := content[0]
	m := make(map[string][]string)

	for _, v := range content[2:] {
		n := strings.Split(v, " = ")
		lr := strings.Split(n[1], ",")

		m[n[0]] = []string{strings.TrimSpace(lr[0][1:]), strings.TrimSpace(lr[1][:len(lr[1])-1])}
	}

	startTime := time.Now()

	steps := solve("AAA", instr, m, false)

	endTime := time.Now()
	elapsed := endTime.Sub(startTime)
	fmt.Printf("Part 1: %d (%v)\n", steps, elapsed)

	startTime = time.Now()

	var solves []int
	for k := range m {
		if k[2] == 'A' {
			solves = append(solves, solve(k, instr, m, true))
		}
	}

	// do stuff
	endTime = time.Now()
	elapsed = endTime.Sub(startTime)
	fmt.Printf("Part 2: %d (%v)\n", LCM(solves), elapsed)
}

Today we learned about CamelCards, a game of poker meant to play on the back of a camel. The most interesting part here was the parsing of the cards and figuring out how to properly rank them. Part 2 turned out to be as easy as tracking Jokers.

package main

import (
	"fmt"
	"sort"
	"strconv"
	"strings"
	"time"

	"arjenwiersma.nl/aoc/internal/aoc"
)

type Card struct {
	bid    int
	hand   []int
	jokers int
}

func (c *Card) strongerThen(o *Card) bool {
	for i, v := range c.hand {
		if v > o.hand[i] {
			return true
		} else if v < o.hand[i] {
			return false
		}
	}
	return false
}

func (c *Card) rank() int {
	freq := make([]int, 15)
	for _, v := range c.hand {
		if v == 1 { // skip counting the joker
			continue
		}
		freq[v]++
	}

	sort.Ints(freq)

	freq[len(freq)-1] += c.jokers
	strength := 2 * freq[len(freq)-1]
	// full house and 2 pair
	if freq[len(freq)-2] == 2 {
		strength += 1
	}
	return strength
}

func NewCard(s string, bid int, p2 bool) *Card {
	c := &Card{}
	c.bid = bid
	c.jokers = 0
	for p := 0; p < len(s); p++ {
		if s[p]-'0' >= 2 && s[p]-'0' <= 9 {
			c.hand = append(c.hand, int(s[p]-'0'))
		} else {
			x := 10
			switch s[p] {
			case 'A':
				x = 14
			case 'K':
				x = 13
			case 'Q':
				x = 12
			case 'J':
				if p2 {
					c.jokers += 1
					x = 1
				} else {
					x = 11
				}
			case 'T':
				x = 10
			}
			c.hand = append(c.hand, x)
		}
	}
	return c
}

func (c *Card) String() string {
	return fmt.Sprintf("%v (%d)", c.hand, c.bid)
}

func main() {
	content := aoc.AsLines("2023/Day07/input.txt")

	var cards []*Card
	for _, v := range content {
		p := strings.Split(v, " ")
		b, _ := strconv.Atoi(p[1])
		c := NewCard(p[0], b, false)
		cards = append(cards, c)
	}

	startTime := time.Now()
	lessFunc := func(i, j int) bool {
		if cards[i].rank() == cards[j].rank() {
			return cards[j].strongerThen(cards[i])
		}
		return cards[i].rank() < cards[j].rank()
	}

	sort.Slice(cards, lessFunc)

	res := 0
	for i, c := range cards {
		res += (i + 1) * c.bid
	}

	endTime := time.Now()
	elapsed := endTime.Sub(startTime)
	if 251216224 != res {
		panic("Wrong answer")
	}
	fmt.Printf("Part 1: %d (%v)\n", res, elapsed) // 251216224

	cards = []*Card{}
	for _, v := range content {
		p := strings.Split(v, " ")
		b, _ := strconv.Atoi(p[1])
		c := NewCard(p[0], b, true)
		cards = append(cards, c)
	}
	startTime = time.Now()

	sort.Slice(cards, lessFunc)

	res = 0
	for i, c := range cards {
		res += (i + 1) * c.bid
	}
	endTime = time.Now()
	elapsed = endTime.Sub(startTime)
	if 250825971 != res {
		panic("Wrong part 2")
	}
	fmt.Printf("Part 2: %d (%v)\n", res, elapsed) // 250825971
}

Day 6 turned out to be the easiest day in the range so far. A simple implementation of the algorithm was more than sufficient.

I later learned that it was a quadratic function. On the subreddit Deatranger999 said:

If you hold down the button for x seconds, then you will beat the distance if the quadratic x^2 – t x + d is at most 0, where t is the total time of the race and d is the distance you'd like to beat. So I just plugged each one into WolframAlpha, found the roots, and then calculated the number of integers between the two roots.

My solution was to bruteforce :)

package main

import (
	"fmt"
	"strconv"
	"strings"

	"arjenwiersma.nl/aoc/internal/aoc"
)

func main() {
	lines := aoc.AsLines("2023/Day06/input.txt")

	var times []int
	for _, t := range strings.Split(lines[0], " ")[1:] {
		s := strings.TrimSpace(t)
		if s == "" {
			continue
		}
		i, _ := strconv.Atoi(s)
		times = append(times, i)
	}
	var distances []int
	for _, t := range strings.Split(lines[1], " ")[1:] {
		s := strings.TrimSpace(t)
		if s == "" {
			continue
		}
		i, _ := strconv.Atoi(s)
		distances = append(distances, i)
	}

	result := make([]int, len(times))

	for i := 0; i < len(times); i++ {
		for t := 0; t < times[i]; t++ {
			d := t * (times[i] - t)
			if d > distances[i] {
				result[i] += 1
			}
		}
	}

	ans := 1
	for _, c := range result {
		ans *= c
	}

	fmt.Println("Part 1: ", ans)

	nT, _ := strconv.Atoi(fmt.Sprintf("%d%d%d%d", times[0], times[1], times[2], times[3]))
	nD, _ := strconv.Atoi(fmt.Sprintf("%d%d%d%d", distances[0], distances[1], distances[2], distances[3]))

	ans = 0
	for t := 0; t < nT; t++ {
		d := t * (nT - t)
		if d > nD {
			ans += 1
		}
	}
	fmt.Println("Part 2: ", ans)

}

Today was an interesting problem. We are basically given a map to follow based on a number, possibly transforming the number at each step. With a single number this is quite simple, just apply the rules and step through each set of transformations. The problem becomes tricky when it turns out we have to deal with enormous ranges of numbers. On the subreddit some people reported their implementation to take hours and use 20GB of memory.

Luckily there is always a fast solution. In this case it was using ranges of numbers to go through the transformations, so just taking the first number and then creating a (new) range out of the transformation instead of each individual number.

package main

import (
	"fmt"
	"log"
	"math"
	"os"
	"strconv"
	"strings"
	"time"

	"arjenwiersma.nl/aoc/internal/aoc"
)

type Range struct {
	d, s, r int
}

func (r *Range) transform(i int) int {
	if i >= r.s && i <= r.s+r.r-1 {
		delta := i - r.s
		return r.d + delta
	}

	return i
}

type Segment struct {
	from, to int
}

func main() {
	content, _ := os.ReadFile("2023/Day05/input.txt")
	segments := strings.Split(strings.TrimSpace(string(content)), "\n\n")

	seedStr := strings.Split(segments[0][6:], " ")
	var seed []int
	for _, x := range seedStr {
		if strings.TrimSpace(x) == "" {
			continue
		}
		s, err := strconv.Atoi(strings.TrimSpace(x))
		if err != nil {
			log.Fatal(s, err)
		}
		seed = append(seed, s)
	}
	// fmt.Println("Seeds: ", seed)

	maps := make([][]Range, len(segments)-1)
	for i, s := range segments[1:] {
		l := strings.Split(s, "\n")
		maps[i] = make([]Range, len(l)-1)
		for j, x := range l[1:] {
			var m Range
			fmt.Sscanf(x, "%d %d %d", &m.d, &m.s, &m.r)
			maps[i][j] = m
		}
	}

	startTime := time.Now()

	min := math.MaxInt
	for _, s := range seed {
		c := s
	trans:
		for _, m := range maps {
			for _, t := range m {
				source := c
				c = t.transform(c)
				if c != source {
					continue trans
				}
			}
		}
		if c < min {
			min = c
		}
	}
	endTime := time.Now()
	elapsed := endTime.Sub(startTime)
	fmt.Printf("Part 1: %d (%v)\n", min, elapsed) // 662197086

	startTime = time.Now()
	// starting segments
	var S []Segment
	for i := 0; i < len(seed); i += 2 {
		S = append(S, Segment{seed[i], seed[i] + seed[i+1]})
	}

	for _, m := range maps {
		var A []Segment
		for _, t := range m {
			var nS []Segment
			for _, s := range S {
				nA, nnS := createSegments(s, t)
				A = append(A, nA...)
				nS = append(nS, nnS...)
			}
			S = nS
		}
		S = append(S, A...)
	}
	min = math.MaxInt

	for _, s := range S {
		min = aoc.Min(s.from, min)
	}
	endTime = time.Now()
	elapsed = endTime.Sub(startTime)
	fmt.Printf("Part 2: %d (%v)\n", min, elapsed) // 52510809
}

func createSegments(s Segment, t Range) (A []Segment, nS []Segment) {
	before := Segment{s.from, aoc.Min(s.to, t.s)}
	inter := Segment{aoc.Max(s.from, t.s), aoc.Min(t.s+t.r, s.to)}
	after := Segment{aoc.Max(t.s+t.r, s.from), s.to}

	if before.to > before.from {
		nS = append(nS, before)
	}
	if inter.to > inter.from {
		inter.from = inter.from - t.s + t.d
		inter.to = inter.to - t.s + t.d
		A = append(A, inter)
	}
	if after.to > after.from {
		nS = append(nS, after)
	}

	return A, nS
}

The difficulty is going up and down. This day was quite easy in comparison to yesterday. Today it was about parsing some numbers and finding a set of winning numbers.

As I am doing these puzzles in Go I found out that there is no default set type. There is an implementation by HashiCorp named go-set that fills this void. I did not use an external package (I try to avoid them while doing AoC), but I am very tempted to pull that package in.

Here is my solution using lists.

package main

import (
	"fmt"
	"math"
	"strings"

	"arjenwiersma.nl/aoc/internal/aoc"
)

func main() {
	lines := aoc.AsLines("2023/Day04/input.txt")

	ans := 0
	counts := make([]int, len(lines))

	for x := 0; x < len(lines); x++ {
		counts[x] = 1
	}

	for i, l := range lines {
		nums := strings.Split(l, ":")
		parts := strings.Split(nums[1], "|")
		myInts := aoc.StrToInts(strings.Split(parts[1], " "))
		winInts := aoc.StrToInts(strings.Split(parts[0], " "))

		count := 0
		for _, v := range myInts {
			for _, x := range winInts {
				if x == v {
					count += 1
				}
			}
		}
		if count > 0 {
			c := int(math.Pow(2, float64(count)-1))
			for x := 1; x <= count; x++ {
				counts[i+x] += counts[i]
			}
			ans += c
		}
	}
	fmt.Println("Part 1: ", ans)
	ans = 0
	for _, v := range counts {
		ans += v
	}
	fmt.Println("Part 2: ", ans)
}

Day 3 was quite something. I think that in an attempt to make it harder for AI to solve the puzzles the creators also increased the difficulty level of the base puzzles a little too much. The test was not very clear as to what should happen with negative numbers and it might trip people up. The puzzle itself is a great to exercise grid knowledge as you have to work with neighbors and you have to extend the numbers when you find them. Part 2 was just some bookkeeping on my existing implementation, so it was not too much work.

As a note; I first started out with a list of Points, keeping track of the numbers in a coordinate system. This failed miserably :).

package main

import (
	"fmt"

	"arjenwiersma.nl/aoc/internal/aoc"
)

type Point struct {
	Y, X int
}

func IsDigit(b byte) bool {
	return b-'0' >= 0 && b-'0' <= 9
}

func main() {
	lines := aoc.AsLines("2023/Day03/input.txt")

	ygrid := len(lines)
	xgrid := len(lines[0])

	grid := make([][]byte, ygrid)
	for y, l := range lines {
		grid[y] = make([]byte, xgrid)
		for x := 0; x < len(l); x++ {
			grid[y][x] = l[x]
		}
	}

	neighbors := [][]int{
		{-1, -1}, {0, -1}, {1, -1},
		{-1, 0}, {0, 0}, {1, 0},
		{-1, 1}, {0, 1}, {1, 1},
	}

	gears := make(map[Point][]int)
	sum := 0
	for y := 0; y < ygrid; y++ {
		for x := 0; x < xgrid; x++ {
			num := 0
			hasSymbol := false
			isGear := false
			var gearCoord Point
			for IsDigit(grid[y][x]) {
				num = num*10 + int(grid[y][x]-'0')
				for _, n := range neighbors {
					if y+n[1] >= 0 && y+n[1] < ygrid &&
						x+n[0] >= 0 && x+n[0] < xgrid {
						v := grid[y+n[1]][x+n[0]]
						if !IsDigit(v) && v != '.' {
							if v == '*' {
								isGear = true
								gearCoord = Point{X: x + n[0], Y: y + n[1]}
							}
							hasSymbol = true
						}
					}
				}
				x += 1
				if x >= xgrid {
					break
				}
			}
			if num > 0 && hasSymbol {
				if isGear {
					gears[gearCoord] = append(gears[gearCoord], num)
				}
				sum += num
			}
		}
	}

	fmt.Println("Part 1: ", sum) // 498559

	sum = 0
	for _, v := range gears {
		if len(v) == 2 {
			sum += v[0] * v[1]
		}
	}
	fmt.Println("Part 2: ", sum) // 72246648
}