Log in
Seblog.nl

English posts

So now I am a Game Master

I just finished So You Want To Be a Game Master by Justin Alexander, and I guess I am now a Game Master. Well, practically speaking, I already was, because 18 May I ran my first session (with a dungeon from the book) and that was the first of the seven sessions I ran since then. Most of the sessions were in the ever so popular Dungeons & Dragons, but one of them was Pirate Borg.

And I love it. Long time followers of this blog know that 12 years ago, I was deeply into creative writing. Since then, it kind of waned, as I found the game of Go and other hobbies, as well as a job that made me write code all day. More recently, through a Go summer camp, I discovered other boardgames, the Boardgayming Amsterdam community and through that I got into D&D again.[^1]

What I love about it, is that it brings together the storytelling of creative writing, with the mathematics and execution paths of coding, but also just the general social experience of an evening with friends. I even picked up my drawing a bit, albeit mostly for maps.

I still like boardgames, but diving into the RPG-space taught me also a bit more about what I like about those: the story that you tell at the table. Sure, I like to win,[^2] but I am really only able to withstand six hours of Risk because of the epic story that unfolds on the battlefield. Or in my more recently played games: I like to be a member of a house in Night of the Ninja, to be a circus owner in SCOUT, or a radio officer in Captain Sonar.

My next goal is to actually start a campaign, as my seven one-shots are not really sustainable in the long run (so much preparation proportional to the game time). And the general goal is to just get better at improvising at the table, to just go with the flow of where-ever the players want to go. And maybe a subgoal is to write about my progress from time to time here.

It's great to be back in language.

[^1]: Again, because pre-covid I actually played in a campaign as a player, thanks to Mike, Luuk and the others.
[^2]: I wrote about wanting to win in another blogpost.

A more inclusive workspace

The only reason I dare to write any of this is because Henrique wrote about the positives and negatives of his new workplace.

I have a relatively new job too, since March, and one of the things I struggle most with is that my coworkers are much less "my kind of people" than I used to have around me in the previous company, which unfortunately went bankrupt. They are still nice people and most of them mean well, but I don't feel at home.

Last Friday, during lunch time, I found myself suddenly in a homophobic conversation among the three other coworkers at the table. It was the kind of conversation where straight males find an anecdote from their past where they were confronted with homosexuality, and then distance themselves from it by telling how they rejected it in the moment. The group will then encourage this by confirming they wouldn't have that either, and then someone else can take the turn to tell such an anecdote.

Being in the conversation felt like being in a slow train wreck. I looked up from my phone, wondered what was happening here, but then it not only continued, it worsened, with the anecdotes just piling up. It is very hard for any person to break such a chain, even for allies willing to change the subject, and I as an open gay person (to them too!) just did not how to handle this. When the conversation ended I walked away, did a solitary walk around the block and packed my stuff to work from home the rest of the day.

The reaction of my teamlead was good: we scheduled a meeting with someone from HR. After the meeting, I talked it over with two of the coworkers that same Monday, and with the last coworker yesterday, as he wasn't present anymore on Monday. With this, everything should be fine.

But I notice I still feel bad. To be fair, I felt much better on Monday. The reactions of the first two coworkers were really good and I noticed how completely at ease I worked on Monday afternoon. I knew I belonged, that I could sit there behind that desk, that it was my place and that I was valued. That is a very important feeling.

The third coworker was back on Tuesday, and this is the coworker I have caught with homophobic and racist comments before, so I felt a bit more nervous going into this conversation.

He didn't notice there was something wrong with the topic, and he said he did not have the intention of hurting me. I said I could try to help him by being more clear about when a topic wasn't suitable. He agreed. I said that I am actively withholding parts of myself and my opinions from lunch conversations, because I know he has different political views. He said that yes, he is that way, he likes to ventilate. At the end of the conversation the teamlead asked if I wanted to add anything. I said that for that moment, I did not.

I was already dissatisfied when we walked out of the room. I gave my coworker space to be himself (as I always try to) and hoped he would return the favor. He took the space, but offered none. In a way, I now made it my problem to wait for the next homophobic moment. I now have to be watchful again, because it might happen again – dare I say, will happen again. The first time it will be mild, but if I let it slip, it will come back bigger, until we are at full homophobia and full racism again.

I mean this last part is obviously speculation. But it reflects how I envision the situation to go, and how I lost that feeling of being able to just be, to just focus on my work without having to worry about what conversations are happening around me. Inclusivity is an effort, and it should not be on the shoulders of those who are in some minority.

In the conversation with HR, they said they were alarmed because I said "I have as much right to this job as them". Conversations like the one at lunch are a way to subvert that right for minorities, because it makes them be on guard when the straight white cis males can work with all their focus. Help, I even think I played this down a bit for HR, just because I didn't want them to feel so uncomfortable with the thought of homophobia in their company. It wasn't aimed at me right? Maybe they didn't mean it like that? But no: it was very toxic and it should not have happened. And: it is not my job to educate my coworkers.

I don't exactly know what note I want to end on. I guess I want to just thank you for reading. Trying to understand each other and to see life from their perspective is the best thing we can do in these matters.

Chasing the casing in Vim

A lot of programming is really just taking data in one form and turning it into another. Imports and exports. Within different contexts, different conventions apply. Within Laravel, database columns are usually snake_case, yet within PHP most variables are camelCased. Within HTML and CSS, things are usually kebab-cased, until you find a React component, which are usually PascalCased.

