Skip to content

Latest commit

 

History

History
2380 lines (2003 loc) · 157 KB

posts.org

File metadata and controls

2380 lines (2003 loc) · 157 KB

Content

Hosting a Valheim Plus server on a VPS with Podman and docker-compose

One of the best parts of being a self-hoster is being able to host a game server for your friends, right? Well, yes but also it can go sideways and not only because you’re a system admin for more than yourself. There are many ways but Podman, docker-compose, and Syncthing can help!

Unwarranted exposition

I plan to write a getting started guide at some point but after doing a bit of maintenance to get everything working again I wanted to share some quick thoughts. I did not want to expose Docker to the external web because running Docker as root is easier but also dangerous since root inside the container is root on host – took a bit to really understand that. There are (involved) ways to run Docker rootless and other security measures but after Docker pulled some shenanigans and Podman being almost fully API compatible, it was an easy choice to just switch to Podman.

Valheim

Valheim is a game where you explore, build, craft and conquer in a procedurally generated world inspired by Norse mythology.

Valheim is stunning game that really captured my attention and was an excellent escape during a long winter a few years ago, I describe it as Viking Minecraft. The game supports multiplayer and people have figured out how to run it headless so there’s a persistent world on a server allowing people to play and build asynchronously. It’s quite fun to play with some friends when a simple resource gathering trip turns into a harrowing boondoggle. However, there are a lot of small quality of life improvements that could make the game more enjoyable. These (and many more) things have coalesced into a mod-pack called Valheim Plus.

Valheim Plus

Valheim Plus can live alongside the game and I first used it locally for my solo game. But when a group of friends wanted to play I suggested we keep our options open to enable Valheim Plus and selectively tweak the game. All of that finally led me to this excellent container stack.

The Setup

I went through a couple of different approaches but I’m happy with a docker-compose.yaml, a valheim.env file, and the valheim_plus.cfg file to fully define the “server”. It is a bit annoying that all three files in are their separate file types but that’s just modern development.

Documentation and Version Control

I use a private git forge, Forgejo, for my self-hosted infrastructure so I can commit secrets in .env files and not worry about leaking them accidentally. I wrote up a readme file, screenshot included to show how nice self-hosted Forgejo is and how it pays to do documentation for your own sake.

Quick points on each piece of the tech stack, I hope to expound in a planned article on “self-hosting on an affordable VPS”, feedback and topics to cover are very welcome.

Podman

Podman installation is pretty straight forward. I suggest creating a separate user on the server who owns all the containers. If you’re familiar with docker, you can replace all docker commands with podman and it’ll work a treat, Redhat also has a nice article.

Docker Compose

Podman uses an equivalent… podman-compose, I appreciate them not being cute about it and going with a familiar setup. I can share the docker-compose file as is, since I’m using the .env file to store all secrets.

  version: "3.1"

services:
  valheim-mod-prod:
    image: ghcr.io/lloesche/valheim-server
    cap_add:
      - sys_nice
    volumes:
      - ./config:/config
      - ./data:/opt/valheim
      - ./valheim_plus.cfg:/opt/valheim/plus/BepInEx/config/valheim_plus.cfg:ro
    ports:
      - "2456-2458:2456-2458/udp"
      - "9001:9001/tcp"
    env_file:
      - ./valheim.env
    restart: always
    stop_grace_period: 2m
    networks:
      - reverse_proxy

networks:
  reverse_proxy:
    name: reverse_proxy
    external: true

A couple things to point out:

  • The Valheim Plus documentation will tell you to place the valheim_plus.cfg file in the BepInEx folder (after downloading the and unzipping the mod) but that made version controlling it a bit janky so I map it directly into the location inside the container where it’s expected.
  • Also note the “:ro” at the end, it’s mounted internally as a read-only file. You will see messages in the log stating it’s read-only, that’s not an error and it works just fine. The game will download a fresh config from GitHub if there’s no file present so it’s best to make it read-only so it doesn’t get overwritten accidentally.
  • networks: reverse-proxy refers to Caddy that is proxying the requests coming to the Supervisor (running on port 9001), which is a small web-app that allows control of the pieces of the tech stack.

Version Control

With this setup you only have to version control the three files mentioned before: docker-compose.yaml, valheim.env (don’t share publicly), and valheim_plus.cfg (I also suggest creating a README.md with details of your specific setup and version controlling that too).

Syncthing

Syncthing is probably the odd-ball part here, it’s basically the easiest way for me to create an “on-site backup” for my “off-site server” (VPS). This really isn’t in scope but wanted to throw it out there as a cheaper place to backup those frequent and bulky (by cloud storage standards but light for home) backups that the container stack creates.

Takeaway

There are great tools out there to self-host things for fun and to be in control of your data (more on that in the future too). But being able to recreate, reproduce, with reliability requires a little planning and some documenting.

This blog doesn’t have comments but happy to discuss further on Mastodon.

To our own detriment

This post alleging extortion from CloudFlare is plausible/cautionary. Market monopolies/tech mono-cultures suck so it’s tempting to just be outraged at CF.

However, we allow this when we cede control to third parties (not just cloud) without mitigation plans, by NOT:

  • using open standards/formats
  • separating concerns
  • staffing expertise in-house/independent long-term partners
  • balancing risk with cost

Let’s have that bigger talk!

Note: Originally posted on Mastodon, any conversation will be there.

Enshittification protests beget more enshittification

Like many others, I am frustrated by corporations that have ingested immense amounts of user generated content and are using/selling it to train LLMs[fn:2]. However, deleting the content now is counter productive, let’s protest more effectively by building a better web.

Context and Concern

I’m seeing many influential people on the Fediverse urging others to go register their displeasure by deleting the knowledge they contributed via answers and comments. This is especially puzzling in the case of the current Stack Overflow and OpenAI partnership announcement that has caused blowback on Mastodon[fn:1] because these folks are tech savvy and should understand that deleting their contribution from showing up on the front-end does not delete the data. It’s a bit crazy making because I feel like I’m missing something here, am I?

Please don’t advise people to delete their existing answers. Not contributing new answers and not logging in/deleting your account are great ways to register protest but deleting data is self-defeating. These companies already have the data backed up and will use it anyway, deleting answers only hurts humans who are searching for the answers through traditional searches. Removing the information by deleting it or issuing GDPR requests only locks up the knowledge and makes it exclusive to the “AI”.

Potential Solutions

A dramatic analogy before we get to solutions… If a marauding force is razing your city and driving you away, you absolutely shouldn’t come back to help them rebuild the city where you can’t live anymore. But you also don’t burn your books in the fire on the way out. You take your books with and build a better city with fortified libraries!

Just in case it’s not clear, I’m not siding with the platforms here. Even if they have a legal right to the use the data as they see fit, they’re violating trust with the community by going against its wishes. And in the case of Stack Overflow all contributions are CC-BY-SA so anyone can use the data provided they follow the attribution requirement[fn:3].

Instead of letting a good crisis go to waste, let’s use this (as yet another) impetus to leave walled gardens and join community spaces. Instead of encouraging others to delete answers, direct them to community forums, write the answer on your own website and post a link to the platform (POSSE style), stop contributing further to closed platforms and liberate existing data by using export tools where available.

I hope we can learn these lessons from the on-going Enshittification and start taking back the web instead of furthering Enshittification by protesting it ineffectively.

Discussion: This site doesn’t have comments but happy to chat via this Mastodon post.

Footnotes

[fn:3] Citing attribution would be difficult at best and while I’m not a lawyer I can’t see how their response could help legally. [fn:2] Large Language Model, falls under the umbrella of the colloquial Artificial Intelligence (AI) terminology. [fn:1] As of 2024-05-07, I don’t want to single out individual posters.

In appreciation of podcasts

I’ve seen several posts and threads lately on the fediverse that take a rather pessimistic view of podcasts and advocate for written media instead. I guess here’s my hot take about it because no one asked!

You know what? Transcripts would be great and having them integrated into the podcast app like synchronized lyrics, even better. Being able to quickly refer to the part that I didn’t catch or copy a piece of information to save as a note, would be fantastic. I’m excited for all these types of features coming to podcast apps. Multi-track media, like video with subtitles is great for everyone and helps with accessibility.

However, there’s a lot of “this could have been an email” energy in a lot of these conversations. And a lot of straight up hate about tech bro podcasts… it makes me question why these folks are listening to that content, I don’t want to read the crap content I wouldn’t want to listen to either. I’m sure there must be a lot of atrocious podcasts out there for people to have such strong opinions. Luckily I’m not familiar with them and even if I were, I would just move on instead of crapping on the medium as a whole. There’s a lot of equivalence of audio and text and I would rather have text because that’s easier to skim. Sure, no argument about the skimming part. But audio is a different (and richer) medium than text and good podcasts lean into the strengths of the medium; tone, tamber, pacing, ambiance, relative loudness, etc. I’m not just talking about radio drama, I’m also talking about news, conversational, and reporting podcasts.

There’s no substitute for good content (I mean that in the definition of the word not content-creator sense) but audio is a great medium so I’m defending that in text form so it might actually reach folks that will not listen to a podcast about it. I would love for podcasts to have transcripts and make all media more accessible. But hey Podcasters, please keep making great podcasts with excellent audio mixing and production, I appreciate your craft!

Starting a webring in 2024

I’m happy to see that a lot of people, both regular and “content creators”, are relying less solely on commercial social platforms. But discoverability is still an issue, maybe taking back the web requires using ancient tools like webrings.

The idea of POSSE is very appealing from a data sovereignty perspective but you’re still relying on third-party platforms for discoverability. If only there was a way to discover others from each other… well there used to be: webrings. The basic premise was, you land on a site(D) and you were pointed to the previous(C) and next(E) sites in the ring. A random site button was also common.

An easier and older(?) concept is a “blog roll”, where you just list the other sites you find cool/interesting on your site somewhere (typically in the side-navigation bar which was as common design pattern). However, the downside with blog rolls is that you have to maintain it manually and it could grow quite long. As a reader, it’s hard to choose which site to visit; it’s a real judging a book by it’s cover dilemma.

The previous/next links in a webring kind of solve both those problems, especially if there is an index page that one can visit to see all the sites. But the biggest downside is that it typically requires an active server running “code” to route the traffic (because manually linking to sites would require constant upkeep) and if a site(C) ceases to exist or removes the previous/next links then it breaks the ring. The other issue is that if there’s a popular site in the ring(B) then only the adjacent sites (A & C) benefit from the additional 👀.

So what are some mitigations we can apply to this webring model?

  1. Don’t run “code” on each site or a central server
  2. Give sites an easy way to link previous/next sites (minimal technical ability required)
  3. Make sure that all sites actually exist and link correctly to their adjacent positions
  4. Change the order of the sites that link to each other to facilitate more discoverability

Well, turns out that Ringfairy actually address all of these:

  1. It uses statically generated redirect pages for each node in the ring
  2. Individual sites only need to insert three (previous/index/nextg) regular url links
  3. It audits sites and excludes missing/malformed sites during build to preserve ring integrity
  4. It can shuffle the site order each time it builds the ring

In my quest to just do things and learn from them instead of wishing it existed and with encouragement from the System Crafters Community, I started Craftering. Of course this site is a part of the ring and you can explore others with the previous/next links in the footer. I can say for certain that my site is not (B) but it’s been fun to bring others into the fold. We’re collaborating via a Codeberg repo and we’ve had a few people who made their first contribution to an open source repo and/or their first Pull Request(PR), it’s rewarding to be a part of that milestone. A couple first times for me as well: this is first community project that I’m maintaining; and I’ve submitted my first Rust-lang PR to Ringfairy (more on that in a future post).

2024 Total Eclipse Prep

The total eclipse is tomorrow, I’ve known about it since the last one I missed in 2017, so of course I started preparing (earlier today). Here’s some handy information for myself and it might help you too.

Why is this one special

Well the next total eclipse on the continental USA is in 2044 and not in driving distance for me. Besides that, there are other reasons as well, and this is the best explanation with (good) animations that I’ve come across. I highly recommend it, it’s approachable and kid friendly.

Path planning

I have created myself a list of pins on a map and identified roads that I can use if I need to shift my location based on the weather. Here are some resources:

  • Path of eclipse: click on a city to find details about: when the eclipse starts, how long totality is, what’s the total duration.
  • Radar view for Eclipse path: it’s the same view area as the map above, so I plan to move about based on weather.
  • My path targets: potential target locations based on weather.
  • Edit: More hot tips! Download a local copy of the map area on Google maps so you can find alternate routes during peak traffic congestion when the internet by the main roads also crawling.

Safety Tips

DO NOT look at the sun unless you’re actually on the line of totality and it’s in the middle of the totality period. I’ll very much be in doubt about whether I can guarantee both of those conditions are true, so I will not be looking directly at the sun.

