summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore1
-rw-r--r--Makefile9
-rwxr-xr-xbin/categories12
-rwxr-xr-xbin/make48
-rwxr-xr-xbin/makepage36
-rwxr-xr-xbin/makewritindex53
-rwxr-xr-xbin/markup198
-rwxr-xr-xconfig23
-rw-r--r--content/404.md3
-rw-r--r--content/about.md8
-rw-r--r--content/index.md7
-rw-r--r--content/software.md6
-rw-r--r--content/software/dedit.md33
-rw-r--r--content/writings/hands-on-unix-philosophy.md105
-rw-r--r--content/writings/year-with-x201t.md55
-rw-r--r--static/img/rms-horse.pngbin0 -> 1217928 bytes
-rw-r--r--static/style.css154
-rw-r--r--templates/footer.html5
-rw-r--r--templates/header.html9
-rw-r--r--templates/page.html19
20 files changed, 784 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..364fdec
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+public/
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..b2bce2a
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,9 @@
+.POSIX:
+
+make:
+ @"bin/$@"
+
+clean:
+ rm -r public/
+
+.PHONY: make clean
diff --git a/bin/categories b/bin/categories
new file mode 100755
index 0000000..49b9d22
--- /dev/null
+++ b/bin/categories
@@ -0,0 +1,12 @@
+#!/bin/awk -f
+
+FNR == 2 {
+ sub(/^;[ ]*/, "")
+ for (i = 1; i <= NF; i++)
+ seen[$i] = 1
+}
+
+END {
+ for (w in seen)
+ printf "%s\n", w
+}
diff --git a/bin/make b/bin/make
new file mode 100755
index 0000000..3d9c6fd
--- /dev/null
+++ b/bin/make
@@ -0,0 +1,48 @@
+#!/bin/sh -eu
+
+. ./config
+
+title() {
+ [ -z "$1" ] && echo "$TITLE" && return
+ echo "${1##*/} - $TITLE"
+}
+
+mkdir -p "$DIR_BUILD"
+
+echo ">> Building content"
+
+find "$DIR_CONTENT" -type f -name "*.$EXT_WRITINGS" | while read -r file; do
+ base=${file#"$DIR_CONTENT"/}
+ name=${base%."$EXT_WRITINGS"}
+
+ outdir="$DIR_BUILD/$name"
+ if [ "$name" = "index" ]; then
+ output=$DIR_BUILD/index.html
+ name=""
+ else
+ output="$outdir/index.html"
+ mkdir -p "$outdir"
+ fi
+
+ case "$name" in
+ *"${DIR_WRITINGS##*/}"*) name="$(sed -n '1s/^; //p' "$file")" ;;
+ esac
+ echo " $output"
+ markup < "$file" | makepage "$(title "$name")" > "$output"
+
+done
+
+echo ">> Generating writings indexes"
+
+mkdir -p "$DIR_WRITINGS_DST"
+echo " $DIR_WRITINGS_DST/index.html"
+makewritindex | makepage "$(title writings)" > "$DIR_WRITINGS_DST/index.html"
+
+for c in $(categories "$DIR_WRITINGS"/*."$EXT_WRITINGS"); do
+ mkdir -p "$DIR_WRITINGS_DST/$c"
+ echo " $DIR_WRITINGS_DST/$c/index.html"
+ makewritindex "$c" | makepage "$(title "#$c")" > "$DIR_WRITINGS_DST/$c/index.html"
+done
+
+echo ">> Copying static content"
+cp -rv "$DIR_STATIC"/* "$DIR_BUILD" | sed -E "s/.*-> '(.*)'/ \1/"
diff --git a/bin/makepage b/bin/makepage
new file mode 100755
index 0000000..386f0de
--- /dev/null
+++ b/bin/makepage
@@ -0,0 +1,36 @@
+#!/bin/sh -eu
+
+. ./config
+
+title="${1:-$TITLE}"
+content=$(cat)
+
+awk \
+ -v title="$title" \
+ -v content="$content" \
+ -v header="$TEMP_HEAD" \
+ -v footer="$TEMP_FOOT" \
+ '
+function replace(str, pat, rep, out, parts, n, i) {
+ n = split(str, parts, pat)
+ out = parts[1]
+ for (i = 2; i <= n; i++)
+ out = out rep parts[i]
+ return out
+}
+
+BEGIN {
+ while ((getline line < header) > 0) hbuf = hbuf line "\n"
+ close(header)
+ while ((getline line < footer) > 0) fbuf = fbuf line "\n"
+ close(footer)
+}
+
+{
+ line = $0
+ line = replace(line, "{{TITLE}}", title)
+ line = replace(line, "{{CONTENT}}", content)
+ line = replace(line, "{{HEADER}}", hbuf)
+ line = replace(line, "{{FOOTER}}", fbuf)
+ print line
+}' "$TEMP_PAGE"
diff --git a/bin/makewritindex b/bin/makewritindex
new file mode 100755
index 0000000..5562c85
--- /dev/null
+++ b/bin/makewritindex
@@ -0,0 +1,53 @@
+#!/bin/sh -e
+
+. ./config
+
+writings_by_cat() {
+ for file in "$DIR_WRITINGS"/*."$EXT_WRITINGS"; do
+ case "$(sed -n '2p' "$file") " in
+ *" $1 "*) echo "$file" ;;
+ esac
+ done
+}
+
+if [ -z "$1" ]; then
+ cats="$(categories "$DIR_WRITINGS"/*."$EXT_WRITINGS")"
+ writings="$DIR_WRITINGS/*.$EXT_WRITINGS"
+ all=1
+else
+ cats="$1"
+ writings="$(writings_by_cat "$1")"
+fi
+
+writings=$(for f in $writings; do
+ date="$(date -d "$(sed -n '3s/^; //p' "$f")" "+%s")"
+ printf '%s %s\n' "$date" "$f"
+done | sort -rn | cut -f2 -d' ')
+
+cat <<EOF
+<nav class="categories">
+$([ -z "$all" ] && echo "#<a class=\"category\" href=\"/writings/\">all</a>")
+$(for c in $cats; do echo "#<a class=\"category\" href=\"/writings/$c\">$c</a>"; done)
+</nav>
+<ul class="writ-index">
+EOF
+
+for f in $writings; do
+ title=$(sed -n '1s/^; //p' "$f")
+ cats=$(sed -n '2s/^; //p' "$f")
+ date="$(date -d "$(sed -n '3s/^; //p' "$f")" "+%d %b %Y")"
+ file="$(basename "$f" .md)"
+
+ cat_links=$(echo "$cats" | tr ' ' '\n' | while read -r cat; do
+ echo "#<a class=\"category\" href=\"/writings/$cat\">$cat</a>"
+ done | tr '\n' ' ')
+
+ cat <<EOF
+<li class="writing">
+<span>$date&nbsp; <a href="/writings/$file">$title</a></span>
+<span>$cat_links</span>
+</li>
+EOF
+done
+
+echo "</ul>"
diff --git a/bin/markup b/bin/markup
new file mode 100755
index 0000000..5b2ca88
--- /dev/null
+++ b/bin/markup
@@ -0,0 +1,198 @@
+#!/bin/gawk -f
+
+BEGIN {
+ list = ""
+ quote = 0
+ code = 0
+}
+
+function handle_meta( i, n, cats, link)
+{
+ if ($0 ~ /^; /) {
+ if (meta_count == 0) {
+ } else if (meta_count == 1) {
+ sub(/^; /, "")
+ n = split($0, cats, " ")
+ printf "<nav class=\"categories\">"
+ printf "#<a class=\"category\" href=\"/writings/\">all</a> "
+ for (i = 1; i <= n; i++) {
+ link = "/writings/" cats[i]
+ printf "<a class=\"category\" href=\"" link "\">#" cats[i] "</a> "
+ }
+ print "</nav>"
+ } else if (meta_count == 2) {
+ sub(/^; /, "")
+ print "<p class=\"date\">" $0 "</p>"
+ }
+ meta_count++
+ next
+ }
+}
+function handle_list_item(condition, type)
+{
+ if ($0 ~ condition) {
+ if (list != type) {
+ if (list != "") print "</" list ">"
+ print "<" type ">"
+ list = type
+ }
+ sub(condition, "")
+ print "<li>" $0 "</li>"
+ next
+ }
+}
+
+function handle_quote()
+{
+ if ($0 == "\"\"\"") {
+ if (quote == 0) {
+ print "<blockquote>"
+ quote = 1
+ } else {
+ print "</blockquote>"
+ quote = 0
+ }
+ next
+ }
+}
+
+function handle_codeblock()
+{
+ if ($0 ~ /^'''/) {
+ if (code == 0) {
+ if (list != "") {
+ print "</" list ">"
+ list = ""
+ }
+ print "<pre><code>"
+ code = 1
+ } else {
+ print "</code></pre>"
+ code = 0
+ }
+ next
+ }
+
+ if (code == 1) {
+ gsub(/&/, "\\&amp;")
+ gsub(/</, "\\&lt;")
+ gsub(/>/, "\\&gt;")
+ print
+ next
+ }
+}
+
+function inline(delim, tag, escaped, pattern)
+{
+ escaped = delim
+ gsub(/\*/, "\\*", escaped)
+ gsub(/\./, "\\.", escaped)
+
+ pattern = escaped "([^" escaped "]+)" escaped
+ while (match($0, pattern)) {
+ $0 = substr($0, 1, RSTART-1) \
+ "<" tag ">" substr($0, RSTART+length(delim), \
+ RLENGTH-length(delim)*2) "</" tag ">" \
+ substr($0, RSTART+RLENGTH)
+ }
+}
+
+function inline_link(pattern, result, pre, arr)
+{
+ pattern = "\\[([^]]+)\\]\\(([^)]+)\\)"
+ result = ""
+ while (match($0, pattern, arr)) {
+ pre = substr($0, 1, RSTART-1)
+ result = result pre "<a href=\"" arr[2] "\">" arr[1] "</a>"
+ $0 = substr($0, RSTART+RLENGTH)
+ }
+ $0 = result $0
+}
+
+function inline_image(pattern, result, pre, arr)
+{
+ pattern = "!\\[([^]]+)\\]\\(([^)]+)\\)"
+ result = ""
+ while (match($0, pattern, arr)) {
+ pre = substr($0, 1, RSTART-1)
+ result = result pre "<a href=\"" arr[2] "\">" "<img alt=\"" \
+ arr[1] "\" src=\"" arr[2] "\">" "</a>"
+ $0 = substr($0, RSTART+RLENGTH)
+ }
+ $0 = result $0
+}
+
+function inline_footnote_ref(pattern, result, pre, arr)
+{
+ pattern = ".\\[\\^([^]]+)\\]"
+ result = ""
+ while (match($0, pattern, arr)) {
+ pre = substr($0, 1, RSTART)
+ result = result pre "<sup id=\"fnref-" \
+ arr[1] "\"><a href=\"#fn-" arr[1] "\">" arr[1] "</a></sup>"
+ $0 = substr($0, RSTART+RLENGTH)
+ }
+ $0 = result $0
+}
+
+function handle_header(i, level)
+{
+ if ($0 ~ /^#+[ ]/) {
+ level = 0
+ while (substr($0, level+1, 1) == "#") level++
+ sub(/^#+[ ]+/, "")
+ print "<h" level ">" $0 "</h" level ">"
+ next
+ }
+}
+
+function handle_footnote(pattern, arr)
+{
+ pattern = "^\\[\\^([^]]+)\\]:[ ]*(.*)"
+ if (match($0, pattern, arr)) {
+ if (footnote != 1) {
+ print "<ol class=\"footnotes\">"
+ footnote = 1
+ }
+ print "<li id=\"fn-" arr[1] "\">" arr[2] \
+ "<a href=\"#fnref-" arr[1] "\">↩</a></li>"
+ list = "ol"
+ next
+ }
+}
+
+{
+ handle_meta()
+ handle_quote()
+ handle_codeblock()
+
+ inline("**", "b")
+ inline("*", "em")
+ inline("--", "s")
+ inline("'''", "code")
+
+ inline_image()
+ inline_link()
+
+ inline_footnote_ref()
+
+ handle_list_item("^- ", "ul")
+ handle_list_item("^[0-9]+\\.[ ]+", "ol")
+ handle_footnote()
+
+ if (list != "") {
+ print "</" list ">"
+ list = ""
+ }
+
+ handle_header()
+
+ if ($0 != "") {
+ print "<p>" $0 "</p>"
+ }
+}
+
+END {
+ if (list != "")
+ print "</" list ">"
+}
diff --git a/config b/config
new file mode 100755
index 0000000..07a5ed7
--- /dev/null
+++ b/config
@@ -0,0 +1,23 @@
+#!/bin/sh -eu
+
+TITLE=JHHM
+
+BIN_DIR=bin
+DIR_CONTENT=content
+DIR_STATIC=static
+DIR_BUILD=public
+DIR_TEMP=templates
+
+DIR_WRITINGS="$DIR_CONTENT"/writings
+DIR_WRITINGS_DST="$DIR_BUILD"/writings
+
+TEMP_PAGE="$DIR_TEMP"/page.html
+TEMP_HEAD="$DIR_TEMP"/header.html
+TEMP_FOOT="$DIR_TEMP"/footer.html
+
+STYLESHEET="$DIR_STATIC"/style.css
+
+EXT_WRITINGS="md"
+
+export PATH="$PWD/bin:$PATH"
+
diff --git a/content/404.md b/content/404.md
new file mode 100644
index 0000000..41de80e
--- /dev/null
+++ b/content/404.md
@@ -0,0 +1,3 @@
+# 404 page not found!
+
+I couldn't find that page. :(
diff --git a/content/about.md b/content/about.md
new file mode 100644
index 0000000..0a89803
--- /dev/null
+++ b/content/about.md
@@ -0,0 +1,8 @@
+# about
+
+I am a computer science student from Norway, currently studying at [NTNU](https://www.ntnu.no/).
+
+In my freetime I enjoy programming, Linux, tinkering with old Thinkpads, playing [Teeworlds](https://teeworlds.com/) and woodworking.
+
+## links
+- [GitHub](https://github.com/johannesHHM)
diff --git a/content/index.md b/content/index.md
new file mode 100644
index 0000000..a68e864
--- /dev/null
+++ b/content/index.md
@@ -0,0 +1,7 @@
+# welcome
+
+![RMS on a horse](/img/rms-horse.png)
+
+This is my homepage!
+
+It is currently being populated with my software projects, freetime interests and thoughts.
diff --git a/content/software.md b/content/software.md
new file mode 100644
index 0000000..b56b241
--- /dev/null
+++ b/content/software.md
@@ -0,0 +1,6 @@
+# software
+
+Here is a list of free and open-source software I have written. It is not an exhaustive list of my projects, only the more interesting or useful ones.
+
+- [dedit](/software/dedit/) - Tool for inspecting and editing [Teeworlds](https://teeworlds.com) demo files
+- [lol-cal](https://github.com/johannesHHM/lol-cal) - A lolesports schedule viewer in the terminal
diff --git a/content/software/dedit.md b/content/software/dedit.md
new file mode 100644
index 0000000..9b04a21
--- /dev/null
+++ b/content/software/dedit.md
@@ -0,0 +1,33 @@
+# dedit
+
+Dedit is a [Teeworlds](https://teeworlds.com/) / [DDNet](https://ddnet.org/) demo inspector and editor. It is written in portable C99 with zero dependencies. The code repository is hosted on GitHub [here](https://github.com/johannesHHM/demo-edit).
+
+## features
+
+- renaming tees based on ID / name
+- changing the skin of tees
+- changing the map of the demo
+- extracting the map from the demo
+- printing information about each demo chunk
+
+## background
+
+The main motivation for dedit came from wanting to create some consistency for the [RRT FNG montages](https://www.youtube.com/@rrtteeworlds5108). Being able to edit the map and skins of the montage clips could be used for cool effects, or simply for changing the name of a player if an alt was used.
+
+Because of this, only aesthetic changes can be made using the tool, and there are no plans to support editing actual gameplay data.
+
+## status & future work
+
+Dedit is currently only tested and working for demo versions 5 and 6 (DDNet), and the Teeworlds 0.6 network protocol. I might tinker with supporting older demo versions, but the use case for this would be slim to none. Support for the 0.7 network protocol is planned, but I don't know when I will get around to implementing it.
+
+Parsing of message chunks is currently working, but I have yet to expose this to the user. The most obvious use case for this would be printing the chat to stdout, similar to the [demo_extract_chat](https://github.com/ddnet/ddnet/blob/master/src/tools/demo_extract_chat.cpp) DDNet tool.
+
+I have done some experimental work on “merging” two demo files in an attempt to have data from multiple demos present in one. The use case for this would be having a demo from a PvP match include data for multiple POVs, since the server only sends player data for players close to you. The current experiments have not been successful, and the demos appear jittery when transitioning from one demo’s data to another. I might continue working on this feature, but I am not hopeful that it will work.
+
+## credits
+
+A couple of functions and implementations are heavily inspired by other sources. They are commented in the code with links to their references. Below are some credits and acknowledgements:
+
+- [libtw2](https://github.com/heinrich5991/libtw2): variable-length integer implementation, demo/snapshot documentation
+- [teeworlds](https://github.com/teeworlds/teeworlds): Huffman implementation
+- [ddnet](https://github.com/ddnet/ddnet): some demo writing specifics
diff --git a/content/writings/hands-on-unix-philosophy.md b/content/writings/hands-on-unix-philosophy.md
new file mode 100644
index 0000000..1619e1d
--- /dev/null
+++ b/content/writings/hands-on-unix-philosophy.md
@@ -0,0 +1,105 @@
+; Hands on Unix Philosophy
+; tech
+; Tue Mar 10 12:29:22 PM CET 2026
+
+# Hands on Unix Philosophy
+
+I sometimes find myself in a position of trying to explain to someone why I use the software that I do. Two common examples are Linux and "all those text programs". These questions might come from a curious person who has no idea what Unix or a terminal is, or from a technically savvy person who thinks people who use the terminal are hipsters trying to impress themselves. In this post I will defend my choice of software by quickly explaining the Unix philosophy and demonstrating it through a practical example.
+
+## The Philosophy
+
+I won't wade too deep in my explanation of the Unix philosophy. There are countless other, better resources on the internet available for anyone who wants a deeper understanding. But for those who only want to dip their toes in the water, here is the gist of it.
+
+Programs are more useful when they are simple, small in scope, and allow for easy interoperability with other programs. Simple programs are good because they have fewer bugs, are easier to maintain, and probably won't become resource-hungry behemoths. This goes hand in hand with being small in scope. Programs that have a small, well-defined function are easier to reason about,thus making them easier to use. The last piece of the puzzle is where the magic happens: connecting these small programs. [Malcolm Douglas McIlroy](https://www.cs.dartmouth.edu/~doug/) of Unix heritage elegantly captured this idea when he said:
+
+"""
+This is the Unix philosophy: Write programs that do one thing and do it well. Write programs to work together. Write programs to handle text streams, because that is a universal interface.
+"""
+
+"Do one thing and do it well" is perhaps the phrase most associated with the Unix philosophy. I like it because it implies the existence of software that does a lot of things poorly, which sure seems to describe a lot of software.[^1]
+
+## Practical Example
+
+The other day I found myself doing a repetitive task I had done hundreds of times before. I came across a script that used an option flag that I was unfamiliar with, and started wondering what it did. I used the program **man**, which shows the manual page for a program[^2], and searched for the unfamiliar option flag and read about it. This is a trivial operation, easily done in a couple of seconds, yet it can be surprisingly frustrating at times. Certain options are often referenced before their own section, or are a substring of another option, so finding it can involve pressing '''n''' multiple times before reaching the correct place.
+
+My go-to solution for these small inconveniences is usually to just go about my day. Then it happens again, and I think, "I have been here before," and then it happens again and again, until eventually I make a mental note that I could improve my experience. For a problem as trivial as this one, I also know immediately what the solution is. I want to make a program that takes a program and an option and prints the section of the manual describing that option.
+
+On a Unix-y operating system, writing a script that does this is fairly trivial. All we need are two small programs, one that prints the entire manual, and one that filters it. The first program has already been mentioned, **man**, which prints the manual page in plain text formatting.[^3] The next program should read this output (a text stream) and filter it the way we want. I chose **awk** for this task, which allows you to take text input and transform it in various ways by filtering and rearranging it into a new text. The resulting shell script ended up looking like this:
+
+'''
+#!/bin/sh
+
+set -eu
+
+[ "$#" -ne 2 ] && echo "usage: ${0##*/} PAGE OPT" >&2 && exit 1
+
+man "$1" | awk -v opt="$2" '
+ /^[[:space:]]*-/ {
+ hit = ($0 ~ "(^|[ ,])" opt "([ ,=[]|$)")
+ }
+ hit
+'
+'''
+
+There is some extra fluff before the core functionality of the script, which I will go over in detail in the next part. The important part, however, is that by taking two programs, both using plaintext as their interoperable interface, we have created a new program which solves the problem. By spending 15 minutes writing a script, I have been able to improve my workflow, saving me a bit of time, and more importantly, the cognitive effort of manually scanning the page for the correct section.
+
+Importantly, I would argue that this could not have been done as elegantly or easily using other programs that do not follow the Unix philosophy. Perhaps this use case is common enough that a fully fledged manual viewer would include it, but for more niche use cases, that might not be true. One thing that I know my new program has an edge over the theoretical manual viewer is that it outputs a text steam, which makes it extendable just like **man** and **awk**.
+
+## Script Specifics
+
+For those interested, I wanted to write about some of the decisions I made when writing the script (I call it **boy**; get it?) Here’s a line-by-line explanation of the script:
+
+'''
+#!/bin/sh
+'''
+
+I try to write most, if not all of my scripts in POSIX compliant shell for portability and simplicity sake. This is fairly trivial in small scripts like this, which don't require much from the shell other than running programs.
+
+A simple '''/bin/sh''' shebang is preferable to '''/usr/bin/env sh''' for POSIX scripts, since '''/bin/sh''' is already mandated by POSIX. Adding another dependency via **env** would therefore be unnecessary, and if the system's '''/bin/sh''' either does not exist or is not POSIX-compliant, there are larger problems afoot.
+
+'''
+set -eu
+'''
+
+Setting exit on error ('''-e''') and unset variables ('''-u''') is usually a good practice, although in this specific script neither will do much. The only program that can realistically fail is **man**, but '''-e''' does not protect against failure in pipes, so the script won't exit if **man** fails. We are also checking for required arguments in the next line, so '''-u''' will not catch any usage of unset variables. I've included them mainly in case I make edits to the script in the future, and as I mentioned, it is good practice.
+
+'''
+[ "$#" -ne 2 ] && echo "usage: ${0##*/} PAGE OPT" >&2 && exit 1
+'''
+
+This line checks if the correct amount of arguments is given to the program (2 in this case). If it is not 2, the usage message will be printed to stderr, and the program will exit with status code 1, indicating an error. I try to include a usage message in most of my scripts, as it provides helpful documentation for the future. The '''${0##*/}''' part is a useful shell syntax called pattern removal, which removes everything up to and including the last '''/''' in the '''$0''' variable. This is an alternative to using the **basename** command, e.g, '''$(basename "$0")'''.
+
+'''
+man "$1" | awk -v opt="$2" '
+ /^[[:space:]]*-/ {
+ hit = ($0 ~ "(^|[ ,])" opt "([ ,=[]|$)")
+ }
+ hit
+'
+'''
+
+This line is the main part of the script. It runs the **man** command, and pipes the output into **awk**, which is passed the second argument as '''opt'''. The awk script can look intimidating if you’re not familiar with awk or regular expressions, but I will try to explain it without getting bogged down in language-specific syntax.[^4] The script can be read as follows: for every line, if the line starts with whitespace followed by a *-*, check if '''opt''' appears as a word in that line. If that is true, '''hit''' is set to true, and if '''hit''' is true, the line is printed.
+
+Summarized, the logic basically sets hit to true if the section corresponds to the given '''opt''', and sets '''hit''' to false when the section is not for '''opt'''. It then prints the line if hit is true.
+
+An unintended side effect of this script is that it allows for regular expressions in the '''opt''' input via **awk**, e.g.:
+
+'''
+$ boy ls '--time.*'
+ --time=WORD
+ select which timestamp used to display or sort; access time (-u): atime, access, use; metadata change time (-c):
+ ctime, status; modified time (default): mtime, modification; birth time: birth, creation; with -l, WORD determines
+ which time to show; with --sort=time, sort by WORD (newest first)
+
+ --time-style=TIME_STYLE
+ time/date format with -l; see TIME_STYLE below
+
+ -t sort by time, newest first; see --time
+'''
+
+As a last aside, I am sure that there exists some edge cases that this script will not handle correctly. Man pages can be formatted in all sorts of different ways, and there is no standard for how options should be documented. The point of this post is to demonstrate how following the Unix philosophy allows for useful extensibility, with only a small amount of effort.
+
+[^1]: I am sometimes "forced" to use Microsoft Teams for university collaborations. It has mediocre file sharing, poor document editing, and frustrating formatting in Microsoft Word, all while being so horribly slow that it takes 20 seconds to launch, and stresses my CPU cores just trying to open a folder.
+[^2]: In a hidden example of the Unix philosophy, **man** does not actually display the manual page in a scrollable, searchable interface. It simply prints the contents of the man page and hands it off to another program, in most cases **less**, which provides an interactive interface to the wall of text.
+[^3]: The term "plain text" hides a lot of assumptions, which can lead to a lot of harmful consequences. [Here](https://www.youtube.com/watch?v=_mZBa3sqTrI) is an entertaining talk all about it. Luckily for our purposes we can be quite naïve, and just assume that text is text.
+[^4]: For a nice introduction to awk, I recommend [this](https://www.grymoire.com/Unix/Awk.html) old tutorial / introduction. Some of it might be a bit dated, but I think you are better off knowing some of the old idiosyncrasies/intricacies of the Unix world.
diff --git a/content/writings/year-with-x201t.md b/content/writings/year-with-x201t.md
new file mode 100644
index 0000000..d31fa8d
--- /dev/null
+++ b/content/writings/year-with-x201t.md
@@ -0,0 +1,55 @@
+; A Year With the Thinkpad x201t
+; tech thinkpads
+; Fri Feb 20 01:27:46 AM CET 2026
+
+# A Year with the Thinkpad x201t
+
+As of writing this it has been exactly 1 year since i bought my x201t, and i have been using it as my main[^1] laptop for the past 10 months. I thought that it might be interesting to write down my experience with the now almost 15 year old machine, and my motivations for using it.
+
+## Backstory
+
+I became fascinated by older Thinkpad models a few years back for all of the reasons most people do. Their reliability, sturdiness, keyboard, Linux support, and price all drew me in to the point where I now own a small collection of them. I have mixed feelings about owning machines that I have no real use for, but I take some comfort in knowing that there are worse consumerist urges then buying cheap second-hand laptops.
+
+Exactly why I ended up using the x201t was partly the result of methodical reasoning, but mostly chance. I already owned and was using an x220, but I had an irresistible itch for something more. Although there was nothing wrong with the x220, I had recently discovered the world of convertible two-in-one Thinkpad tablets. When a high-end x201t appeared as a new listing on finn.no[^2], I knew I had to have it.
+
+I now consider the x201t, in some ways, to be the pinnacle of Thinkpad convertibles. It features a 16:10 aspect ratio screen, a display latch, a whopping five buttons on the display assembly, and a full collection of indicator LEDs. These are all features that the later x220t and x230t lack, to their detriment. Those later models do have their own strengths however: they can be equipped with 16GB of RAM, double that of the x201t, and offer more modern ports such as DisplayPort, USB-3, and mSATA, along with newer-generation CPUs. If you do have a choice between these three models, I believe these are the distinguishing factors, and if you are like me then the retro features of the older model wins out. I later ended up buying an x230t, and these feelings only grew stronger while owning both machines. The x230t sits on a shelf now, and has probably began it's new life as a dust magnet.
+
+## Hardware
+
+- CPU: Intel(R) Core(TM) i7 CPU L 640 @ 2.13GHz
+- RAM: 8GB DDR3 1334 MT/s
+- STORAGE: 128GB Intel SSD
+- DISPLAY: 12.1" 1280x800, 400 nits
+
+When i first started using the x201t I knew that the main drawback of the machine would be it's aging hardware. I am by no means under the destructive[^3] delusion that any computer older then 5 years needs to be replaced by the latest and greatest, but the 15 year old hardware does show it's age, especially on the bloated web. That being said, it has performed admirably on most other tasks that i threw at it, in no small part due to my choice of software, but more on that later.
+
+The only upgrades that i did for the hardware was upgrading to 8GB of RAM from 4GB, and giving it some new thermal paste[^4]. The CPU hovers around 40-50°C during light workloads, and goes to around 65-75°C when streaming videos. This is a bit hot for my liking, but the fan never gets too loud, so it is something I can deal with. I might have to upgrade the drive to something larger as it fills up, but the old SSD has been snappy enough for me.
+
+The battery that came with the machine lasted roughly 1.5-2.5 hours during medium workloads, which i was pleasantly surprised about. Other then the old hardware, the battery life will always be a weak point of these older machines, and you can be happy to get around 3 hours of battery life. I ended up buying a replacement battery from China, which holds a charge for maybe 1 hour longer. Not great, not terrible.
+
+I have already laid out some of the killer-features of the x201t compared to it's successors, and here are the reasons why they are killer-features. The 16:10 aspect ratio is great for most tasks, whether reading, programming or just surfing the web. I often have two windows open, stacked on top of each other, and the extra vertical space makes both programs comfortably usable. Perhaps the greatest testament to the taller aspect ratio is that I immediately notice, and miss it when I use a computer without it.
+
+The display itself on my x201t is quite nice, much better then the standard TN displays most Thinkpads of the era came equipped with. It only has a resolution of 1280x800, but on such a small screen it is completely fine, especially since the hardware would struggle streaming 1080p videos anyways. I am no expert in display technology, but I do notice the black levels being quite shallow, and it has a overall more washed-out appearance then some nicer, more modern displays i have seen. It can get quite bright, and I find it pleasant to use even when outside in direct sunlight.
+
+The display latch is one retro feature that I don't particularly miss when using the x220, and i don't particularly love it on the t420, but I consider it almost a must-have for the tablet models. I've found that the single-hinge design of the tablet models can make them a bit more flimsy, so being able to lock the display down when carrying it in a backpack, or when in tablet mode is a nice feature. My x230t came with a loose hinge, and the replacement hinge I bought online was just as lose, so a display latch would have been a significant improvement to it's usefulness.
+
+The classic 7-row Thinkpad keyboard is probably the main reason i stick to using these old machines. The typing experience is better then any other laptop keyboard i have ever used, and i even prefer it to my mechanical keyboard at times. The laptop came with a US ANSI layout, which I eventually switched out for a Danish[^5] ISO keyboard, which I am more used to. The x201t has a slightly different layout then the later xx20 models. Most noticeably the escape key is smaller, and the keys have a more "rounded" feel to them. I have a slight preference for the newer type, but I do like the mushy volume and power buttons on the old ones. In addition to the keyboard buttons, there are five additional buttons on the top display assembly, which can also be used in tablet mode. Of these buttons I only use the two rotation buttons (only one is needed), and the lock button. I do not use the second power button, and I never bound the last "toolbox" button to anything.
+
+This might be a good time to address the main draw of the x201t, the tablet functionality. The x201t comes equipped with a Wacom touch screen (mine only supports pen touch, though other models also support finger-touch), and it has the ability to transform into tablet mode with the unique single-hinge design. The reality is that i rarely use the touch functionality, since i prefer typing out notes, and I don't do any drawing. I have used it on rare occasions for hand-drawn diagrams, and it has come in handy a couple of times when I need to make some sketches. My main takeaway is that the tablet functionality is not a big win for me, and I will probably not value it highly when i choose my next laptop.
+
+## Software
+
+Since using the x201t my choice of software has been in a turbulent state, so i will avoid going into too many details. The most important choices are to avoid Windows, and any other overly complex, bloated software. I have been using Arch Linux, with [dwl](https://codeberg.org/dwl/dwl) as my wayland compositor for the past year. I am not quite content with my setup, but I doubt it will change much in the near future[^6]. The next important piece of software is the terminal, which has been [foot](https://codeberg.org/dnkl/foot) for me the past year. It is lightweight, and has all of the features i want and need in a terminal. It also has the ability of being run in a server client mode, which decreases the already tiny startup time to be instantaneous, and saves on system resources.
+
+When it comes to web browsers I have been using [Qutebrowser](https://www.qutebrowser.org/) as my main browser, and Firefox when necessary. Both are large and slow, but the modern web basically necessitates it. The rest of my graphical programs are all fairly standard Linux stuff: [GIMP](https://www.gimp.org/), [mpv](https://mpv.io/), [zathura](https://pwmt.org/projects/zathura/). They all run fine, although all of them will sometimes slow down when handling certain large files.
+
+## Conclusion
+
+I have grown to love the x201t over the last year of use. One aspect of the laptop that might not have come through in this write-up is just how beautiful the laptop is. In a world full of matt anodized aluminum MacBook lookalikes, the retro look and feel of the x201t really shines through. It has flashing indicator lights, a bump for the antenna, a comically tiny touchpad, and it's signature swivelling hinge. All of these features make it a fun laptop to use, and it inspires me to get my work done using it. It has become my favorite Thinkpad by far, and i hope that it can continue to server my needs in the future.
+
+[^1]: Ideally I would like to only use one laptop, but I am required to use a modern windows laptop when taking exams from my university. I also tinker with other machines on a semi-consistent basis, but i do not use them outside of this tinkering.
+[^2]: Finn.no is a Norwegian equivalent to eBay or Facebook marketplace.
+[^3]: Destructive to the mind, wallet and planet. Most of us consume too much of most things, electronics only being one of these things.
+[^4]: The tablet Thinkpads are probably some of the least fun Thinkpads to re-paste. It requires a full disassembly, and the thin top covers can be tricky to remove without breaking something. Luckily this operation is rarely needed, and most other components like RAM and storage are just a couple of screws away.
+[^5]: I could not find a Norwegian keyboard, so I ended up settling for a Danish one, and swapping the Ø and Æ letters to their proper places.
+[^6]: Once River 0.4 releases I think I will give it a shot, since dwl seems to be in a stagnant rut in an ever-changing wayland world. I have also started exploring other operating systems, namely the BSDs and Alpine Linux, but I have yet to take the leap from my familiar Arch Linux.
diff --git a/static/img/rms-horse.png b/static/img/rms-horse.png
new file mode 100644
index 0000000..3612dfa
--- /dev/null
+++ b/static/img/rms-horse.png
Binary files differ
diff --git a/static/style.css b/static/style.css
new file mode 100644
index 0000000..5b91113
--- /dev/null
+++ b/static/style.css
@@ -0,0 +1,154 @@
+:root {
+ --fg: #000;
+ --bg: #eee;
+ --bg-code: #ddd;
+ --border: 0.1rem solid var(--fg);
+}
+
+/* Generic */
+body {
+ color: var(--fg);
+ background-color: var(--bg);
+ fill: var(--fg);
+
+ margin: 0 auto;
+ max-width: 48em;
+ padding: 1em;
+ font: 1em/1.2 serif;
+
+ overflow-y: scroll;
+}
+
+h1 {
+ font: 1.4em serif;
+}
+
+h2 {
+ font: 1.2em serif;
+ margin: 0em;
+}
+
+a {
+ font-style: italic;
+ color: #000;
+}
+
+/* visited link */
+a:visited {
+ color: #555;
+}
+
+img {
+ max-width: 100%;
+}
+
+/* Blockquote */
+blockquote {
+ margin: 1.5em 0;
+ padding: 0.75em 1em;
+ border-left: 0.3em solid #aaa;
+ background: #f9f9f9;
+}
+
+blockquote p {
+ margin: 0.5em 0;
+}
+
+blockquote {
+ color: #333;
+ font-style: italic;
+}
+
+blockquote p:first-child { margin-top: 0em; }
+blockquote p:last-child { margin-bottom: 0em; }
+
+/* Codeblocks */
+pre {
+ background: var(--bg-code);
+ padding: 0em 1em 1em 1em;
+ overflow-x: auto;
+}
+
+code {
+ font-family: monospace;
+ padding: 0em;
+}
+
+/* Inline Code */
+p code {
+ background: var(--bg-code);
+ border: 1px solid #ddd;
+ padding: 0.1em 0.3em;
+}
+
+/* Footnotes */
+.footnotes {
+ border-top: var(--border);
+ font: 0.8em serif;
+ padding: 1em 3em;
+
+ li {
+ margin-bottom: 0.5em;
+ }
+}
+
+/* Header */
+header {
+ border-bottom: var(--border);
+ padding-bottom: 1em;
+
+ .site-title a {
+ font-style: normal;
+ color: inherit;
+ text-decoration: none;
+ }
+}
+
+.site-nav a {
+ font-style: normal;
+ margin-right: 1.5em;
+ text-decoration: none;
+ color: #333;
+}
+
+/* Footer */
+footer {
+ border-top: var(--border);
+ padding: 1em;
+ text-align: center;
+ font: 0.7em sans-serif;
+}
+
+/* Writings Index */
+li.writing {
+ display: flex;
+ justify-content: space-between;
+ list-style: none;
+ margin-bottom: 0.5em;
+}
+
+li.writing span:first-child {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ margin-right: 1ch;
+}
+
+li.writing span:nth-child(2) {
+ margin-left: auto;
+ white-space: nowrap;
+}
+
+ul.writ-index {
+ padding: 0em;
+}
+
+.categories {
+ margin-top: 0.5em;
+}
+
+a.category {
+ margin-right: 0.1em;
+ /*text-decoration: none;*/
+ color: #222;
+}
diff --git a/templates/footer.html b/templates/footer.html
new file mode 100644
index 0000000..6f15869
--- /dev/null
+++ b/templates/footer.html
@@ -0,0 +1,5 @@
+<footer>
+ <span class="line">© 2026,</span>
+ <span class="line">JHHM,</span>
+ <span class="line">All Rights Reserved</span>
+</footer>
diff --git a/templates/header.html b/templates/header.html
new file mode 100644
index 0000000..a28c2f0
--- /dev/null
+++ b/templates/header.html
@@ -0,0 +1,9 @@
+<header>
+ <h1 class="site-title"><a href="/">JHHM</a></h1>
+ <nav class="site-nav">
+ <a href="/">home</a>
+ <a href="/writings/">writings</a>
+ <a href="/software/">software</a>
+ <a href="/about/">about</a>
+ </nav>
+</header>
diff --git a/templates/page.html b/templates/page.html
new file mode 100644
index 0000000..39fd7b7
--- /dev/null
+++ b/templates/page.html
@@ -0,0 +1,19 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="UTF-8">
+<link rel="stylesheet" href="/style.css">
+<title>{{TITLE}}</title>
+</head>
+<body>
+
+{{HEADER}}
+
+<main>
+{{CONTENT}}
+</main>
+
+{{FOOTER}}
+
+</body>
+</html>