Every once in a while I end up copying names from one source and having to turn them into another case. With the Vim language of editing, turning a_cased_string into aCasedString usually involves me typing f_ to jump my cursor to the next underscore, and then x~ to delete that underscore and turn the next character into it's uppercase variant. I then have to do that for however many times there are underscores in my target string. (Subsequent jumps to underscores can be made with ; though.)

The conversion back from aCasedString to a_cased_string is always a bit more bothersome, because you need to insert a lot of underscores. I usually do it with a second pass: use fC to jump to the first insert point, then use i_ and escape to insert the underscore, then jump with fS, and use the . to repeat my last insert. Then when I am done with the string, I use guiw to change the case of the 'inner word' to lowercase (gu).

The nice thing about that approach is that it feels efficient: I am using all kinds of obscure Vim keystrokes to get my work done and I feel like a wizard. I never touch my mouse! The bad thing about the approach is that it is still a lot of work, especially in longer strings or with many occurrences. Today I thought: there must be a better way for this. Maybe there is even a plugin?

And yes there is, and of course it's by Tim Pope. It's called Abolish and it's main purpose seems to be auto-replacement (which I don't want to use) but it also adds a very handy :Subvert command, and precisely the mappings I wanted to have.

If I now every have to change any token to camelCase, I can just jump my cursor to it and type crc. Do I need snake_case? Just crs and that's it, no matter how long it is. And of course it works with the . command as well. Why have I allowed myself to do all the nonsense I just described above? Just install the tpope-plugin and you're done.

Last year I shared a Vim keybinding that I use quite frequently: I mapped gy to "+y, meaning that with the gy I yank text into the system clipboard (without the awkwardness of typing double quote and plus).

I recently added another mapping to it: if I do gY, it will actually yank the full content of the open file into my system clipboard. This saves me the awkwardness of typing gggyG. See my mapping below though: by using the command style yank, I actually don't let the cursor jump, which is much nicer.

nmap gy "+y
vmap gy "+y
nmap gY :%y+<cr>

Mindful boardgaming

So, two weeks ago I wrote about being competitive in boardgames. As I discussed there: I viewed myself as "not competitive", which to me meant that I did not mind losing, and that I would allow others to win if it clearly meant more to them than to me.

This meant that when I was ahead in the game, I would hold back and make smaller moves, just to even the playing field again. I would do this both consciously as well as unconsciously. And seriously: I would say sorry to the other players when I did end up winning. I was a bad winner.

The discussion of two weeks ago changed my mind about this: it is unfair to other players to not give it your all. It is also related to self love (a topic I have been exploring a lot in the past two months): trying to win means you can lose, and you should know that you are still an okay person when you do. Also it's okay to take up the space in a group when you are the winner: you won, you may be seen.

So in the past two weeks I've been trying harder to win, and it changed my experience in boardgames for the better. I didn't necessarily won more, but I am prouder of the wins I did get, and I didn't talk myself down afterwards ("sorry" or "it was just luck"). The wins felt like validation: I am good at games.

At the same time, the loses are indeed hurting a bit more. But I don't see that as a bad thing. I played Unfathomable and lost. But I also identified a few big mistakes in my way of playing the game. Because I was so invested in winning, the mistakes actually hurt, so I will for sure remember not to take on those strategies if I ever play it again. Actually trying to win the game makes you better at games.

That said, I just finished my first in-person Dungeons & Dragons session since 2020, and I really enjoyed it. This is a game that is not about winning at all: it can be endless and it's really just a form of collaborative story telling.

But here also, I made some "mistakes". I felt like I could've tried harder to come up with nice twists for the story (there was a lot of "sure I'll follow" and shooting arrows from a distance). Even though this game is not about winning, there is still a skill and a commitment to bring your best to it. It feels similar to what I call "being competitive".

Two weeks ago I chose the words "therapeutic boardgaming", but I really want to go for "mindful boardgaming" now. Enjoy the moment and give it your best, in that way, you get the best experience.

It's all fun and games

Today, a discussion spawned in a queer boardgaming Whatsapp group I am a member of, about the boundaries of cheating, the value of rules and about competitiveness and fun.

In general, I like to think of myself as 'not competitive'. To me this means I don't try to win in games, but to just enjoy the experience. In the discussion I shared that I sometimes make smaller moves when I have a big lead, to even the game a bit. Not everyone in the discussion liked this.

To give a bit more context: I play the game of Go and I am around 7 kyu. This means that if I go to a tournament, I have no chance of winning a top-3 position, but against someone who knows all the rules but hasn't played before, I have a chance of winning that nears 100%. That's not my style of winning.

To me, the experience of the game is just much more important than winning.

Until someone is holding back

Someone in the discussion said they found it unfair to let someone play with a handicap without them knowing it. I have never thought of my 'holding back' in this way, but I think they have a point. Players are doing their best and they expect me to play to my full ability as well. Holding back undermines the base of the game.

A story related to that: I was playing Ticket to Ride a lot with housemates and they were really fanatic about it. I could just never win: they always completed all their routes, they always went for the longer connections (those get more points) and in general they played efficient.

Then later I joined another friend group, who were already playing Ticket to Ride a lot. I joined their game and won by a huge margin, not just once but several weeks in a row. That is the kind of experience where I feel bad about winning.

But on the other hand: I only learned how to play well because the housemates did not hold back. And my friends also got better because I did not hold back in those first games. Not holding back makes everybody improve their understanding of the game.

The weight of winning

There is another part of not being competitive, which might have to do with the way I look at myself and others. In the past month, I have done a lot of reflecting on self-acceptance and feelings in general. I notice that not wanting to win also has a component of not wanting the attention that comes with it.