Photography safety tips

  • If you’re looking at the sun with any optics (camera lens / binoculars) put the filter on the outside of the glass not on your eyes.
  • Basically don’t put on goggles and look through binoculars, it’s essentially ‘a magnifying glass to burn paper’ situation. You want to cut the light before magnifying.
  • Same deal with cell phones, put the googles on the phone camera and look through the screen.
  • Reminder to self: photo effort is limited to 15 seconds for each minute, enjoy and look around.

Road safety tips

  • Give yourself 150% time margin to get where you’re going.
  • If you pull off to the side of the road, don’t stand next to your car. Other people will not be invested in the eclipse until it’s happening and they’ll rubber-neck.
  • Reminder to self: Bring snacks, chair, and patience

This is NOT a comprehensive list.

  • Reminder to self: hydrate and sunscreen.

Good luck and have a safe eclipse

If the weather is bad, just remind yourself that on any sunny day you can eclipse the sun with your head and create a magical and terrifying experience for small creatures.

Joy is free and comes in small packages

Two little neighborhood kids MADE my day! I’m rolling in my small Subaru hatchback and these kids run to the edge of their yard and give me the trucker/train conductor honk fist pump hand signal! I tapped out a festive sequence of mini honks and everyone was happy. Joy is free and comes in small packages!

I shared this on Mastodon but wanted to remember it, it was awesome!

When open source goes source available

Redis went from an open source license to a source available license. This is not a new pattern of trying to protect share-holder value against large cloud providers extracting value they didn’t create. Corporate finance discussion aside (since that’s a really sad can of worms), it’s interesting to see what the FOSS community did about it.

I posted my thoughts on Mastodon:

This lead to some interesting discussion and Drew DeVault chimed in with some interesting comments and posted a link to why he sometimes prefers the MPL license over a GPL license, it’s worth the short read and I plan to think/write about it more.

So here’s my semi-informed rant: AGPL might not always be the best license to police the poor behavior of corporate users. Licenses can only dictate what is legal, and just because it’s legal doesn’t mean it’s ethical. But there-in also lies the problem: unethical entities will always look for the legal loophole or hope to not get caught, assuming laws are equitably enforced in the first place. Whether the community likes it or not a lot of commercial enterprises have contributed significant portions of the FOSS stack. Leaving the door open to contributions but finding better models to compensate developers is more fruitful use of time and effort than subjecting each other to purity tests.

Face lift

I created my online Avatar in the early 2000s using a Flash tool that someone made and I really wish I could remember more about it. It’s been my online presence for over 20 years and it’s almost like my real face. Fun fact: One of my co-worker’s husband recognized me at a company social event solely from having seen my avatar on caller ID.

I’ve always wanted a higher resolution version and my friend just made one for me! He’s getting started with a drawing application on his iPad and did such an excellent job and I’m delighted, thank you!!!

BEFORE

AFTER

Alacritty Auto Theme Switcher

I like and use the Alacritty terminal emulator, but it does not automatically follow the system theme. The issue tracker discussion made it clear this feature won’t be supported, fair enough. And after switching to TOML and discovering partial imports, I knew I could scratch my own itch. Someone wrote a rust tool which was helpful as a guide but I wanted something with low dependency. So I made a bash script and a systemd service and it was fun(?) to learn more about dbus.

So, alacritty-auto-theme was born which automatically switches themes with manual override possible, repo on Github and Sourcehut.

Originally this was a long blog post but I moved the breakdown on how to make a small script and write a systemd service with it. That article is more instructional and is written for an audience that might be interested in tweaking their system but does not necessarily have a technical background so explaining what is happening and why is going to be the focus.

Firefox scrollbar size

I don’t like seeing the scrollbar until I want to see it. I want to see it when:
  • I’m scrolling with an indication of how big the page is
  • I want to grab the scrollbar with the mouse to move it to specific location

When I want to grab the scrollbar, I want it to be a big target to hit, not something I’m chasing around trying to click accurately on a 4K monitor.

./config-screenshot.jpg

Firefox allows you to customize this by going to about:config and then modifying widget.non-native-theme.scrollbar.override, you can also change the style. It only shows up when you scroll and only becomes chonky when you mouse over it. You can control how chonky it is, to your liking. You can also change the style if you would like a non-native style.

./chonky-scrollbar.jpg

filmPoster with Gum Hugo photo post bliss?

I didn’t have a good process to add film photos to my Hugo static site with consistent tags to serve as metadata for camera, film, developer, format, etc. so I cobbled together a small Bash script to collect some input and create a folder as a Hugo `page bundle` but it was a very manual process still and the tag template soon became tedious to maintain.

I discovered Gum and decided to play with it to see if it would improve my post creation experience and it has been pretty good during my brief testing.

That’s what lead to the creation of filmPoster: A Gum powered interactive Bash script to create Hugo film photo posts.

Note:Alt-text provided as closed-caption

How to get Started

Sometimes it’s hard to get started with something new. Sure there are so many resources, almost too many resources, distilling just the essential information is difficult. Here I’m collecting the things I’ve learned over time, so if I were starting from scratch today, I could just jump in and get started.

These are not blog posts fixed in time so if something changes I hope to change the information inline without clarifying edits.

👆🏽 That’s the new section I’m adding to the website. I have benefited immensely from people that have shared their knowledge so I want to capture and share things I’ve learned/am learning that’s a bit more methodical than a drive by note just explaining a specific issue. I hope this also helps me write blog posts more freely because those are not guides and can be more ‘footloose and care-free’ (just like me saying that as an inside joke to myself).

I struggled with what to call the new section, since it’s not really a step-by-step for every minute detail but it’s also not skipping over the hurdles in trying to present a polished demo. It’s just about getting started, but “getting started” doesn’t fit with the single word menu structure so I asked a friend for some ideas:

  • Genesis
  • Preamble
  • Prologue

That made me think of Bootstrap but ultimately I decided I’m really just going for jumping in to start something so a simple Start $XYZ made the most sense?

I did a lot of yak-shaving on this Hugo site, refactoring my org-mode file structure for ox-hugo, and finally my org-capture templates to make the process smoother. So much so, that I didn’t get farther than the first paragraph of the first Start article/guide. However, I am pretty impressed with how flexible Hugo yet how simple Hugo is (and how well it’s supported by ox-hugo). I keep replacing more pieces of the layout (now each section has it’s own RSS feed) and edging closer to creating my own theme (if I do, I’ll write a Start guide)!

Alacritty: TOML and partial imports

I have written before about using Alacritty as my terminal and I’ve configured it using YAML. I’ve been a general fan of YAML, I like the way the syntax looks and there aren’t too many brackets of any kind, if it had a line terminator, that’d be great. The downside is that it is indent dependent. But for the most part I’m comfortable with YAML and wherever an option is provided for using YAML, I pick it over TOML.

However, as of version 13 Alacritty is deprecating YAML in favor of TOML (they’re providing a alacritty migrate command that works very well). So I decided to just get with the program instead of delay adoption. This comes with the unexpected side benefit of being able to do imports of one configuration file into another. I discovered this because @benmo got me wondering how to change the theme of the terminal while in a remote SSH session.

The solution that TOML enable is to create a new configuration file which imports the standard configuration and then just overwrites (and/or adds) to the existing configuration. So I just made a new config file called alacritty-remote.toml:

import=["~/.config/alacritty/alacritty.toml"]

[colors.primary]
background = "0x333333"
foreground = "0xD8DEE9"

Then I created a function in zsh called remoteshh to start a new Alacritty shell window with this new config file:

function sshremote() {
	  alacritty --config-file ~/.config/alacritty/alacritty-remote.toml -e ssh $1 & disown
}

Now when I connect to a remote server over SSH instead of typing ssh server I type sshremote server and I get window with a different background color (I’ll probably theme is more later).

After I told my friend about this whole YAML to TOML saga, he made this…

URL shortening for blog links natively in Hugo

This blog post can also be found at 1ba87346. This short URL is designed to make sharing online more compact without having to use an external URL shortening service. The permalink for this post is 62 characters (plus base URL, everything following discounts the base URL), while the short URL is 11 characters. Every post will predictably be 11 characters since I’m using CRC32 hash of the permalink to generate the short link. My base domain is 8 characters including the dot so a fully qualified link will be 27 characters, which is acceptable. All the while resolving to a fully informative URL (date + topic).

Hugo provides an alias functionality to add one or more alias to every page through the front matter. There’s no built-in automation around this and I also use ox-hugo to generate my Hugo files from a single org file so I decided to add the functionality to the org-capture template that I have already customized to generate Hugo slugs for posts.

Generating a CRC32 hash is really straight-forward in Ubuntu (my build OS due to Emacs version requirement, yes it’s heavy for CI/CD), it’s just crc32 file.txt so a naive implementation would be:

echo "20240121_url-shortening-for-blog-links-natively-in-hugo" > slug.txt
crc32 slug.txt

ad734a45

But I didn’t want transient files being created so I found this super hacky and delightful way of doing it:

echo -n "20240121_url-shortening-for-blog-links-natively-in-hugo" | gzip -1 -c | tail -c8 | hexdump -n4 -e ' '"%08x"'
1ba87346%

So I made that into a script and glued it up with my org-capture template for Hugo.

(concat ":EXPORT_HUGO_CUSTOM_FRONT_MATTER: :aliases /s/"
							  (shell-command-to-string
							   (concat "~/dev/shom.dev/crc32Janky.sh " fname)))

As I mentioned in my previous post, my oldest draft is on that topic but since that’s never getting published, most of it is the capture template.