And it's not really attention I dislike, because I have it even more in a group I know very well and with people I value a lot. I think this is because the nature of being the 'winner' kind of places you above the rest of the group. It is this aspect I dislike.

But then again: if you agree to play a game, you agree that there will be a winner (depending on which game you pick, of course, but most games work this way). Someone has to fill that role at the end of it. For me personally, I think it would be good to explore my competitiveness a bit more, seeing what happens if I actually try to win.

Trying to win is a bit scary too, because if I actually try, there is still the chance that I loose. It is about valuing myself enough to say "yes I won, I am the best this time", as well as forgiving myself enough to say "I tried and lost, and I am still okay". Therapeutic boardgaming, I guess.

The obligatory post-FOSDEM post

This weekend I went to FOSDEM, an open source conference in Brussels, with Henrique and I thought I ought to blog about it. Let me prefix this with explaining that it was my first time, which meant I did not really know what to expect.

We quickly learned that attending a room wasn't as simple as just going there. There was an interesting talk announced for the Networking room, and after getting a coffee we went there to be just in time, only to find a giant queue of people in front of a door that said the room was full. This pattern repeated itself throughout the weekend.

As Henrique is a train nerd (yes) we got ourselves into the 'Railways and Open Transport' room, for talks about how to count passengers using open source software. It's great how this conference can provide such niche talks and get so many people interested in them. It was not easy to get into the Transport room.

I must admit that the niche also made me feel lost a few times. Everything is so zoomed in, it makes it hard to pick a place to attend, especially since getting into a room is always an investment. The trick is then to just pick a room that sounds interesting and just stay there, let the serendipity hit you.

Since nobody bought a ticket to the event, nor even did have to register, it is quite impossible to know the exact capacity of the event. It was really crowded, but that was mainly a good thing.

A few personal highlights:

  • a thing about state machines in the Erlang/Elixir room (which was next to Transport, so H. stayed there)
  • all the examples of how Liquid prompt provides a better thought-out presentation of information in the terminal
  • the idea that we should design our security features for a person who just had a baby and has a cat who pukes in the corner – we aren't paying perfect attention all the time
  • drinks in Delirium, but also the other nice foods, meeting nice people
  • the motivational talk by the guy who maintains curl, that piece of software that is in almost everything, including most apps, your car and on Mars
  • all the git commands I did or (mostly) did not know about
  • the various tweaks in my dotfiles or other workflows just because someone mentioned something
  • the weird LED-screen badge that everybody had and we finally bought too

It was also really nice to talk almost exclusively in Dutch with Henrique all weekend. It's really amazing how far he has come with learning the language and I am glad I could help him in his efforts by providing casual conversations with an occasional gentle correction.

So all in all, yes, this was a very good weekend and I would go again.

Toggling Github Copilot in Vim with unimpaired

I was trying to remove the Github Copilot configuration from my Vim setup, but then I noticed it was not there at all. I have been neglecting to commit changes to my dotfiles from my MacBook, as I did not have the courage to share stuff I was just trying out. On my new Linux-based Thinkpad I do in fact commit everything, as running open source stuff makes me want to work in public more too.

But since this config was only in the copy of my dotfiles that now lives on ~/dotfiles-mac, it will get lost now that I don't copy it. A good reason to blog about it.

First: unimpaired is a Vim plugin by tpope, and it is one that should just be part of Vim itself. It adds all kinds of mappings with the [ and ] brackets, and many of them I use daily (most notably [q, [e and [space). The o variant of this will change options, with the special yo binding to toggle the option. I use yoh all the time (toggles search highlighting) and also yol is very useful (shows invisible characters).

I wanted to be able to toggle Github Copilot for the current buffer in this same style. Luckily, the g was still available, and luckily, unimpaired provides an easy way to add new mappings. Unlucky as we are, copilot does not actually provide a toggle, but with Ctrl+R and = in insert mode, we can evaluate some internal options and print a string based on that. See the full command below.

" Going with that flow
Plug 'github/copilot.vim'

" Toggle github copilot
nmap <script> <Plug>(unimpaired-enable)g  :Copilot enable<CR>
nmap <script> <Plug>(unimpaired-disable)g :Copilot disable<CR>
nmap <script> <Plug>(unimpaired-toggle)g  :Copilot <C-R>=copilot#Enabled() ? "disable" : "enable"<CR><CR>

As for why I stop with Copilot? It was very useful in my time at Sneaker District, as I was the only developer working on the project and I wanted to be faster than I was on my own. Having a junior developer in my editor was a nice thing to have: it provided me with good suggestions that I usually had to edit a bit.

But I also noticed it slowed me down at times: I got into the habit of stopping to type to see what it would suggest. And sometimes I just stopped to think, and then it would give me suggestions for directions I did not want to go in. For now, I want to experience less distractions, and get into the habit of typing and thinking for myself again.

(There is also some money involved, and the question whether or not you want to send your code to Github for this purpose. It is nice to have those concerns gone, but it wasn't my primary one.)

Floppies

It just hit me. I learned how to code via an article in a Dutch school magazine (Taptoe), which explained how to create a website on a floppy disk. They were already old, but at least our computers had the drives still. The article just explained how to write flat HTML, but it was enough to spark this fascination that ended up being my job.

What hit me is that actually most websites these days won't actually fit on a 1,44 MB floppy anymore. (The content on this site already is more than 1 GB.) It would be a nice challenge again, to make a website fit 1,44 MB.

Related to this is the 10 KB Club, who try to keep their homepage under 10 KB. My site fails that test too, but at least the homepage fits a floppy.

Expanding the search in Vim

I just discovered a new Vim trick I think I am going to use quite often, so I am sharing it here to look it up when I forget.

As you may know, you can issue the / command in Vim to start a search. Every character you type will be in the search, until you hit enter, which will jump you to the first occurance of your searched text.

Another thing you may know, is that you can use this search-motion as a motion to other commands as well. When you do v/here, you will visually select everything between the current cursor position and the first occurance of 'here' in the buffer. I use this to delete stuff that I can't target using the f and t motions.

Then there is the thing I once found out, but never could remember: once you press /, you are in a little submode I can find the name for. In this mode, you can use Ctrl+L and Ctrl+H to increase or decrease your search string by the characters of the first match. You can also use Ctrl+G to move that virtual cursor to the next match, or Ctrl+T to get to the previous. Once you hit enter you are on that last position, removing the need to do n.

But here it comes: when you are deleting with d/, you can also use Ctrl+G to get to that next match. If you searched for a small snippet that accidentally occurs before the place you wanted to go, you can press Gtrl+G to jump over it, and pressing enter will delete the full distance.

Using PHPStan to fill Vim's Quickfix list

I am using PHPStan and the ALE plugin to add some error checking to my Vim. It gives red arrows on lines that contain errors in my currently opened files. But sometimes, in a big refactor, I want to know all errors in my project.

Vim has a build-in feature for this: the quickfix list. It is designed to take the error output of a compiler and lets you jump to all those locations with the :cnext and :cprev commands. I personally use the essential Unimpaired plugin by the one and only tpope, which maps these to ]q and [q.

I use these a lot: the :grep command fills the quickfix list with all the occurrences of your search, like normal grep but with pagination (or, if you set your grepprg to something faster: like ripgrep with pagination).

To get PHPStan to fill this quickfix list, I looked for plugins, but they all seemed hairy. I was convinced this should be simple, and it was. The following leader mapping seems to just work:

nmap <leader>pa :cexpr system('vendor/bin/phpstan analyse --no-progress --error-format=raw')<cr>

Customising Git: some things I did

One thing that always puzzled me a bit about my own workflow, is that almost all of it is based in the terminal (I use Vim and Tmux), except for Git: where most people seem to use the Git CLI commands, I use a graphical program (Fork, which is quite good).

Another thing then: I never used Github professionally, apart from the time I was a self employed web developer, but back then I was the only developer on my projects. All my previous jobs had a self-hosted Gitlab running somewhere.

Long story short: I am trying to get better at Git in the terminal and using Github. And 'better at Git' to me both means 'being able to confidently rebase' as well as 'customise my workflow'.

Aliases in the Gitconfig file

Customising Git means setting configuration in the ~/.gitconfig file. This file contains settings for Git, like your name and email, but can also be used to add aliases. To create the first alias you can run git config --global alias.co checkout. After this, you can use git co as git checkout, which is shorter to type and yes I use this often now.

Another alias I have is this one:

publish = !git push --set-upstream origin $(git symbolic-ref --short HEAD)

If you try to push a branch that has no linked branch on Github (the upstream), Git will complain about it. It will be nice to you and state the command you should have ran, but I got tired of having to copy and paste that new command.

fatal: The current branch feature/new-shoes has no upstream branch.
To push the current branch and set the remote as upstream, use

    git push --set-upstream origin feature/new-shoes

In Fork, this was just a checkbox away. With my new git publish command I get the convenience again: it will push the branch and set the upstream with the same name as I have locally, exactly as Git suggested I should have done, but in less typing.

Links to Github

So far we have seen two kinds of aliases: one that just aliases a simple Git command (b = branch) and one that actually ran a shell command, because we started it with a ! (note that you will have to start with ! git there). But there is another way: having a command that starts with git- in your path.

I have the following file as ~/bin/git-github, marked as executable (chmod +x ~/bin/git-github) and in my path (export PATH="$HOME/bin:$PATH" in my ~/.zshrc file):

#!/bin/zsh

local url
url=$(git remote get-url $(git remote))
url=$(echo $url | sed 's/.*github\.com[:\/]\(.*\).git$/https:\/\/github.com\/\1/')

if [ -n "$1" ]; then
  append="/$1/$(git symbolic-ref --short HEAD)"
fi

open -u "$url$append"

Yes, my ZSH is crude. Yes, I can better share this in Bash. Yes, it could've probably been on one line and be included as an alias in my Gitconfig. But it works for me.

The command figures out the Github URL of the project, based on the URL of the remote (it assumes you have only one). It then opens that URL with the macOS open command. If an argument is given, it appends that to the URL, with the name of the branch too.

In my Gitconfig I have two aliases that use this command:

pr = github pull/new
compare = github compare

With git github, my default browser will open a tab with the 'homepage' of the repository. With git compare, it will open a tab that contains a diff of the current branch and the default branch on Github (the remote versions of those). With git pr, the browser will open the correct page to open a for the current branch.

And you mentioned Vim and Tmux?

Yes, I actually use the obligatory Tim Pope plugin Fugitive. This means I can do a lot of things which you can also read about in the help file. (Which I read a lot these days.)

But this allows me to stage and commit and rebase and reword all my changes in Vim, and then when I am ready, run :G publish and :G pr and make a PR for it on Github. These two commands alone make me feel so much more productive: no longer do I have to search for another program to compose commits and then search for the browser to handle the cooperative side of it... I just handle everything in my editor.

For Tmux I have another nice addition: in my ~/.tmux.conf I have the following command:

bind-key y display-popup -E -h "90%" "git log --oneline --decorate --graph --all"

This will – when I press <prefix> + y – open a popup window, which closes when the command exits and has a certain height. It will show me an ASCII-art style graph of all the commits – one of the features I missed from Fork – as an overlay over my editor, a small keystroke away.

With this configuration – and a lot of reading the Fugitive help file – I feel much more at home with Git in the terminal.

I also watched this fantastic series on mastering Git and these unfortunately dated but still useful screencasts on Fugitive.

Storing posts by juggling with Git internals

I have been wanting to rework the core of this website for a couple of years now, but since the current setup still works, and since I have many other things to do, and finally since I am very picky about how I want it to work, I have never really finished this part at all. This makes me stuck at the save version of this site, both visually as behind the scenes.

Now that I am in between jobs I wanted to work on it a bit more, but I still do not have time enough to fully finish it. I guess it all comes down to a few choices I have to make regarding trade-offs. In order to make better decisions, I wanted document my current storage and the one I have been working on. After I wrote it all out I think I am deciding not to use it, but it was a nice exploration so I will share it anyway.

The description heavily leans on some knowledge about Git, which is software for versioning your code, or in this case, plain text files. I will try to explain a bit along the way but it is useful to have some familiarity with it already.

tl;dr: I did fancy with Git but might not pursue.

How it is currently done

At the time of writing, my posts are stored in a plain text format with a lot of folders. It is derived from the format which the Kirby CMS expects: folders for pages with text files within it, of which the name of the text file dictates the template that is being used to render the page. In my case, it is always entry.txt.

I have one folder per year, one folder per day of the year and one folder per post of the day. In that last folder is the entry.txt and some other files related to the post, like pictures, but also metadata like received and sent Webmentions.

An example of the tree view is below. It shows two entries on two days in one year. Note that days and years also have their own .txt file that is actually almost empty and pretty much useless in this setup, but still required for Kirby to work properly. The first day of the year my site is broken because it does not automatically create the required year.txt (or did I fix that finally?).

./content
└── 2022
    ├── 001
    │   ├── 1
    │   │   └── entry.txt
    │   │   ├── .webmentions
    │   │   │   ├── 1641117941-f6bc3209f3f33f0cb8e4d92e5d46b5090b53aa11.json
    │   │   │   └── pings.json
    │   └── day.txt
    ├── 002
    │   ├── 1
    │   │   ├── some_image.jpg
    │   │   └── entry.txt
    │   └── day.txt
    └── year.txt

Also note that there is a hidden .webmentions folder which contains a pings.json for all the sent webmentions and a JSON file with timestamp and content hash in the name for every received webmention. Not in the diagram but also present are some other folders for pages like ./login/login.txt (because that is how Kirby works) and ./isbn/9780349411903/book.txt (for books).

All these files are stored in a Git repository, which I manually update every so often (more bimonthly than weekly, sadly) via SSH to my server. I give it a very generic commit name (’sync’ or so) and push to a private repo on Github, which takes a while because the commits and the repo contain all those images and all those folders.

What is wrong with this

The main point of wanting to move off of this structure by Kirby, is that it requires those placeholder pages in my content folder. I have no need for a ./login/login.txt: the login page is just a feature of the software and should be handled by that part of the code. But at least that file contains some text for that page: the files for year.txt and day.txt are completely useless.

Another point is that I want to make the Git commits automatically with every Micropub request: Git provides me with a history, but only if I actually commit the files once I changed them. Also, if I do not push the changes to Github, I have no backup of recent posts.

The metadata of the received and sent Webmentions are now also available in the repo. This is nice, as it stores the information right next to the post it belongs to, but on the other hand it feels kind of polluting: these Webmentions contain content by others, where as the rest of the content is by me. There is some other external content hidden in the entry.txt file but I’ll get to that later.

The last point is that the full size images are stored in the repo and every book and article about Git says that you should not use it to store big files in it. Doing a git status takes a while and also the pushes are much slower than any other Git repository I work with.

Git history: the Git Object Model

Before I go further into the avenue I am taking to solve the problem, I need to explain a bit about the Git Object Model, also known as ‘how Git works under the hood’. For a more thorough explanation, see this chapter in the Git Book.

As you’ll learn from that chapter, every object is represented as a file, referenced by the SHA1 hash of its contents. And there are three (no, four) types of objects:

  • blobs, which are the contents of files tracked by Git (and thus also the versions of those files)
  • trees, which are listings of filenames with references to blobs or other trees. These trees together create the file structure of a version.
  • commits, which are versions. A commit contains a reference to the root tree of the files you are tracking, a parent commit (the previous version) and a message and some metadata.
  • tags, are not mentioned by the chapter, but do exists: these look like commits, but create a way to store a message with a tag (making annotated tags, I’ll explain plain tags soon).

Note that Git does not store diffs, it always stores the full contents of every version of the file, albeit zlib compressed and sometimes even packed in a single file, but let’s not get into that right now.

Git’s tags and branches are just files and folders (they can have / in their names) which contain the hashes (names) of the specific commits they point to. The tags can also point to a tag object, which will then contain a message about the tag (which makes them ‘annotated tags’).

This all brings me to the final point about my storage: for every new post, Git has to create a lot of files. First, it needs to add a blob for the entry.txt, possibly also a blob for the image and blobs for other metadata. Then it needs to create a tree for the entry folder, listing entry.txt and if present the filenames of the images and metadata files. Then it creates a new tree for the day, with all the existing entries plus the newly created one. Then it creates a new tree for the year, to point to this new version (tree) of the day. Then it creates a new tree for the root, with this new version of the year in it. And finally it also needs to create a commit object to point to that new root tree. Every update requires all these new trees. The trees are cheap, but it feels wasteful.

Also note that a version of a file always relies on the version of all other files. This is what you want for code (code is designed to work with other code), but it does not feel like the right model for posts (I might come back on this tho).

And there is also the question of identifiers: currently, my posts are identified as year, day of year, number (2022/242/1), but especially that number can only be found in the name of the folder and thus in the tree, not in the blob. I have not yet found a good solution for this, but maybe I am seeing too many problems.

The new setup

To get rid of some of the trees, I tried to apply my knowledge of the Git Object Model to store my posts in another way. To do this, I used the commands suggested by the chapter in the Git Book in a script that looped over all my files, to store them in a new blank repo to try things out.

For each year, for each day, for each post, I would find the entry.txt and put the contents in a Git blob with git hash-object -w ./content/2022/242/1/entry.txt. The resulting hash I used in the command git update-index --add --cacheinfo 100644 $hash entry.txt to stage the file for a new tree. I would do that too for all images and related files, and then I would run git write-tree to write the tree and get the hash for it and git commit-tree $hash -m "commit" to create a commit based on it (with a bad message indeed). With that last hash I would run git update-ref refs/heads/2022/242/1 $hash to create a branch for that commit. (I contemplate adding an annotated tag in between, for storing some metadata like ‘published at’ date.)

This would result in a Git repository with over 10,000 branches (I have many posts) neatly organised in folders per year and day. When one were to check out one of these branches, just the files of that posts will appear in the root of your repo: there are no folders. When you check out another branch, other files will appear. This is not how Git usually works, but it decouples all posts from one-another.

Multiple types of pages

The posts I describe above all follow the year-day-number pattern because they are posts: they are sequential entries tied to a date. There are other objects I track, though, that are not date-specific. One example is topical wiki-style pages: these pages may receive edits over time, but their topic is not tied to a date. (I don’t have these yet.)

Another example is the books that I track to base my ‘read’ posts off. I haven’t posted them in a while, but I would like to expand this book collection to also include other types of objects to reference, such as movies, games or locations. These objects also have no date to them attached, at least not a date meaningful to my posts.

I could generate UUIDs for these objects and pages, and store branches for those commits in the same way Git does store it’s objects internally, with a folder per first two characters of the hash (or UUID) and a filename of the rest:

./refs/heads
├── 0a
│   ├── 8342d2-d6f1-4363-a287-a32948d04eaa
│   └── edcb13-433c-48d2-b683-a407c3a88f57
└── 3d
    ├── 243a27-114e-4eee-9bd8-2a51b01939e6
    ├── 25965b-2da5-422d-abce-f3337fa97fc4
    └── 611b59-499a-48a0-b931-afe06192e778

I could even reference the same post/object with multiple identifiers this way. Maybe I want to give every book a UUID, but also reference it by its ISBN. The downside to that, however, is that I need to update both branches to point to the same commit once I make an update do the book-page.

Drawbacks of the approach

The multiple identifiers are probably not feasible, but there are some other drawbacks too. My main concern is that it is much harder to know whether or not you pushed all the changes: one would have to loop over all 10,000+ branches and perform a push or check. In this loop you would probably have to check out the branch as well. It is of course better to just push right after you make a change, but my point is that the ‘just for sure’ push is a lot of work.

Another drawback is actually the counter to what I initially was seeking: wiki-style pages might actually reference each other, and thus their version may depend on a version of another page. In this case, you would want the history to capture all the pages, just as the normal Git workings do. My problem was with the date-specific posts, but once you are mixing date-specific and wiki-style pages, you might be better off with the all-file history.

One problem this whole setup still does not solve is that of large files. The git status command is much faster for it does not have to check all the blobs in the repo to get an answer, but the files are still in the repo, taking up space. And there do exist other solutions for big files in git, such as Git LFS, the Large File Storage extension.

Also, I am still not 100% sure it is a good idea to store metadata in the Git commits and tags. When we already store the identifier in the tree objects, I thought I could also add the ‘published at’ date into the commit. Information about the author is already present, and as my site supports private posts, it also seemed like a reasonable location to store lists of people who can view the post. But again, maybe that should be stored in another way, and not be so deeply integrated with Git.

Conclusion

It was very helpful to write this all out, for by doing so I made up my mind: this is just all a bit too complicated and way too much deeply coupled to Git internals. I would be throwing out the ‘just plain text files’ principle, because I would store a lot of data in Git’s objects, which are actually not plain text, since they are compressed with a certain algorithm.

My favourite Git GUI Fork is able to work with the monstrous repository my script produced, but many of the features are now strange and unusable, because the repo is so strangely set up. I would have to create my own software to maintain the integrity of the repo and that could lead to bugs and thus faulty data and maybe even data loss.

I still think there are some nice properties to the system I describe above, but I won’t be using it. But I learned a few new things about Git internals along the way, and I hope you did too.

Quickly look up PHP docs from Vim

As I don’t use a full IDE like PHPStorm, I don’t get much help with the parameters to function calls from my editor. On one hand I find this a good thing: IDE users rely so much on autocomplete that they don’t remember names of things at all. On the other hand: who has the mental space to waist on such things?

My middle ground is that I look up a lot of things on PHP.net. They make that very simple: just add the name of the function you are looking for after the slash and they will give you the correct documentation. But: switching to a browser and typing out the address requires a lot of keystrokes. I found a solution.

nmap <Leader>pd :silent !open https://php.net/<c-r><c-w><cr>

This adds a leader key mapping to look up the word (so: function name) under the cursor. I prefix all my PHP related leader mappings with a p, but feel free to pick something else.

The :! runs a shell command, in this case open, which on Mac can be given a URL, which will then be opened in the default browser. I added silent to ignore the output of the command: I just want to open a URL.

As the URL, I let Vim type out the address to PHP.net, and since we are in command mode, one can do Ctrl+R, Ctrl+W, which will paste the word that is currently under the cursor (very nice to know in itself). We end the sequence with an enter (carriage return) to run it.

So the tip within the tip is Ctrl+R (register) Ctrl+W (word). In general Ctrl+R in insert mode gives you this interesting ‘paste from register’ mode that is good to know. See :help i_ctrl-r and :help c_ctrl-r for more.

When @hacdias.com posted about our conversation about post topics I couldn’t stay behind to also formulate my part of it in a blogpost.

Currently I have various feeds for various post types. I don’t want to link them all here, in case I want to change them around, but I have different feeds that only show my likes, my photos, my replies, etc (you can probably guess the URLs).

These feeds are relatively easy to set up: does it have a photo? Then it’s a photo. Does it have a title? Then it’s an article. This post doesn’t have any, so it’s a note. I have a few of those rules set up and they fill these pages.

But when you scroll through my photo feed, you will also see drawings. When you scroll through my notes, there are various topics represented. It is not that bad right now, but that is mainly because I don’t post as much as I could, because I don’t want to bore my readers with topics they don’t want to follow.

On social media, we live a siloed life, and the people on the IndieWeb are trying to bring that all back to their own site. But, in the siloed life, we can pick the silo for the post. ‘Insta is for friends, Twitter is more business, Reddit is shitposting’, something like that. Sometimes the silo is aimed at a certain kind of post, sometimes it is just the kind of bubble you created for yourself on that silo that makes you post a certain way.

On the IndieWeb, I have only one site. Of course I can get multiple – I have – but I like having all my posts in one place. But I also want to give people options for how to follow me, different persona to share posts with.

I do have tags but most are not that useful. Most of them only contain one post, and also, most of them are very specific. I like the indieweb and vim tags, for they are quite topical, but those are exceptions.

At one point (not now) I would like to divide posts up into probably five rough categories. The homepage might still show a selection of all, and there will also be a place to actually see everything, but I think these categories make sense to me:

  • professional / helpful for all those posts in which I share something about IndieWeb, Vim, something about programming, something I learned
  • personal for stories about what happened in my life, maybe also some tweets, the more human connection
  • too personal for checkins, books I’ve read, food I’ve eaten, movies I’ve watched, still about life but without commentary
  • art for those good pictures, occasional drawings, fiction stories, the things I post too little

I said five and I posted four, because I don’t think this is final. I might also want to add a ‘current obsession’ category, to blog about those things I am deeply into. (There has been posts about keyboards here, you missed Getting Things Done, currently I am into the game of Go again.)

A last category I might also need is ‘thinking out loud’, as this is a post that would fall into that. For what is worth, I’ll post it anyway.

Faster copy to clipboard in Vim

tl;dr: I just mapped Y to "+y and I am very pleased with it.

A coworker saw me copying some text out of Vim and humored me: everything seemed to happen like magic in my editor, but for a simple thing as copy and paste I needed a lot of keys.

It is true: in order to copy text within Vim, you can use the y command, combined with the motion of what you want to yank. So: yiw will yank inside a word, ggyG will go to the top of the file and then yank until the bottom (so, the whole file), yy will yank a full line. But these yanks are only pastable (with p) within Vim itself.

In order to get text out of Vim, you need to use a special register. Registers are a sort of named boxes, letters a to z, in which you can put snippets of text. To use it, you prefix your yank (or delete) with a quote: "ayi( will select register a and then yank the text within parentheses. To get to your system clipboard (the one all other programs use), you select the special register with "+.

Thus: my coworker saw me hunting for the "+y combination, probably. I almost always look at my keyboard when I do that, so awkward is the combination. But I just found a solution: Y.

nmap Y "+y
vmap Y "+y

By default, Y does a yy, which I never use, because it's inconsistent with other commands. D, for example is equivalent to d$, delete until the end of the line, same for C as c$. I guess it makes Y play along with V (line-wise visual select) and S (subsitute full line), but I always use yy anyway, so Y is free to use. When I now want to yank to my system clipboard, I just use Y instead of y and that's it.

You can also consider adding this as gy, which does not have a meaning, but g combines with various commands to activate variation of their meaning, so it's not a bad choice either. I mapped it to both and will see which one sticks.

Adding a prefix to TailwindCSS classes with VIM

Today we needed to add a TailwindCSS prefix to a small project we are building, to prevent clashes with other TailwindCSS classes on the same page. The setting in the tailwind.config.js was straightforward, we added this:

module.exports = {
  prefix: 'sf-',

... and now TailwindCSS will generate classes like .font-bold and .border-none as .sf-font-bold and .sf-border-none. Next up, we needed to replace all the classes in the project with their prefixed counterpart.

There were some plugins available to do this automagically in Webpack, but we decided we wanted to add them manually. A few of the classnames are dynamically set, and we doubted the plugins would find all those cases. Also: the plugins seemed to be old.

My co-worker started writing a regex for a search-and-replace, but soon stranded. You need to find the proper locations, and then within those locations, add the prefix. The prefix, however, is sometimes more of an infix, since there are other prefixes that are added before the prefix itself. See the class with md: for example:

<div className="bg-gray-100 px-4 md:px-0">
// Becomes:
<div className="sf-bg-gray-100 sf-px-4 md:sf-px-0">

While he worked on the regex and subsequently went to get coffee, I thought: I should be able to solve this in VIM in some way. I know I can make a visual selection and then type :! to get the selection as input into some shell command. If I pipe the lines to some script, I can then just split strings all I want.

One problem I ran into was that, if I make a visual selection within quotes (vi") and I then use the colon-exclamation to pipe it to some script (:'<,'>! php transform.php), I will receive the full line, as if I was using the line-wise V selection.

While searching the internet I thought: can I use a register here? And the internet had a solution for that. I ended up running the following command:

:vmap m "ay:call setreg('a', system('php transform.php', getreg('a')))<cr>gv"ap

This sets up a mapping for visual mode, and binds the m to do a series of things. It first selects the 'a' register and yanks ("ay). Since we are already in visual mode here, y is a direct yank of that selection. We are now in normal mode, so we continue to type out a command that calls a function to set register 'a' again with the output of a system call to php transform, taking in the current contents of register 'a'. We execute this command by pressing enter (<cr>). Register 'a' now contains the scripts output. We then re-select the last visual selection (gv in normal mode) and then paste from register 'a' ("ap). This effectively runs the script over the visually selected area.

I then just opened the alphabetically first file of the project and manually made visual selections that selected all the classnames. For this, I also temporary mapped m to vi", so I could just do mm once my cursor was in a className="..." (for which mouse mode is nice too: click + mm, very fast). Then, to get to each next file, just press ]f.

The project was small enough to do this last part so manually, but this at least saved me a lot of stops in between all the classnames, and also saved me the headache to scan for md: prefixes and pick the right spot. Even when the classnames were actually in a backtick-string it was no problem: as long as I defined a correct visual selection, m would replace things.

For completeness, here is transform.php:

<?php

$input = file_get_contents('php://stdin');

$classes = array_filter(explode(' ', $input));

$classes = array_map(function ($class) {
    $parts = explode(':', $class);
    $parts[count($parts)-1] = 'sf-'.$parts[count($parts)-1];
    return implode(':', $parts);
}, $classes);

echo implode(' ', $classes);
als antwoord op Henrique Dias

Nice to hear you are enjoying the series! (It turns out to be a series.) It also feels really good to have finally found a form again where I can cycle between writing code and writing blog posts about the code.

Looking forward to read your article && yaay it worked!

Edit: lol, next topic really is Webmentions: I had to manually send this one because I am certainly not parsing HTML with regex (I would never) and your rel has no quotes around it.

Next up must be Webmentions, I feel. I just posted this article with a lot of backlinks to old posts, but since I send all Webmentions in a synchronous way on first-post-visit, it took ages, and I actually got a 502 Bad Gateway out of my site. But I also need weekend, so we will see.

IndieAuth without IndieAuth.com

In February 2017, I added the ability for other people to log in to my site, both via IndieAuth and Twitter. Since June 2019 this feature is more useful, as I moved all my checkins to being ‘private’, meaning there is actually something to see (albeit sparsely posted).

For this, I used the now outdated and deprecated service IndieAuth.com, which back then was also used to log in to the IndieWeb wiki. Due to the confusion between IndieAuth.com (the service) and IndieAuth (the protocol), a new project called IndieLogin was started and the IndieWeb wiki moved to that one. Since it didn’t offer that login service to others, as IndieAuth.com did, I never switched.

Since this week I have a new IndieAuth endpoint for myself. (Meaning: a place I can use to prove my own identity to others.) Unfortunately, it is not compatible with the outdated IndieAuth.com, so I could not use it to prove my identity to my own site. Because this felt wrong (and because I had the IndieAuth flow in my head again now), I decided that my site needed to do that part of IndieAuth on it’s own as well.

Since I was at it, I also have Twitter login working via the new flow. That was easier, because the library support was more plug-and-play.

Juggling with cookies

Last week I wrote about my new setup where I run two applications, the old PHP and the new Elixir, behind an unusual NGINX configuration. For the aforementioned feature of authentication, I had to create even a bigger beast.

All the posts, and all the logic for showing or hiding private posts, are still in PHP. The new authentication is in Elixir. Both ends are storing the information about the current logged-in user in session cookies. How do I merge those two into one user experience?

Luckily, since both applications run under the same domain, the browser is actually unaware of the fact that there are two apps. It just sends all cookies back.

So, in PHP I ask for all headers, look for a cookie-one that is named _yak_key, and if it is there, it makes a direct localhost call to Yak, the Elixir server, with that cookie. Yak exposes one route that just returns the currently logged-in user as a JSON. If that call returns a user, PHP can set a cookie of its own.

Then for the logout, PHP used to redirect to the homepage, but now it redirects to the page that logs you out of Yak, which redirects to the homepage.

At some point this setup will change again, but for now it seems to me as a good way to make one further step in the features I want to support in the new version of my site, while keeping the old one running.


Oh yeah, you may hunt down that localhost-call that checks the login status. It is actually exposed to the outside world too. But if you can provide it with a cookie for a person that is not you, that person and I have a problem anyway, regardless of you knowing the endpoint.

Meer laden