(use-package ox-hugo
  :straight t
  :config
  ;; Org capture template for Hugo posts
  ;; https://ox-hugo.scripter.co/doc/org-capture-setup/
  (with-eval-after-load 'org-capture
	(defun org-hugo-new-subtree-post-capture-template ()
	  "Returns `org-capture' template string for new Hugo post.
See `org-capture-templates' for more information."
	  (let* ((title (read-from-minibuffer "Post Title: ")) ;Prompt to enter the post title
			 (fname (concat (format-time-string "%Y%m%d_") (org-hugo-slug title))))
		(mapconcat #'identity
				   `(
					 ,(concat "\n* DRAFT " title)
					 ":PROPERTIES:\n:EXPORT_FILE_NAME: index"
					 ,(concat ":EXPORT_HUGO_BUNDLE: " fname)
					 ,(concat ":EXPORT_HUGO_CUSTOM_FRONT_MATTER: :aliases /s/"
							  (shell-command-to-string
							   (concat "~/dev/shom.dev/crc32Janky.sh " fname)))
					 ,(concat ":EXPORT_HUGO_IMAGES: /posts/" fname "/image.jpg")
					 ":EXPORT_HUGO_MENU:\n:END:"
					 "%?\n")          ;Place the cursor here finally
				   "\n")))

	(add-to-list 'org-capture-templates
				 '("h"                ;`org-capture' binding + h
				   "Hugo post"
				   entry
				   ;; It is assumed that below file is present in `org-directory'
				   ;; and that it has a "Blog Ideas" heading. It can even be a
				   ;; symlink pointing to the actual location of all-posts.org!
				   (file+olp "~/dev/shom.dev/content.org" "Content")
				   (function org-hugo-new-subtree-post-capture-template)
				   :prepend t))))

Now I need to make a nice fancy little sharing link and icon that is rendered on every page and go back and update the old posts. The downside of this approach is that it doesn’t generate all shortened links on build only at capture, which is generally better for not breaking links.

I don’t know much about theme-templating (have a few overrides and shortcodes) or using page data to create new elements so I’ll appreciate pointers and help in making my aliases as nice share-links automatically rendered by Hugo.

Tech debt for personal projects

A great thing about Free and Open Source Software (FOSS) is that anyone can create a thing, share it with others, and anyone else can contribute and make it better. It’s also great to solve a problem for yourself and just share it with the world just in case it helps someone else. I know I have benefited from both those modalities, so I have tried to share a few tools and knowledge on this blog and through my repos. So that’s the ideal scenario, but there is a downside that I hadn’t quite grasped before I fell into it… personal tech debt.

Scratch your own itch “software”

It’s one thing to write a janky script to solve your problem but a whole different ball-game to share it with the world. This is after you’ve overcome the first hurdle: the notion that others would have done it better and smarter, fully embracing the benefits and vulnerability of working in the open. Sharing with the world means that you have to fix things properly and shimming a problem isn’t always an option. Getting great input from others is awesome but also overwhelming because I don’t want to just implement something without understanding what it is. The genesis of some of these “projects” was to “scratch your own itch” but once I put it out there, I have a strong desire to not put out crap, which means finding the time and motivation to learn nuances and do it right. Ultimately that means the project quickly becomes unmaintained.

A really-nice-post™

The same goes for writing blog posts, “ohhh, I had to read a bunch of old forum threads which are all just a little outdated but I figured it out so I’ll make a really-nice-post™ to help others”. Well, finding time and motivation to write that post is even scarcer. Now the itch has been scratched and unless it’s a really-nice-post™ then there’s really no point in writing it at all since the information is technically out there. Such a post has a high bar for quality, it must:

  • Provide clear context and describe the problem
  • Layout out potential solutions
  • Describe the chosen solution
  • Provide detailed steps
    • Code snippets in fences so they’re rendered with highlighting
    • Screenshots where applicable
  • Links and credit to all the sources

So it’s not much of a surprise that such a post is just as well-intentioned as it is non-existent.

The three mes

There’s a past-me, the now-me, and the future-me and they’re almost never happy with each other. The now-me always expected more of past-me and has high ambition for future-me. Given that the only me that has agency now is now-me, I’ve decided to… rant. To be fair to past-me, the only reason I’m ranting now is because I finally updated Keyoxidizer[^fn:1], hopefully future-me is happy that there was an update and this post was published (and hopefully, that there was a shift in attitude, but let’s not jump ahead).

Keyoxidizer

I made Keyoxidizer to scratch my own itch, sharing it with the world was exciting as it looked like some people got some use out of it. GPG identity management isn’t that straight forward and I was learning by doing. Sharing with the world paid-off because I got great feedback and a couple of contributions. But some of the feedback about best practices was a bit over my head. I understood enough to know that what I had implemented (RSA) was fine but neither modern nor performant. But I didn’t understand the feedback enough to directly translate that to the GPG unattended key generation config format (it’s not straight-forward, or so I thought because it’s complicated)! Also I wanted to learn and make other improvements on handling this, like giving users choice of algo… so predictably it didn’t get done. It fell into the really-nice-post™ black-hole.

Ultimately, what helped was to just narrow my focus on a specific implementation[^fn:2]. Something that I would have easily done in a work context, but doing it for a personal project is difficult because there are so many competing goals[^fn:3].

Personal tech-debt

So why did it take 700+ words to get to the titular point? Because I’m still fighting against past-me’s desire to have a really-nice-post™[^fn:4] and future-me’s concern that this is all rubbish anyway.

I have been doing an increasingly better job of keeping personal notes on hobbies, highlights from articles, archiving links and documents that have reference potential, etc.[^fn:5] for months. I’m trying to build on that habit to decrease my personal tech-debt. Capturing information when I think of it and do a quick search so I can come back to it more easily later. Biting off smaller chunks and getting it running; basically, everything I would do at work but for hobbies? I don’t want to make my hobbies a chore but it is nice to see progress and completion. I’m still uncomfortable with the idea that my personal tech-debt can become someone’s problem if I share what I’m doing and they decide to use it. But the alternative might be DenverCoder9, so doing work out in the open and not producing a really-nice-post™ might be okay?

[^fn:1]: Linking to Github mirror because Codeberg and Sourcehut have been dealing with DDoS attacks. [^fn:2]: changing the Key-Type: EDDSA and specify the Key-Curve: ed25519. [^fn:3]: I want to learn, yak-shave, experiment, be present instead of being results oriented and enjoy the experience. [^fn:4]: Fun fact - the oldest incomplete draft on this blog is from 2021-10-25 about org-capture template for Hugo, it wasn’t nice enough to finish and post but would have helped a more recent past-me had I finished writing it. [^fn:5]: I’ve been using Logseq for it (org data format) but just using the `#` notation to create pages and links using the journal as a front-end for note capture.

Film Photography

There’s a new section on the website for Film Photography that is being built by Hugo but independent of the blogging workflow. I’ll create a new entry documenting how that is setup in a follow-up post.

I’m not sure if the film photos should be a on the main blog feed but without realizing it I have flooded the RSS feed, perhaps it should be on it’s own feed (don’t want to inconvenience the non-existent readership of this blog!). The other thing that might require some thinking/restructuring is whether to use categories to get a better handle on taxonomies. Hugo does not do hierarchical tags but posts tagged with camera and camera/canonQL17Giii will show up under the camera tag, which is nice but a drill down would be preferable (without extensive JavaScript magic). Categories could serve as the drill down but I don’t know if that added convolution has any practical benefits.

For now, I’ll just leave a family portrait of the range finders: Olympus 35 SP, Canon QL17 Giii, and Olympus 35RC.

More eagles

I went back to look for eagles again and found that the lake/pond/ditch had frozen over but there were still a couple of eagles milling about and several black birds.

Lessons learned:

  • 1/1600 of second still pretty slow for capturing even a big bird in flight.
  • ISO 400 is pretty reliable on my camera and darktable matches the noise profile, doing a decent job of noise removal.
  • Busy backgrounds are hard to work with but they aren’t show stoppers.
  • JPEG is a terrible image format and makes the photos look much worse.

Eagles in the “backyard”

Took some photos of bald eagles fairly close to my house, thanks to a tip from a friend. I never think to go exploring around home whereas I would have hiked miles on vacation to see wildlife. This is a great proof and reminder to see your “backyard” with fresh eyes.

Photos captured with a Sony A7C with a Sigma 100-400.

Trigger site rebuild to update copyright

I just saw this post from VKC about updating the footer of your website, meaning the copyright section. It occurred to me that it’s a manual process for my fully auto-generated blog, so I never think about the copyright info in the footer. Static sites do kind of have a shortcoming of sort.

I still have my photos on a WordPress blog and it automatically updates the copyright, which came up, very recently, in a conversation with a friend and yet I never thought about my Hugo site (this one). I wonder if I need a build pipeline that triggers at midnight UTC every year just to update the footer!? No.

I wanted to scribble this thought quickly to first, trigger a rebuild and secondly, to put a commitment to finally investigate the Creative Commons licenses and choose an appropriate license site-wide. I would like for any information/photo/art to be usable for non-commercial use with attribution and maintaining an equivalent license. =======

Let’s Encrypt with acme.sh behind CPanel

I have access to webhosting through the generosity of a friend and his hosting provider used CPanel and offers paid SSL certificates but does allow for SSH access. So, the best and free way to get SSL certificates is getting certificates from Let’s Encrypt using acme.sh.

While I’ve had this setup for years and it works great, it’s a real issue if it breaks because I do the sad thing of hitting up in the terminal history #somuchshame. So I’m documenting it for myself and anyone else that might find this useful.

  1. Clone acme.sh from Github and cd into folder.
  2. Issue the certificate with:

    ./acme.sh --issue --webroot /home/USERNAME/public_html/ --domain example.org --deploy-hook cpanel_uapi

  3. Deploy the certificate if the deploy hook doesn’t do its job properly

    ./acme.sh --deploy --domain example.org --deploy-hook cpanel_uapi

  4. Setup the cron job so it will renew automatically

    ./acme.sh --cron

Another win for FOSS and SSH access on a Linux box.

In dire situations, you can actually go to CPanel and manually enter the certificate information that acme.sh generates. The acme.sh folder will contain a sub-directory named example.org (whatever your domain name is), inside that you’ll need to map the contents of the following files to the following fields:

File NameCPanel Field
example.org.cerCertificate: (CRT)
example.org.keyPrivate Key (KEY)
ca.cerCertificate Authority Bundle: (CABUNDLE)

NOTE: If you’re having issues with the ZeroSSL.com CA that acme.sh now defaults to, you can edit example.org.conf and specify the api using:

Le_API='https://acme-v02.api.letsencrypt.org/directory'

Editorial note: The API isn’t French, it’s Le for Let’s Encrypt… capitalizing acronyms in variable names is always contentious, snake case should makes it easier. But mixing usage seems like the worst of all choices. Le but not Api? Why not LE_API or le_api.

Update: @benoitj makes another great point, LE is not providing any additonal context, acme or api (regardless of capitalization) would make the variable name better.

FOSS Woodworking

I built a small table this weekend and realized that the thing I desperately need if I’m going to build more stuff is a workbench. There are a TON of workbench options to pick from, which is great. But, I didn’t want to get into picking and choosing dimensions and features on the fly, that was asking for a disaster. So I decided to take the plunge and learn FreeCAD. There are excellent YouTube videos targeted at “FreeCAD for Woodworkers” which was a delightful surprise. Being a novice at woodworking and CAD is not the best combination, but you gotta start somewhere.

I decided to start backwards by doing the build first and the design later. Just like I built a small table physically, it seemed like a good idea to model something simpler. I was able to learn enough FreeCAD in one evening to make this model:

Had I made the model before the bottom shelf might have turned out better, just maybe, but something else would have provided “an opportunity for improvement”… there are many ways to get better, exciting!

Fiddly Fig

Haven’t done any water coloring in months and this fiddly fig wasn’t the easiest one to start with. My friend had a book of botanical watercolors and the fiddly fig was one of the choices. Turns out I have a hard time drawing non standard leaves🍃 that overlap. I hadn’t originally planned to sketch the outline but after I finished (messing up) coloring the leaves, it all looked like blobs in need of structure. The actual instructions were a bit confusing but after doing it poorly I now understand a better way to approach it: paint a lighter base layer of green, wet on dry, and then at the base of the leaf inject some darker green, wet on wet.

It was a fun way to spend an evening with friends and reminded me to paint more. Only the drawing/sketching is stressful the painting part is fun!

Fun with pipes

Just came across this excellent post: Poor mans mind mapping tool with just the terminal from @fullstackthaumaturge (account no longer exists) toot on Fosstodon. The whole premise is that you can do a lot things with the UNIX philosophy of using files for everything and manipulating them with simple tools that do one thing but do it well. So if you wanted a mindmap then just touch files in a folder hierarchy and then print it out with tree.

I found that amusing and thought, “well what if you don’t want to clutter your file-system and wanted to zip up your mindmap?” Would you be able to get a nice tree output without unzipping the archive? Well turns out you can do just that by piping from zipinfo to tree, which supports reading from a file (instead of reading a file-system) using the --fromfile argument.

So you end up with this command:

zipinfo -1 mindmap.zip | tree --fromfile $1 -C -r

and this output:

.
`-- mindmap
    |-- top
    |-- first
    |   |-- second
    |   `-- first
    `-- 2
        |-- 2
        `-- 1

3 directories, 5 files

I’m not suggesting anyone do this, but it’s a fun example of UNIX principles and pipes.

Markdown anchor linking on Github

I’ve been using org-transclusion for an “inverse literate” Emacs config and tangling all the config chunks on save and exporting it as a markdown file. This has worked fairly well except for the fact that org-export creates org-export regenerates ids for all the headings which creates noise in the git commit history and also in-page anchors can’t be reliably linked to a specific part of the document (independent of the git forge’s markdown parsing implementation).

In order to remedy that without relying on a full-featured package without additional capabilities, I decided to adapt a snippet of @alphapapa’s unpackaged configuration, which advices the export to create unique anchors that won’t change between exports (unless the headings themselves have been changed). However, this is ended being the beginning of the solution and how I discovered GitHub renders markdown internal links to HTML is not consistent with how Sourcehut does it.

  ;;usefulanchors_begin
;; From @alphapapa's unpackaged repo https://github.com/alphapapa/unpackaged.el#export-to-html-with-useful-anchors
(use-package ox
  :config
  (define-minor-mode unpackaged/org-export-html-with-useful-ids-mode
    "Attempt to export Org as HTML with useful link IDs.
Instead of random IDs like \"#orga1b2c3\", use heading titles,
made unique when necessary."
    :global t
    (if unpackaged/org-export-html-with-useful-ids-mode
        (advice-add #'org-export-get-reference :override #'unpackaged/org-export-get-reference)
      (advice-remove #'org-export-get-reference #'unpackaged/org-export-get-reference)))

  (defun unpackaged/org-export-get-reference (datum info)
    "Like `org-export-get-reference', except uses heading titles instead of random numbers."
    (let ((cache (plist-get info :internal-references)))
      (or (car (rassq datum cache))
          (let* ((crossrefs (plist-get info :crossrefs))
                 (cells (org-export-search-cells datum))
                 ;; Preserve any pre-existing association between
                 ;; a search cell and a reference, i.e., when some
                 ;; previously published document referenced a location
                 ;; within current file (see
                 ;; `org-publish-resolve-external-link').
                 ;;
                 ;; However, there is no guarantee that search cells are
                 ;; unique, e.g., there might be duplicate custom ID or
                 ;; two headings with the same title in the file.
                 ;;
                 ;; As a consequence, before re-using any reference to
                 ;; an element or object, we check that it doesn't refer
                 ;; to a previous element or object.
                 (new (or (cl-some
                           (lambda (cell)
                             (let ((stored (cdr (assoc cell crossrefs))))
                               (when stored
                                 (let ((old (org-export-format-reference stored)))
                                   (and (not (assoc old cache)) stored)))))
                           cells)
                          (when (org-element-property :raw-value datum)
                            ;; Heading with a title
                            (unpackaged/org-export-new-title-reference datum cache))
                          ;; NOTE: This probably breaks some Org Export
                          ;; feature, but if it does what I need, fine.
                          (org-export-format-reference
                           (org-export-new-reference cache))))
                 (reference-string new))
            ;; Cache contains both data already associated to
            ;; a reference and in-use internal references, so as to make
            ;; unique references.
            (dolist (cell cells) (push (cons cell new) cache))
            ;; Retain a direct association between reference string and
            ;; DATUM since (1) not every object or element can be given
            ;; a search cell (2) it permits quick lookup.
            (push (cons reference-string datum) cache)
            (plist-put info :internal-references cache)
            reference-string))))

  (defun unpackaged/org-export-new-title-reference (datum cache)
    "Return new reference for DATUM that is unique in CACHE."
    (cl-macrolet ((inc-suffixf (place)
                               `(progn
                                  (string-match (rx bos
                                                    (minimal-match (group (1+ anything)))
                                                    (optional "--" (group (1+ digit)))
                                                    eos)
                                                ,place)
                                  ;; HACK: `s1' instead of a gensym.
                                  (-let* (((s1 suffix) (list (match-string 1 ,place)
                                                             (match-string 2 ,place)))
                                          (suffix (if suffix
                                                      (string-to-number suffix)
                                                    0)))
                                    (setf ,place (format "%s--%s" s1 (cl-incf suffix)))))))
      (let* ((title (org-element-property :raw-value datum))
             (ref (replace-regexp-in-string "%.." "-" (url-hexify-string (substring-no-properties title)))) ;replace all encoded characters with dashes
             (parent (org-element-property :parent datum)))
        (while (--any (equal ref (car it))
                      cache)
          ;; Title not unique: make it so.
          (if parent
              ;; Append ancestor title.
              (setf title (concat (org-element-property :raw-value parent)
                                  "--" title)
                    ref (url-hexify-string (substring-no-properties title))
                    parent (org-element-property :parent parent))
            ;; No more ancestors: add and increment a number.
            (inc-suffixf ref)))
        ref))))
;;usefulanchors_end

NOTE: actual header anchor is GitHub’s internal linking and there’s separate <p> with the user exported anchor from markdown, just interesting.

Turns out that GitHub won’t do anchors with any non-alphanumeric links even if they’re properly hex-coded. I had to modify the function which creates the unique slugs because by default it hex encodes the url, which is the “correct/smart” thing to do and Sourcehut happily renders that. But GitHub generates its own slugs which removes all non-alphanumeric characters (which makes the slug less readable, I prefer more readable urls).

(replace-regexp-in-string "%.." "-" (url-hexify-string (substring-no-properties title)))

Another excellent example of how #foss enables these customizations by empowering the user.

Bonaire Art

I was privileged to visit Bonaire in the Dutch Caribbean last week. While the whole reason for the trip was scuba diving (the entire island is essentially a dive site, just walk out into ocean in any direction), I also enjoyed the downtown area and found the art very charming. Here are a few pieces that caught my eye:

My favorite mural was definitely this whimsical scene of this mural of a guy vibin’ with some chill goats and playing his ukulele. The island has a lot of wild/stray goats and they’re definitely quite chill and the baby goats are super cute, kid you not! The artist is Dodici and has a very unique style.

The island is also famous for its flamingos grazing in the salt flats. Bonaire is a sea-salt producing island and there are huge mounds of salt that are stacked before shipping, all of that area is a good hang for flamingos. I was lucky to get to see some flamingos up close while at the Washington Slagbaai National Park. But I think I saw a lot more flamingo art, which was also great.

I enjoyed the simple style of this mermaid and the paint colors effectively capture all the hues of the waters around Bonaire.

Dushi means all the things in life that are good and sweet. The artist tag is @kayakorsou but I wasn’t able to find an online presence.

We can’t end without yet another cute flamingo!

From fish on Gnome Terminal to zsh with Starship on Alacritty

Gnome Terminal and fish

I have been using the default Gnome Terminal with the fish shell for a long time and it has served me well. Since fish provides a lot of functionality out of the box (including meta information about git repos in the prompt), I have stuck with it for the convenience. However, there is ONE major downside to fish; it is not POSIX compliant.

Why ditch fish?

See what had happened was… Non-POSIX compliant wasn’t a big problem until I found myself writing a couple helper functions with fish syntax. This was a proverbial red-flag since fish becomes a hard dependency for all my systems going forward. This coupled with the how fish saves aliases (as separate functions when you call funcsave aliasname) which I always found a bit tedious led me to finally think about moving to zsh.

What would I miss most from fish and Gnome Terminal?

The baseline against which I’m making this list is default bash on Ubuntu based systems, which is what I’ve had the most exposure to. So compared to that experience:

  • Right off the bat, I like fish’s default prompt and never felt the need to customize it because it showed the current path and for git repos shows the current branch.
  • Completions! The fish completions are great and the history sub-string search is excellent.
  • Syntax highlighting of commands as you type so you can easily spot typos as they happen.
  • As for Gnome Terminal, I wouldn’t really miss anything assuming I could theme the terminal a bit. What I wouldn’t miss is the lack of a configuration file that could be added to my other dotfiles.

While this list serves as the basis for the requirements of the new tool-chain, the top requirement was plain-text based configuration management, which can be placed under version control. The other very soft requirement was tools developed using Rust. I’ve done a few “hello world” things in Rust and want to continue learning more and figured that using more Rust based tools is a good path to learning and contributing. With all that in mind, I landed on Prompt: starship, Terminal: Alacritty, and Shell: zsh.

Prompt: starship

I decided to separate the prompt from the shell with Starship. starship is highly customizable but it does everything I want from it out of the box; which is, cleanly and minimally replicate features of the fish prompt. There are a lot of “themes” and configurations which I’m sure I’d love to tweak and yak-shave someday, but I enjoy the out of box experience. To fully take advantage of the default configuration, you need a nerd font (a font that has been patched with icons that are often used to represent software tools/concepts/applications). I’m a fan of the JetBrains Mono and there is a patched nerd font variant.

Terminal: Alacritty

I have seen Alacritty getting praised for being fast, functional, configurable, and it being cross-platform tool written in Rust was all I needed to land on it. Some of the speed tests are pretty impressive, the configuration is very straight forward, and there are tons of resources so I won’t delve into things that have been covered very well everywhere.

One big advantage of Alacritty that I don’t see touted often is a very keyboard focused workflow. I especially enjoy the vim-like visual mode (bound to CTRL + SPACE by default) which allows navigating within the buffer, searching the output, and making text selections and copying from anywhere in the buffer all with the familiar vim keybindings.

Here’s my minimal Alacritty configuration:

# Configuration for Alacritty, the GPU enhanced terminal emulator.
window:
  # Window dimensions (changes require restart)
  padding:
    x: 10
    y: 5
  decorations: none
  opacity: 0.85

# Font configuration
font:
  size: 14.0
  normal:
    family: JetBrains Mono Nerd Font
    style: Regular

# Colors (Nord)
colors:
  # Default colors
  primary:
    background: '0x2E3440'
    foreground: '0xD8DEE9'

  # Normal colors
  normal:
    black:   '0x3B4252'
    red:     '0xBF616A'
    green:   '0xA3BE8C'
    yellow:  '0xEBCB8B'
    blue:    '0x81A1C1'
    magenta: '0xB48EAD'
    cyan:    '0x88C0D0'
    white:   '0xE5E9F0'

cursor:
  style:
    shape: Beam
  vi_mode_style: Underline
  thickness: 0.25

# Live config reload (changes require restart)
live_config_reload: true

key_bindings:
  - { key: N,              mods: Shift|Control,                action: SpawnNewInstance      }
  - { key: Space,          mods: Control, mode: ~Search,       action: ToggleViMode          }
  - { key: Return,         mods: Alt,                          action: ToggleFullScreen      }

Shell: zsh

This post is getting to be quite long and there’s a lot to discuss with zsh. I’ll hit the highlights here and do a more detailed write-up in the future when I’ve lived in it for a few weeks/months. I have seen lots of helpful posts on zsh and even fish to zsh migrations but all of the ones I came across use the Oh my zsh “framework”. While oh my zsh is great, I wanted to stick to a smaller/leaner configuration that I could understand myself. The great thing is that since oh my zsh is a collection of scripts that marshaled, the underlying functionality is available as independent repos which I added as git submodules to my dotfiles repo and got a fairly streamlined experience on my laptop and phone (via Termux).

# Minimal zsh configuration

# Personal functions
fpath=(~/.config/zsh/functions "${fpath[@]}")
autoload -Uz vi
autoload -Uz cat
autoload -Uz ls
autoload -Uz lst

# Aliases
alias gs="git status"
alias ga="git add --all"
alias gd="git diff"
alias gc="git commit -m"
alias gf="git fetch"
alias gF="git pull"
alias gp="git push"

# History
export HISTFILE=~/.config/.zsh_history
export HISTSIZE=100
export SAVEHIST=1000

# Command prompt using starship
eval "$(starship init zsh)"

# All zsh "plugins" are git submodules symlinked to ~/.config/zsh
# Sourced from: https://github.com/orgs/zsh-users/
source ~/.config/zsh/zsh-autosuggestions/zsh-autosuggestions.zsh
source ~/.config/zsh/zsh-ssh-agent/ssh-agent.zsh
source ~/.config/zsh/zsh-syntax-highlighting/zsh-syntax-highlighting.zsh

# History substring matching like fish, load after syntax-highlighting
source ~/.config/zsh/zsh-history-substring-search/zsh-history-substring-search.zsh
#requires keybinds for up and down
bindkey '^[[A' history-substring-search-up
bindkey '^[[B' history-substring-search-down

Before and After

Yeah, I get it: just show the screenshots.

So far I’m pretty happy and comfortable with the new system. The thing I still miss from fish is expanding the commands and sub-commands of CLI apps. I’m sure there are zsh packages for that and I look forward to learning more. If you have any suggestions, I would love to learn from you.

QMK caps word

I use a Corne low profile keyboard running the QMK firmware. It is a 42 key layout and although it has a sixth column where a lot of folks put the traditional SHIFT and CTRL modifier keys, I’ve opted to go with the home row mods so that I’m not stretching my fingers and negating some of the ergonomic advantages. The downside is that it is difficult to type full words/phrases in capital letters without switching which hand is holding the modifier. Also, it’s not as convenient to press the capslock button since it’s on a layer and most things that I’m typing aren’t very long in ALL CAPS, I don’t do a lot of yelling online.

This is where the very interesting and awesome Caps Word feature that I just discovered comes in handy. It temporarily sends out capital letters from the keyboard (importantly, it doesn’t turn on CAPSLOCK since it might be mapped to something else. Here’s how it works:

  • Caps Word is activated by pressing the left and right shift keys at the same time.
  • Caps Word automatically disables itself at the end of the word.

I was able to set it up pretty easily on the keyboard, well, once I realized that I had MOD_LSFT on both halves accidentally and fixed it. It’s going to take a bit of getting used to but awkwardly typing in ALL CAPS is a good reminder to use the proper feature.

Seahorse

I still need to figure out how to get ox-hugo to process images that are within shortcodes. Might be a good opportunity to learn the code base a bit and maybe contribute.

Note: I did wrap this in a proper img tag to support alt-text but the rendered effect is the same as just putting a raw file in.

Here’s the figure shortcode.

Hugo photos with EXIF data

I have been wanting to transition my photography site to Hugo as well but have not investigated how to utilize Hugo’s image processing capabilities. Yesterday I came across Wivik’s helpful shortcodes that display EXIF information and presents the photo with a frame and a caption. I’m experimenting with it now and might modify it and eventually migrate my photography content.

I ran into nil pointer evaluating resource.Resource.Resize error when running the shortcode and tried out the built-in figure shortcode with the same path to verify that it wasn’t an actual path issue. The answer lies in Hugo’s use of Page Bundles, essentially standalone directory per post which bundles the text and images in a single folder. Many thanks to Tim Walls’ post explicitly helping future sufferers of the same error and DuckDuckGo for indexing the page keywords well.

I still have to smooth out the edges for making the page bundle play nice with the short-code within ox-hugo. Kudos to Kaushal Modi for already supporting page bundles elegantly in ox-hugo. But for this post, I can “cheat” because I need to show the old style rendering anyway for a comparison. By inserting the image directly, ox-hugo will copy the image to the right location so Hugo can do image processing on that page bundle.

The “old” method is just the original image linked directly with no captions or EXIF metadata:

Note: I did wrap this in a proper img tag to support alt-text but the rendered effect is the same as just putting a raw file in.

And the “new” method which uses the shortcode to resize the image to save bandwidth (the original image can be viewed at full resolution by clicking it… I’m not thrilled with the compression quality but it’s decent) and also shows the EXIF metadata. I plan to do a bit more with the EXIF info but this is a great start thanks to the shortcode, the caption parameter I added (to provide descriptive alts for accessibility independent of the caption) and icons from the Remixicon project who provide high-quality FOSS icons.

Update: Shortcode appears to be working locally when testing with hugo server -D but failing on sourcehut ci/cd, I’ll investigate with fresh eyes tomorrow.

Update 2: Kaushal happened to see my toot and quickly provided a work-around, I’ll sing his praises more preemptively so he can do troubleshooting for me without even asking :), more later.

Corne LP

I fell down the split ergo mechanical keyboard rabbit hole thanks to a dear friend who was kind enough to loan me his Gergoplex (despite my incessant teasing about his hipster keyboard). The Gergoplex is on the deeper end of the rabbit-hole with only 36 keys and 12g switches but it demonstrated the value to me. I ended up getting a pre-built Corne and added the lightest switches I could find at the time: Gateron MX 35g switches. I’ve been pretty happy with the Corne (and it’s 3x6 +3 layout) but I very much enjoyed the lower profile and light action of the Gergoplex and wanted to chase it…

So, I got a Corne LP kit but went with an acrylic case (the aluminum case looks really nice but it’s quite rich) and got the 25g Purpz Choc switches.

It was fun to assemble the kit and the board looks great:

I didn’t have any Choc keycaps so I had to wait a bit for the MK Ultra MBK Choc (vendor may be defunct) keycaps. I’m very impressed with the keycaps and the homing keys feel great. I’m a big fan of the look and feel and quite happy with the low profile and light touch which I was looking for.

The keyboard worked “out of the box” but I was able to flash my custom QMK firmware and was able to get up and going with my keymap. Now, maybe I’ll look into making the kit wireless? It never ends.

OBS, virtual camera, guix

I’ve used OBS as a virtual camera input for various reasons (to compose scenes, to control field of view, etc.). I was setting it up on my desktop (Pop_Os! 20.04 with Guix as the package manager) today because Microsoft Teams recognizes my El Gato CamLink 4K but won’t show any video. Since I had successfully used virtual camera before I tried setting it up, but ran into some issues.

OBS needs the v4l2loopback driver in order to enable the virtual camera functionality.

guix install obs-studio v4l2loopback-linux-module

Installing OBS and the loopback driver worked but even after a restart OBS would not show the virtual camera option. I decided to see if it was a package/path issue and tried using apt but even after restart that didn’t work, turns out apt’s version of OBS is too old.

apt install -y obs v4l2loopback-dkms

NOTE package names are different

Ultimately, I had to get OBS from guix and the loopback from apt. This mismatch makes me uneasy since it goes directly against a declarative config, so I’m documenting the discrepancy for when it bites me in the future.

Found Nemo!

First painting with the new paint set. Good paper (140 lbs) and paint make a big difference.

Water color paint key/legend

I got a new water color paint set. Turns out I like painting enough and I was encouraged by a friend with a generous gift of brushes and a marine life water color book. The same friend also clued me in to making a paint key/legend. Well, first I made a poor design decision on how to structure the key and then failed to follow the design (further indicating poor design). I also had to reconcile my desire for perfection with lack of a ruler, lack of patience, and lack of necessity for the outcome to be perfect. So what I intended to be a relaxing afternoon activity turned out to be a bit frustrating. BUT, I’m glad I persevered and now I have this legend to guide me on what colors to pick when I paint. Now that it’s done, it looks pretty to me, not surprising that I also love opera warming up cacophonous sound.

Setting up Protonmail in Emacs

I’ve used Protonmail for several years and use the web interface for the most part and used Thunderbird on the desktop to keep offline copies of email. Since Protnmail takes care of the encryption it requires a local bridge to provide a standard interface like IMAP. Essentially, it is running an IMAP server on the local machine that any compatible client can connect to. Technically, the bridge can be made accessible on a local network so many clients from many machines can connect to it. I might eventually set this up when I have had a chance to get a better handle on vlans and access control.

Installing packages

In order to use connect to the local IMAP bridge locally, I will be using mbsync. I’m using guix for package management, guix (and other package managers) refer to mbsync as isync. The mu package also includes mu4e (at least in version 1.6+ and it’s not recommended to mix/match versions).

guix install isync mu

Configuring mbsync

mbsync expects a configuration in ~/.mbsyncrc (does anyone know how to move this to ~/.config? I’m disheartened by all the home directory clutter). Ideally one would GPG encrypt the password but since Proton Bridge generates it locally and it’s is available as clear text to the local machine anyway, I didn’t bother. Instead I just put the password from the ProtonBridge application into a text file (ensure no extra characters exist like space or return) and cat that into the PassCmd.

IMAPAccount proton
Host 127.0.0.1
User [email protected]
PassCmd "cat ~/.protonBridgePass"
SSLType NONE
CertificateFile /etc/ssl/certs/ca-certificates.crt

IMAPStore proton-remote
Account proton

MaildirStore proton-local
Subfolders Verbatim
Path ~/mail/proton
Inbox ~/mail/proton/inbox

Channel proton
Far :proton-remote:
Near :proton-local:
Patterns *
Create Both
SyncState *

Running the sync command gave me an error that sent me on a goose chase:

mbsync -a

Socket error: secure connect to 127.0.0.1 (127.0.0.1:1143): error:1408F10B:SSL routines:ssl3_get_record:wrong version number The issue was the SSLType NONE is the proper config as shown above, I originally had it set to IMAPS. Once the sync starts it will take a long time depending upon how many emails you have.

Configuring mu4e

Configure the mu4e-maildir location to wherever you want to store the mail directory (remember mail in this folder is stored in clear-text). The mu4e-****-folder variables need to include the sub-directory in the relative path, in my case proton.

(use-package mu4e
  :straight nil
  :defer 20 ; Wait until 20 seconds after startup
  :config

  (setq mu4e-change-filenames-when-moving t ; avoid sync conflicts
	  mu4e-update-interval (* 10 60) ; check mail 10 minutes
	  mu4e-compose-format-flowed t ; re-flow mail so it's not hard wrapped
	  mu4e-get-mail-command "mbsync -a"
	  mu4e-maildir "~/mail/proton")

  (setq mu4e-drafts-folder "/proton/Drafts"
	  mu4e-sent-folder   "/proton/Sent"
	  mu4e-refile-folder "/proton/All Mail"
	  mu4e-trash-folder  "/proton/Trash")

  (setq mu4e-maildir-shortcuts
	  '(("/proton/inbox"     . ?i)
	    ("/proton/Sent"      . ?s)
	    ("/proton/Trash"     . ?t)
	    ("/proton/Drafts"    . ?d)
	    ("/proton/All Mail"  . ?a)))

  (setq message-send-mail-function 'smtpmail-send-it
	  auth-sources '("~/.authinfo") ;need to use gpg version but only local smtp stored for now
	  smtpmail-smtp-server "127.0.0.1"
	  smtpmail-smtp-service 1025
	  smtpmail-stream-type  'ssl))

I’m also configuring smtpmail in the config section of mu4e just to keep mail config together, smtpmail is part of Emacs core. I’m adding SMTP authentication info to the un-encrypted .authinfo for the same reason as .mbsyncrc explanation above.

machine 127.0.0.1 login [email protected] password ProtonBridgeGeneratedPassword port 1025

Using org-mode to compose HTML emails

At this stage plain-text email will work just fine, in order to send email with formatting I’m using org-msg which lets you compose with org markup and sends it out as HTML (including in-lining images, tables, etc.)

(use-package org-msg
  :straight t
  :after mu4e
  :config
  (setq mail-user-agent 'mu4e-user-agent)
  (require 'org-msg)
  (setq org-msg-options "html-postamble:nil H:5 num:nil ^:{} toc:nil author:nil email:nil \\n:t"
	  org-msg-startup "hidestars indent inlineimages"
	  org-msg-default-alternatives '((new		. (text html))
					 (reply-to-html	. (text html))
					 (reply-to-text	. (text)))
	  org-msg-convert-citation t)
  (org-msg-mode))

References

Here are a list of references I used to get everything setup and configured:

“Inverse literate” config via org-transclusion

I am very intrigued by the whole concept of literate programming. There is a lot of opinions and lots of valid points for and against comments, but ultimately it’s always a good idea to understand intent. I wanted to create a literate config but did not want slow down startup with tangling.

I came across an interesting package today called org-transclusion by @nobiot. The package is very interesting, being able to bring in arbitrary lines of text from multiple documents into a single document (while those documents remain the source of truth) is quite powerful. The package also allows extracting sections based on tags (string match) which makes it a good contender to make an “inverse literate” configuration which I’ve been curious about ever since David Wilson did a System Crafters live stream.

So I decided to give it a shot and got started with my custom configuration. I added some text comments to divide the configuration into sections:

;directory_begin
(setq user-emacs-directory "~/.emacs/.custom/")
;directory_end

Then I’m able to include it in an org file:

This line live in my org file, but the content below lives in my init.el file:
#+transclude: [[./init.el::;directory_begin]] :lines 2- :src emacs-lisp :end "directory_end"

Where org-transclusion looks for a file ./init.el and searching for the begin string ;directory_begin and includes everything until it encounters the end string ;directory_end (both strings are arbitrary, I just picked that convention) but doesn’t include the actual line containing “directory_end” as specified by the :line 2- parameter. All of that would produce:

This line live in my org file, but the content below lives in my init.el file:
(setq user-emacs-directory "~/.emacs/.custom/")

And in the future if I added anything in init.el between the ;directory_begin and ;directory_end comment lines, then it would get included in the org file.

Here’s what all of this looks like in my actual configuration repo (I haven’t finished writing up all the sections yet, but plan to soon™.

Overall, this has worked pretty well. The file config.org in my repo contains the “source” and org-transclusion directives and is rendered out to README.md (markdown is better supported for auto-rendering by more forges currently). I’ll eventually automate this process, likely through a git-hook. However, the rendered output is never guaranteed to include all of my config, just the sections that have been manually commented, init.el and includes will remain the source of truth.

Doom Emacs config (deprecated)

I was asked about my Doom config by someone on fosstodon but it’s not a clean repo where I’m not confident that I didn’t accidentally commit private information in the past, so I wasn’t sure how to share. But this is a good time to put a pin in the config and capture a snapshot here, for reference.

This is quite messy and mixes idioms at random as I learned more about configuration. I also used this config to transition over to my custom config so I disabled some Doom functionality as I went down that path, in short, I was using a lot more packages in init.el that the current state below.

init.el

(doom! :input
       ;;chinese
       ;;japanese

       :completion
       company           ; the ultimate code completion backend
       ;;helm              ; the *other* search engine for love and life
       ;;ido               ; the other *other* search engine...
       ;;ivy               ; a search engine for love and life

       :ui
       ;;deft              ; notational velocity for Emacs
       doom              ; what makes DOOM look the way it does
       doom-dashboard    ; a nifty splash screen for Emacs
       doom-quit         ; DOOM quit-message prompts when you quit Emacs
       ;;fill-column       ; a `fill-column' indicator
       hl-todo           ; highlight TODO/FIXME/NOTE/DEPRECATED/HACK/REVIEW
       ;;hydra
       indent-guides     ; highlighted indent columns
       modeline          ; snazzy, Atom-inspired modeline, plus API
       nav-flash         ; blink the current line after jumping
       ;;neotree           ; a project drawer, like NERDTree for vim
       ;;ophints           ; highlight the region an operation acts on
       (popup +defaults)   ; tame sudden yet inevitable temporary windows
       ;;pretty-code       ; ligatures or substitute text with pretty symbols
       ;tabs              ; an tab bar for Emacs
       ;;treemacs          ; a project drawer, like neotree but cooler
       unicode           ; extended unicode support for various languages
       vc-gutter         ; vcs diff in the fringe
       ;;vi-tilde-fringe   ; fringe tildes to mark beyond EOB
       ;;window-select     ; visually switch windows
       ;;workspaces        ; tab emulation, persistence & separate workspaces
       ;;zen               ; distraction-free coding or writing

       :editor
       (evil +everywhere); come to the dark side, we have cookies
       ;file-templates    ; auto-snippets for empty files
       fold              ; (nigh) universal code folding
       ;;(format +onsave)  ; automated prettiness
       ;;god               ; run Emacs commands without modifier keys
       ;;lispy             ; vim for lisp, for people who don't like vim
       ;;multiple-cursors  ; editing in many places at once
       ;;objed             ; text object editing for the innocent
       ;;parinfer          ; turn lisp into python, sort of
       ;;rotate-text       ; cycle region at point between text candidates
       snippets          ; my elves. They type so I don't have to
       ;;word-wrap         ; soft wrapping with language-aware indent

       :emacs
       (dired +icons)    ; making dired pretty [functional]
       electric          ; smarter, keyword-based electric-indent
       ;;ibuffer         ; interactive buffer management
       undo              ; persistent, smarter undo for your inevitable mistakes
       ;;vc              ; version-control and Emacs, sitting in a tree

       :term
       ;;eshell            ; the elisp shell that works everywhere
       ;;shell             ; simple shell REPL for Emacs
       ;;term              ; basic terminal emulator for Emacs
       vterm             ; the best terminal emulation in Emacs

       :checkers
       syntax              ; tasing you for every semicolon you forget
       spell             ; tasing you for misspelling mispelling
       ;;grammar           ; tasing grammar mistake every you make

       :tools
       ;;ansible
       ;;debugger          ; FIXME stepping through code, to help you add bugs
       ;;direnv
       ;;docker
       ;;editorconfig      ; let someone else argue about tabs vs spaces
       ;;ein               ; tame Jupyter notebooks with emacs
       (eval +overlay)     ; run code, run (also, repls)
       ;;gist              ; interacting with github gists
       lookup              ; navigate your code and its documentation
       lsp
       ;;macos             ; MacOS-specific commands
       (magit +forge)      ; a git porcelain for Emacs
       ;;make              ; run make tasks from Emacs
       ;;pass              ; password manager for nerds
       ;;pdf               ; pdf enhancements
       ;;prodigy           ; FIXME managing external services & code builders
       ;;rgb               ; creating color strings
       ;;terraform         ; infrastructure as code
       ;;tmux              ; an API for interacting with tmux
       ;;upload            ; map local to remote projects via ssh/ftp

       :lang
       ;;agda              ; types of types of types of types...
       ;;assembly          ; assembly for fun or debugging
       ;;cc                ; C/C++/Obj-C madness
       ;;clojure           ; java with a lisp
       ;;common-lisp       ; if you've seen one lisp, you've seen them all
       ;;coq               ; proofs-as-programs
       ;;crystal           ; ruby at the speed of c
       ;;csharp            ; unity, .NET, and mono shenanigans
       data              ; config/data formats
       ;;(dart +flutter)   ; paint ui and not much else
       ;;elixir            ; erlang done right
       ;;elm               ; care for a cup of TEA?
       emacs-lisp        ; drown in parentheses
       ;;erlang            ; an elegant language for a more civilized age
       ;;ess               ; emacs speaks statistics
       ;;faust             ; dsp, but you get to keep your soul
       ;;fsharp           ; ML stands for Microsoft's Language
       ;;fstar             ; (dependent) types and (monadic) effects and Z3
       ;;(go +lsp)         ; the hipster dialect
       ;;(haskell +dante)  ; a language that's lazier than I am
       ;;hy                ; readability of scheme w/ speed of python
       ;;idris             ;
       ;;(java +meghanada) ; the poster child for carpal tunnel syndrome
       ;;javascript        ; all(hope(abandon(ye(who(enter(here))))))
       ;;julia             ; a better, faster MATLAB
       ;;kotlin            ; a better, slicker Java(Script)
       ;;latex             ; writing papers in Emacs has never been so fun
       ;;lean
       ;;factor
       ;;ledger            ; an accounting system in Emacs
       ;;lua               ; one-based indices? one-based indices
       markdown          ; writing docs for people to ignore
       ;;nim               ; python + lisp at the speed of c
       ;;nix               ; I hereby declare "nix geht mehr!"
       ;;ocaml             ; an objective camel
       org ;;(org +roam)              ; organize your plain life in plain text
       ;;perl              ; write code no one else can comprehend
       ;;php               ; perl's insecure younger brother
       ;;plantuml          ; diagrams for confusing people more
       ;;purescript        ; javascript, but functional
       ;;python            ; beautiful is better than ugly
       ;;qt                ; the 'cutest' gui framework ever
       ;;racket            ; a DSL for DSLs
       ;;rest              ; Emacs as a REST client
       ;;rst               ; ReST in peace
       ;;(ruby +rails)     ; 1.step {|i| p "Ruby is #{i.even? ? 'love' : 'life'}"}
       rust              ; Fe2O3.unwrap().unwrap().unwrap().unwrap()
       ;;scala             ; java, but good
       ;;scheme            ; a fully conniving family of lisps
       (sh +lsp +fish)     ; she sells {ba,z,fi}sh shells on the C xor
       ;;sml
       ;;solidity          ; do you need a blockchain? No.
       ;;swift             ; who asked for emoji variables?
       ;;terra             ; Earth and Moon in alignment for performance.
       ;;web               ; the tubes

       :email
       ;;(mu4e +gmail)
       ;;notmuch
       ;;(wanderlust +gmail)

       :app
       ;;calendar
       ;;irc               ; how neckbeards socialize
       ;;(rss +org)        ; emacs as an RSS reader
       ;;twitter           ; twitter client https://twitter.com/vnought
       everywhere

       :config
       ;;literate
       (default +bindings +smartparens))

packages.el

;; Useful for position M-x (execute-extended-command) window on screen
(package! ivy-posframe)

;; Simpleclip allows access to system clipboard in a reasonable manner
(package! simpleclip)

;; Use org-journal with org-roam to follow Roam Research workflow
(package! org-journal)

;; Use org-roam-server to visualize org-roam links
(package! org-roam-server)

;; single dired buffer and icons
(package! all-the-icons-dired)
(package! dired-single)

(package! fish-completion
  :recipe (:host gitlab :repo "ambrevar/emacs-fish-completion"))

(package! emacas-0x0
  :recipe (:host gitlab :repo "willvaughn/emacs-0x0"))

(package! org-present)
(package! org-appear)

(package! ox-gemtext
  :recipe (:host nil :repo "https://codeberg.org/woozong/ox-gemtext"))

(package! gemini-mode)
(package! org-preview-html)

(package! ox-hugo)
(package! yaml-mode)
(package! ox-gemini)

(package! org-books)

(package! deadgrep)

(package! vertico)
(package! orderless)
(package! marginalia)
(package! embark)

(package! ace-window)

(package! org-bars
  :recipe (:host github :repo "tonyaldon/org-bars"))

(package! consult)

(package! vertico-posframe)

config.el

(setq doom-theme 'doom-one)

(setq doom-font (font-spec :family "JetBrains Mono" :size 14)
      doom-big-font (font-spec :family "JetBrains Mono" :size 32))

(setq display-line-numbers-type t)

;; CUA type customizations and conveniences=====================================
;; Simpleclip to access system clipboard
(require 'simpleclip)
(setq simpleclip-mode 1)

(map! :gin "C-S-x" #'simpleclip-cut ;Was: C-x chord
      :gin "C-S-c" #'simpleclip-copy ;Was: C-x chord
      :gin "C-S-v" #'clipboard-yank ;freezing on Ubuntu: 'simpleclip-paste ;Was: C-x chord
      :gin "C-z" #'undo ; Was: enable Emacs state
      :gin "C-S-z" #'redo ;Was: C-x chor
      ;; :gin "C-<tab>" #'switch-to-next-buffer ;Was: aya-create snippet
      ;; :gin "C-S-<tab>" #'previous-multiframe-window ;Was: C-x chord
      :gin "C-w" #'kill-buffer ;Was: evil-window-map
      :gin "C-a" #'mark-whole-buffer ;Was: doom/backward-to-bol-or-indent
      )

;; Escape smart-parens after done typing inside
(map! :i "M-;" #'sp-up-sexp) ;Was: comment-dwim

;; Save. Was: isearch-forward
(map! "C-s" #'save-buffer)
;; Save as. Was: nil
(map! "C-S-s" #'write-file)

;; Ctrl shift P like sublime for commands
(map! "C-S-p" #'execute-extended-command)

;; Popup which-key fast
(after! which-key
  (setq which-key-idle-delay 0.1))

;; Org mode related=============================================================
(setq org-directory "~/org/")
(setq org-agenda-files '("~/org/todo.org"))

;; Org files that are refile targets
(setq org-refile-targets (quote (("notes.org" :maxlevel . 1)
                                 ("projects.org" :level . 1)
                                 ("doomNotes.org" :level . 1)
                                 )))

;; Allow text selection by holding down shift key
(setq org-support-shift-select t)

;; Mark when task was completed
(setq org-log-done 'time)

;; Toggle narrow/widen subtree. Was: append-next-kill
(map! "C-M-w" #'org-toggle-narrow-to-subtree)

;; Render modified text only not modifier characters
(setq org-pretty-entities-include-sub-superscripts t)

;; Show images in the org buffers
(setq org-startup-with-inline-images t)

;; Org-roam
(setq org-roam-mode 0)
(setq org-roam-directory "~/org/roam/")
(setq org-roam-buffer "Org-roam Sidebar")
(setq org-roam-buffer-width 0.15)
;;(setq org-roam-buffer-no-delete-other-windows t)
(setq org-roam-link-title-format "∞%s")

;; Org-journal roam integration
;; From @ianjones on doom emacs discord: https://www.ianjones.us/blog/2020-05-05-doom-emacs/#fleeting-notes
(use-package org-journal
      :custom
      (org-journal-dir "~/org/roam/journal/")
      (org-journal-date-prefix "#+TITLE: ")
      (org-journal-file-format "%Y-%m-%d.org")
      (org-journal-date-format "%A, %B %d %Y"))
(setq org-journal-enable-agenda-integration t)
(map! "C-c C-5" #'org-journal-search) ;;was overriding org schedule

(setq org-roam-dailies-capture-templates
      '(("d" "daily" plain #'org-roam-capture--get-point ""
         :immediate-finish t
         :file-name "%<journal/%Y-%m-%d>"
         :head "#+TITLE: %<%Y %B %d, %A>\nTAGS: [[file:dailies.org][∞Dailies]]\n\n*")))

(setq org-roam-capture-templates '(
                                   ("d" "default"
                                    plain
                                    #'org-roam-capture--get-point "%?"
                                    :file-name "%<%Y%m%d>-${slug}"
                                    :head "#+TITLE: ${title}\n"
                                    :unnarrowed t)
                                   ("p" "personal"
                                    plain
                                    #'org-roam-capture--get-point "%?"
                                    :file-name "personal/%<%Y%m%d>-${slug}"
                                    :head "#+TITLE: ${title}\n"
                                    :unnarrowed t)))
;; org-roam-server=====================================
(if (eq system-type 'gnu/linux)
    (use-package org-roam-server
      :ensure t
      :config
      (setq org-roam-server-host "127.0.0.1"
            org-roam-server-port 8008
            org-roam-server-authenticate nil
            org-roam-server-export-inline-images t
            org-roam-server-serve-files nil
            org-roam-server-served-file-extensions '("pdf" "mp4" "ogv")
            org-roam-server-network-poll t
            org-roam-server-network-arrows nil
            org-roam-server-network-label-truncate t
            org-roam-server-network-label-truncate-length 60
            org-roam-server-network-label-wrap-length 20))
  )
;; Posframe customization to position popup=====================================
(require 'ivy-posframe)
;; display at `ivy-posframe-style'
(setq ivy-posframe-display-functions-alist
      '((t . ivy-posframe-display)))
(setq ivy-posframe-display-functions-alist
      '((t . ivy-posframe-display-at-frame-center)))
(ivy-posframe-mode t)

;; Use aspell for spell-checking================================================
(setq-default ispell-program-name "aspell")

;; Speed up frame by loading heavy things when daemon starts
(when (daemonp)
  (require 'org)
  (require 'org-roam)
  (require 'ispell)
  (ispell-start-process))

;; dired config from system builder's emacs from scratch #1
(use-package dired
    :ensure nil
    :commands (dired dired-jump)
    :bind (("C-x C-j" . dired-jump))
    :custom ((dired-listing-switches "-agho --group-directories-first"))
    :config
    (evil-collection-define-key 'normal 'dired-mode-map
      "h" 'dired-single-up-directory
      "l" 'dired-single-buffer))

(use-package dired-single
  :ensure t
  :init
  (require 'dired-single))

(use-package all-the-icons-dired
    :hook (dired-mode . all-the-icons-dired-mode))

;; Magit forge configuration==================================================
(setq auth-sources '("~/.authinfo"))

;; eshell configuration ======================================================
(when (and (executable-find "fish")
           (require 'fish-completion nil t))
  (global-fish-completion-mode))

;; vterm configuration========================================================
(use-package vterm
  :commands vterm
  :config
  (setq term-prompt-regexp "^[^#$%>\n]*[#$%>] *")
  (setq vterm-shell "fish")
  (setq vterm-max-scrollback 10000))

;; org-present configuration from https://github.com/daviwil/dotfiles=========
(defun dw/org-present-prepare-slide ()
  (org-overview)
  (org-show-entry)
  ;(org-show-children)
  )

(defun dw/org-present-hook ()
  (setq-local face-remapping-alist '((default (:height 1.5) variable-pitch)
                                     (header-line (:height 4.5) variable-pitch)
                                     (org-document-title (:height 1.75) org-document-title)
                                     (org-code (:height 1.55) org-code)
                                     (org-verbatim (:height 1.55) org-verbatim)
                                     (org-block (:height 1.25) org-block)
                                     (org-block-begin-line (:height 0.7) org-block)))
  (setq header-line-format " ")
  (org-appear-mode -1)
  (org-display-inline-images)
  (display-line-numbers-mode)
  (dw/org-present-prepare-slide))

(defun dw/org-present-quit-hook ()
  (setq-local face-remapping-alist '((default variable-pitch default)))
  (setq header-line-format nil)
  (org-present-small)
  (org-remove-inline-images)
  (org-appear-mode 1)
  (display-line-numbers-mode))

(defun dw/org-present-prev ()
  (interactive)
  (org-present-prev)
  (dw/org-present-prepare-slide))

(defun dw/org-present-next ()
  (interactive)
  (org-present-next)
  (dw/org-present-prepare-slide))

(use-package org-present
  :after simple
  :after org
  :bind (:map org-present-mode-keymap
         ("C-j" . dw/org-present-next)
         ("C-k" . dw/org-present-prev))
  :hook ((org-present-mode . dw/org-present-hook)
         (org-present-mode-quit . dw/org-present-quit-hook)))

;; Gemini=====================================================================
(require 'ox-gemtext)
(add-hook 'find-file-hook
          (lambda ()
            (when (string= (file-name-extension buffer-file-name) "gmi")
              (gemini-mode +1))))
;; ox-hugo====================================================================
(require 'ox-hugo)
(require 'ox-gemini)

;; Org capture template for Hugo posts
;; https://ox-hugo.scripter.co/doc/org-capture-setup/
(with-eval-after-load 'org-capture
  (defun org-hugo-new-subtree-post-capture-template ()
    "Returns `org-capture' template string for new Hugo post.
See `org-capture-templates' for more information."
    (let* ((title (read-from-minibuffer "Post Title: ")) ;Prompt to enter the post title
           (fname (concat (format-time-string "%Y%m%d_") (org-hugo-slug title))))
      (mapconcat #'identity
                 `(
                   ,(concat "\n* DRAFT " title)
                   ":PROPERTIES:"
                   ,(concat ":EXPORT_FILE_NAME: " fname)
                   ":EXPORT_HUGO_MENU:\n:END:"
                   "%?\n")          ;Place the cursor here finally
                 "\n")))

  (add-to-list 'org-capture-templates
               '("h"                ;`org-capture' binding + h
                 "Hugo post"
                 entry
                 ;; It is assumed that below file is present in `org-directory'
                 ;; and that it has a "Blog Ideas" heading. It can even be a
                 ;; symlink pointing to the actual location of all-posts.org!
                 (file+olp "~/dev/shom.dev/content.org" "Content")
                 (function org-hugo-new-subtree-post-capture-template)
                 :prepend t)))

;; Embark config==============================================================
(use-package embark
  :ensure t
  :bind
  (("C-;" . embark-act)
   ("C-M-;" . embark-dwim)
   ("C-h B" . embark-bindings))

  :init
  (setq prefix-help-command #'embark-prefix-help-command))


;; Org-books==================================================================
(setq org-books-file "~/org/roam/personal/books.org")

;; Vertico ===================================================================
;; Enable vertico
(use-package vertico
  :ensure t
  :init
  (vertico-mode)

  ;; Optionally enable cycling for `vertico-next', `vertico-previous',
  ;; `vertico-next-group' and `vertico-previous-group'.
  (setq vertico-cycle t))

;; Optionally use the `orderless' completion style. See
;; `+orderless-dispatch' in the Consult wiki for an advanced Orderless style
;; dispatcher. Additionally enable `partial-completion' for file path
;; expansion. `partial-completion' is important for wildcard support.
;; Multiple files can be opened at once with `find-file' if you enter a
;; wildcard. You may also give the `initials' completion style a try.
(use-package orderless
  :ensure t
  :custom (completion-styles '(orderless)))
(orderless-define-completion-style orderless+initialism
  (orderless-matching-styles '(orderless-initialism
                               orderless-literal
                               orderless-regexp)))
(setq completion-category-overrides
      '((command (styles orderless+initialism))
        (symbol (styles orderless+initialism))
        (variable (styles orderless+initialism))))

;; Persist history over Emacs restarts. Vertico sorts by history position.
(use-package savehist
  :init
  (savehist-mode))

(use-package marginalia
  :after vertico
  :ensure t
  :custom
  (marginalia-annotators '(marginalia-annotators-heavy marginalia-annotators-light nil))
  :init
  (marginalia-mode))

(use-package consult
  :after vertico)
(map! :gin "C-<tab>" #'consult-buffer
      :gin "C-f" #'consult-ripgrep
      :given "C-i" #'consult-imenu)

;; Window and decoration =====================================================
(set-fringe-mode '(15 . 10))

(use-package ace-window
  :ensure t
  :init
  (global-set-key (kbd "M-q") 'ace-window)
  (setq aw-dispatch-always t)
  (custom-set-faces!
    '(aw-leading-char-face
      :foreground "white" :background "red"
      :weight bold :height 2.5 :box (:line-width 10 :color "red"))))


;; Org-bars configuration ====================================================
(use-package org-bars
  :after org
  :ensure t
  :init
 (defun org-no-ellipsis-in-headlines ()
 "Remove use of ellipsis in headlines.
See `buffer-invisibility-spec'."
  (remove-from-invisibility-spec '(outline . t))
  (add-to-invisibility-spec 'outline))
 (add-hook 'org-mode-hook #'org-bars-mode)
 (add-hook 'org-mode-hook 'org-no-ellipsis-in-headlines))

;; ===========================================================================
(after! tramp
  (setq tramp-inline-compress-start-size 1000)
  (setq tramp-copy-size-limit 10000)
  (setq tramp-verbose 1)
  (setq tramp-default-method "scp")
  (setq tramp-use-ssh-controlmaster-options t)
  (setq tramp-verbose 1))

;; ===========================================================================
(use-package vertico-posframe
  :after vertico
  :ensure t
  :init
  (vertico-posframe-mode 1))

Highland Chewie

I picked up a water color kit for $2 with a brush and eight colors to try something new. I followed along with a tutorial to paint a highland cow and my painting wasn’t that great, but it was fun. Afterwards it felt a little like Chewbacca, so l added a bandolier. Here’s Highland Chewie Chewvaca:

Emacs which-key prefix labels

I’ve been using evil-mode for my Emacs configuration and evil-leader makes the key-mapping very straight-forward with the set-key. I wanted a few functions to be grouped together. SPC is bound as a leader key and there are a few frequent functions bound to single keys, the rest are grouped under other leaders.

However, the secondary leaders show up as x → +prefix and it would nice to give the grouping a name. A lot of suggestions make use of the General.el but at this time I didn’t need anything beyond giving the grouping a name. I was not immediately able to find a way to create an empty keymap to put commands under (I did not know that’s what I needed to do), like this: "e" '("eval" . (keymap))

(evil-leader/set-key
"." 'find-file
"," 'consult-buffer
"'" 'execute-extended-command

"e" '("eval" . (keymap))
"eb" '("buffer" . eval-buffer)
"er" '("region" . eval-region)

"g" '("magit" . (keymap))
"gc" '("commit" . magit-commit)
"gf" '("fetch" . magit-fetch)
"gg" '("status" . magit-status)

"q" '("quit" . (keymap))
"qb" '("buffer" . kill-this-buffer)
"qq" '("save&quit" . save-buffers-kill-terminal)

"h" '("help" . (keymap))
"hf" '("function" . describe-function)
"hk" '("key" . describe-key)
"hv" '("variable" . describe-variable)

"w" '("window" . (keymap))
"wd" '("delete" . delete-window)
"wo" '("delete other" . delete-other-windows)
"ww" '("ace-window" . aw-show-dispatch-help))

Emacs custom configuration

I crafted a custom configuration for Emacs and have been using it for the past few weeks. This is something I didn’t really see myself doing when I started using Emacs, it seemed “very advanced”. This was my journey from a noob to a different kind of noob!

Doom

I started my Emacs journey with Doom and was learning things along the way. This was a great way to get started by borrowing configuration snippets and blindly copying and pasting, which got me up and running quickly. I did find the vastness of Doom intimidating and knew that I wasn’t using all the functionality and wasn’t really discovering it given the jam packed key-maps.

Vanilla Chocolate Swirl

Around the time when I was understanding how to copy paste configs more, there was some community discussion around making Emacs more inviting to new users. As a new user, I felt qualified to collect my thoughts. In an attempt to help new user on-boarding, I even created a literate config that others found useful enough to contribute to. I understood things a lot better through that exercise and felt comfortable enough to try packages by myself.

Back to Doom

Actually, I never left Doom while doing the above exercise. I did not want to invest the time necessary to learn to make my own config. Also, having all the keybindings already setup and showing up nicely in which-key was great. This is also before native-comp was merged into Emacs 28 master branch so I was having issues with compatibility. The final push was that using Doom with all its modules was not performant on a Windows machine that I don’t manage but have to use.

Custom Config

During this time, I kept disabling more and more Doom modules and replacing some of them with other options. There seems to be a lot of excitement about light-weight packages like vertico, consult, etc. that are more single task focused which I started integrating. I started using orderless, embark and ace-window as well and with those, I felt I had enough to use a custom config and be productive. I’m using Chemacs2 to have both my Doom config and my custom config installed but have been exclusively using the config.

I’ll make a few short posts with some of the things I learned in this process with some code snippets as examples.

Sony A7c initial customization

I have been using the Sony A7ii for a long while. It was my first full-frame and mirrorless camera. The A7ii is a great camera and in a lot of ways I have been very spoiled by it. However, when the A7iii came out with fairly significant improvements I resisted the urge to upgrade and decided to wait for the A7iv.

The A7iv was released last week and in many regards is a “perfect” camera. It has made a lot of improvements in an already excellent line-up and is perhaps justifiably is also priced 25% more than it’s predecessor. However, most of the advancements were made in the video department (save for the new 33MP sensor) and that is not the feature set I use. Also, given the shortage of electronic parts it is unclear when pre-orders would get into the hands of users. Ultimately, after waiting for an agonizing 3.5 years after the release of the A7iii, I decided not to buy the A7iv.

Instead, I opted for the A7c which was released in 2020 and is essentially an A7iii in a smaller body. It loses the dual SD cards, 2 custom buttons, and a joystick but gains a fully articulating LCD monitor, compactness, and a range-finder look and feel. I decided to get it with the compact (but quite excellent) 28-60 f/4-5.6 kit lens and make this is my “only” camera for both landscape and underwater photography (with a Seafrog case I intend to get).

All of this preface to get to my main point. I have customized a lot of functions of the A7ii that I no longer remember the “how” or the “why” behind those changes. Here are the main changes I made and replicated on the A7c before the first shoot.

Image from first shoot:

Back-button auto-focus

Decoupling auto-focus from the shutter action allows me to focus on a subject and wait for the right moment to click the shutter without worrying about refocusing/losing focus. This is accomplished in two pieces, first the shutter and auto-focus is decoupled and then the auto-focus function is then remapped to a button on the back of the camera.

Decouple auto-focus from shutter

The current Sony terminology for decoupling auto-focus from shutter is AF w/ shutter. In the A7C it is found in Menu - Camera 1 - AF2 (Page 5).

It is also a good idea to turn off Pre-AF Off which moves the focus point based on the scene changes even before auto-focus is actuated (to improve speed).

NOTE: Shutter half-press is not customizable on the A7c (unlike A7ii)

Remap auto-focus to another button

I’m choosing to use the default AF-On button on the back of the camera for now. But it can be customized to any of the other buttons in Menu - Camera 2 - Custom Operation1 - Custom Key (Page 9).

DRO/Auto HDR

The LCD panel on the camera shows the JPEG preview of the image regardless of whether you’re shooting RAW. The Dynamic Range Optimization (DRO) boosts the shadows and that’s what shows up in the LCD, which in turn means that the RAW is underexposed. The DRO/Auto HDR setting can be turned off in Menu - Camera 1 - Color/WB/Img.Processing1 (Page 11).

This video from Nick Page describes the issue and suggests this fix.

Long Exposure NR

By default the camera attempts to do Noise Reduction (NR) when doing long-exposure. While this does create a lower noise image it comes at the expense of “timing out” for the same length of time the original exposure was. This makes sense since the camera takes a “dark” image with the shutter closed as a noise reference and subtracts it from the original exposure to remove that noise. However, this is a major hindrance for doing multiple longer exposure shots especially as the light is fading. Turning the Long Exposure NR feature off is necessary for those scenarios. The option is found in Menu - Camera 1 - Quality/Image Size2 (Page 2).

NOTE: There’s a related setting for High ISO NR.

NOTE: If attempting proper astrophotography then multiple dark frames are needed anyway for proper post processing.I’ll

These are the main features that would have “bit” me if I didn’t change them before trying to use the camera. If there are others, I will note them here after they bite me.

org-bars view for outlining/folding

I came across this new org-bars package that shows folding indicators and lines to indicate the groupings. In a forum discussion I also discovered that there’s another package org-visual-outline but it actually requires configuring two packages so I didn’t try it.

However, based on the discussion in the comments it seems that org-bars supports narrowing of the buffer (and it works great) which visual-outline does not. I discovered a visual bug and filed a report, so hopefully it’s an easy fix but it’s not a breaking issue.

Here’s the configuration I’m using based on the recommendations in the documentation.

;; Org-bars configuration ====================================================
(use-package org-bars
  :after org
  :ensure t
  :custom
 (defun org-no-ellipsis-in-headlines ()
 "Remove use of ellipsis in headlines.
See `buffer-invisibility-spec'."
  (remove-from-invisibility-spec '(outline . t))
  (add-to-invisibility-spec 'outline))
 (add-hook 'org-mode-hook #'org-bars-mode)
 (add-hook 'org-mode-hook 'org-no-ellipsis-in-headlines))

Here’s a screenshot of my Hugo content buffer showing org-bars:

Wine and CrossOver to use Lightroom on Linux

I have been using Darktable and transitioning away from Adobe’s Lightroom. However, I have almost a decade of edits and experience with Lightroom so doing a cold-turkey switch is proving to be challenging. So I decided to investigate if I could “natively” run Lightroom (without a VM) so I decided to give wine a try.

I use guix as a package manager on Pop!_OS and guix has wine but it did a while to build and install. I needed to download a Lightroom Classic executable but like a lot of software it’s an installer that does the downloading and installing. However, Adobe won’t allow you to download a Windows installer on Linux, so I had to use User-Agent Switcher on Firefox to Windows to even get the installer… this is part of the frustration of software you lease.

wine will ask to install mono installer, which is does by itself and then the same thing with Gecko installer. Overall a pretty smooth process but the Adobe installer failed miserably with a less than helpful error.

Crash Annotation GraphicsCriticalError: |[0][GFX1]: Potential driver version mismatch ignored due to missing DLLs 0.0.0.0 and 0.0.0.0 (t=18.9854
) [GFX1]: Potential driver version mismatch ignored due to missing DLLs 0.0.0.0 and 0.0.0.0

At this point I didn’t have high hopes for avoiding the Windows VM route but wanted to give a quick try to CrossOver. I tried one of their supported applications (Notepad++) and that worked great. They also support “unlisted application” but pointing the Adobe installer led to a lot of nothing, the logs were not very helpful either. At this point I decided to abandon this path and go the VM route to make progress on my actual photo editing goals. However, Steam’s Proton(updated to link to Proton-GE) could be a option to investigate in the future. Are there any other good solutions?

Fully automated deployment

Up until the last commit, the site was being written in Org Mode, exported using ox-hugo, and Hugo was invoked manually to generate the html all locally. The generated public folder was what was being pushed to SourceHut for the deployment. This will be first post which will only commit the actual content in org format only and the full CI/CD will happen on SourceHut.

Using org-publish

I’ll be following along with the System Crafters’ Publishing Website with Org Mode to take full advantage of org-publish and configure multiple outputs (WWW and Gemini). As of the now the following configuration is invoking the correct export function through org-publish but ox-hugo isn’t finding the Hugo sub-trees.

(message "\n==== Exporting Hugo markdown ====")
(setq org-publish-project-alist
      (list
       (list "org-site:main"
             :recursive nil
             :base-directory "./"
             :publishing-function '(org-hugo-export-wim-to-md :all-subtrees nil :visible-only nil)
             :publishing-directory "./public"
             ;; :with-author nil           ;; Don't include author name
             ;; :with-creator t            ;; Include Emacs and Org versions in footer
             ;; :with-toc t                ;; Include a table of contents
             ;; :section-numbers nil       ;; Don't include section numbers
             :time-stamp-file nil)))    ;; Don't include time stamp in file

;; Generate the site output
(org-publish-all t)

(message "\n==== Export complete ====")

RESULTS

Publishing file /home/shom/dev/shom.dev/content.org using `org-hugo-export-wim-to-md'
[ox-hugo] No valid Hugo post subtrees were found

Using hugo-export directly

In order to check sanity and solve the issue, I exported directly with org-hugo-export-wim-to-md which is straightforward since I’ve opted for a single org-content file and it worked as expected. So this is the configuration that is currently building the site:

(message "\n==== Exporting Hugo markdown ====")
(with-current-buffer (find-file "./content.org")
  (org-hugo-export-wim-to-md :all-subtrees nil :visible-only nil))

(message "\n==== Export complete ====")

I would like to get the org-publish route sorted out so I can publish to the Hugo site and the Gemini capsule with a single commit.

Static-site

This entry and all previous ones are taken verbatim from the gemini capsule.

Establishing a web presence

The gemini capsule has been an interesting experiment. In order to be the change I want to see, I will be creating a personal website/blog and the articles will appear both as html and gemtext. All entries prior to this have been made with Android+Termux+emacs. Going forward, the entries won’t strictly be made from Termux.

Static site using org-mode

Org-mode has good html export capabilities and using it with SimpleCss [1] provides a pretty decent standalone webpage. However, creating a site that will auto-generate navigation and headers/footers is a bit more involved.

Hugo / Ox-gemtext

One option is to write the content in org format, use ox-hugo [2] for content export to enable Hugo for the site generation and use a gemtext exporter for gemini. ox-gemtext [3] does not handle gemtext markup for links properly and the generated TOC and section choices are not quite to my liking. Might need to investigate another option. Better to get started than find a perfect solution.

[1] https://simplecss.org [2] https://ox-hugo.scripter.co [3] https://codeberg.org/woozong/ox-gemtext

Framework

I’ve decided to cross post any of my content type toots from Mastodon (Fosstodon instance) here. I’m not sure if there’s a good way to “tag” something aside from filenames so I can generate a toot vs post section? I’ll include that info in the filename and I can investigate automation later.

First impression

I just received my frame.work (i5) DIY edition laptop. I haven’t installed the RAM and NVMe yet (they need to come out from another machine first) but I’m pretty impressed with the build quality so far. The laptop feels solid and the expansion cards fit flush and tight. I’m excited about the 3:2 display, #modular design, and user #replaceable everything. If folks are interested I can share my experience as I get it up and running. #framework 🦣 fosstodon toot

Quick review

Since there was a decent bit of interest, here’s a #framework #laptop update.

1/X:

I opened up the case with the provided multi-tool (T6/phillips reversible bit with a spudger at the other end). The screws on the case are captive, which is great no worries about losing one. 1 of the 5 screws wasn’t perfectly aligned but posed no issues, build is solid. The spudger allows the keyboard to be lifted (top view under keyboard shown). The QR codes link to documentation (which has a lot of photos).

2/X:

The top plate is secured in place with magnets (gluing isn’t the answer!) and feels very secure and still easy to remove with spudger. Only a single ribbon cable connects the top to the main board and it has plenty of play so working with it was comfortable and easy to detach and reattach. Installing the components, RAM (which I had) and wifi-card (which I bought from frame.work) was very easy. I’d even say easier than desktop since everything is open and flat. I’ll install the NVMe later.

3/X:

They supplied an advisory to put some insulating tape under the touchpad cable to prevent a potential short (similar issue to pre-2012 Macbook SATA cable, could potentially rub over time and short). I appreciated the note and put down some electrical tape, photo attached, it was #righttorepair working in the manufacturer favor. The advisory sheet also had a couple stickers on it, which was cool. All other hardware looks good and fits well. I’m not a huge fan of the keyboard, a bit mushy.

4/4:

I have it configured with 2 USB-C ports, 1 USB-A, and 1 microSD card reader. Booting up went fine, I installed #pop_os from flash drive to microSD v90, suprisingly fast. The BIOS (needed to disable secure boot) splash screen doesn’t show FN key legend for boot device (F12) / BIOS (? I mashed a lot of FN keys), so that would be a nice touch to add. Having WiFi issues, will investigate later. But everything else feels snappy and display looks good (3:2 ratio!!). Happy to answer questions!

🦣 fosstodon toot

Video Editing

I’m not a video person, I’ve made an occasional slideshow but even that has been over a decade ago. Recently, I needed to edit a video of an event and splice in the live recording with some pre-recorded segments and stitch it all together with some simple transitions. I didn’t know what the “standard” FOSS offering was when it came to video editing. For photography I would go to Dark Table, GIMP for images, but what for video? I knew that Blender was an option but I wanted something with less steep of a learning curve even if that meant it was less featured. Luckily, there are several articles listing and comparing options, Shotcut and KDEnlive seemed to be the two worth investigating.

Shotcut

I started with Shotcut because it seemed like the lighther of the two packages. There were ample and good tutorials on YouTube that weren’t too out of date, which got me up and running fairly quickly. Though I’m not a video person, I’m familiar with multi track editing and transforms, I just needed to know where the tools were and what they were called.

The interface was fairly intuitive after watching a short tutorial to get familiarized. It did slow down, choke, and crash a few times when working with my 3.5 hour long source video. The timeline zooming in and out (to make precise cuts) were a bit painful until I could cut everything into clips. I quickly learned their keyframe driven actions and also learned how to make a picture-in-picture transform. With all manipulations being filters, configuring transitions was also pretty easy.

However, the problem was stability. It crashed a few more times and then wouldn’t preview the transitions at all unless I removed them and re-did them. Finally, it had lots of issues exporting (a big one was it having path issues that likely was more of a guix package manager complication).

KDEnlive

I turned to KDEnlive after it was recommended by the System Crafters community [1]. The workflow was very familiar between the two programs. Both of them support rearranging panels to customize the UI, that also helped in coming up to speed. The filters/transforms work a bit differently but there are again plenty of YouTube tutorials. KDEnlive has a lot more functionality it seems but it doesn’t force you to use any of it to accomplish your task. It was also noticeably more stable (no crashes even with a 2.25 hour video render) and snappy (the timeline editing and zooming was never laggy or froze up the UI).

All in all, I would say both are good but I personally had a better experience with KDEnlive, even though I much preferred the key framing UI of Shotcut. The stability issues of Shotcut could very well be specific to my machine and environment, so I’m not writing it off and might revisit it. KDEnlive produced a quality video where the editing seems passably professional, which is a testament to the tool and not my skill. It’s amazing to have such high quality FOSS software, thank you devs!

[1] https://systemcrafters.net/community/

Termux + Emacs

One of the biggest advantages of Android is that it runs on Linux and Termux [1] is a very capable terminal emulator. Until pine phone (which I own for playing around on) type devices reach daily driver maturity (to use with job related corporate apps etc.) Termux is my only realistic option to run Linux terminal apps on the phone.

This is where being able to use Emacs as a TUI application on Termux is great. I actually run a slightly tweaked version of my desktop config which relies of doom-emacs [1]. I don’t do anything complex but the editing experience is as comfortable as most other phone app. The editing experience is helped by evil bindings which are more key-sequence driven rather than key-combo driven. Although modifier keys are supported by Termux and a two row soft keyboard is fully customizable, evil bindings are easier. My config for the softkeys is shown here:

extra-keys = [ \
    ['ESC','|','/','HOME','UP','END','~','DEL'], \
    ['TAB','CTRL','ALT','LEFT','DOWN','RIGHT',':','<'] \
]

Also swiping over the soft keyboard reveals a text entry field which is a standard Android text field so text auto correct, gesture typing, and other keyboard features work as expected and is a lot more convenient for typing.

In fact this whole gemini capsule and all entries (so far) were created using Termux + Emacs including setting up the CI/CD for sr.ht.

While I use this setup for capturing notes in org mode, for tasks management I use Orgzly. That’s another topic but in an even smaller nutshell, the biggest benefit is system notifications.

[1] https://termux.com [2] https://github.com/hlissner/doom-emacs.

Latte Art

Getting into home espresso has been fun, it’s a rabbit hole like most fun things. And with rabbit holes it’s best to decide how far you’re willing to fall in before really starting down that hole. For me, it was an used Rancilio Silvia modded with a temperature PID controller and a refurbished Baratza Vario. At this level it’s definitely possible to get good espresso and latte art if one tries, gets lucky, and practices… it’s a good balance. Making one or maximum of two drinks most days is nearly not enough practice especially considering missed practice due to travel. But today was a lucky day, I got the best pour I’ve managed and it’s exciting to see what’s possible at the lower/est end of prosumer equipment.

Gemini Capsule

With a certain nostalgia for the early web ever present, a Gemini site (gemite? edit: I’ve learned it’s called a capsule 💊🚀) is a good spiritual successor. Albeit much fewer under construction gifs and red text on black background…

gemini://gem.shom.dev