Move to org mode for posts

main
Elnu 1 year ago
parent 3a8dd659dd
commit 02f7e7a1a0

@ -0,0 +1,7 @@
((nil . ((eval . (add-hook 'after-save-hook
(lambda ()
(when (string= (file-name-extension (buffer-file-name)) "org")
(setq default-directory (file-name-directory (locate-dominating-file "." ".dir-locals.el")))
(load (concat default-directory "build.el"))
nil
t)))))))

@ -0,0 +1 @@
use nix

3
.gitignore vendored

@ -4,4 +4,5 @@
hugo_stats.json
/.hugo_build.lock
*.xcf
stork-ubuntu-20-04-1.5.0
/content/**/*.md
.packages

@ -0,0 +1,91 @@
;; Only for running rebuild from within Emacs
(setq original-buffer (current-buffer))
;; Set working directory to the directory of this script
(setq working-directory (file-name-directory load-file-name))
(setq default-directory working-directory)
;; Set ~/.emacs.d/elpa path. ./.packages is relative to default-directory
(require 'package)
(setq package-user-dir (expand-file-name "./.packages"))
(setq package-archives '(("melpa" . "https://melpa.org/packages/")
("elpa" . "https://elpa.gnu.org/packages/")))
;; Initialize the package system
(package-initialize)
(unless package-archive-contents
(package-refresh-contents))
;; Install dependencies
(package-install 'ox-hugo)
;; Set Hugo directory
;; we can't use the content directory here because we'll need to rename files
;; When running hugo server, this would cause unnecessary rebuilds
(setq org-hugo-base-dir "/tmp/ox-hugo/")
;; Make sure ~key-binding~ is rendered as <kbd>key-binding</kbd> not `key-binding`
(setq org-hugo-use-code-for-kbd t)
;; Create required static folder
(make-directory (concat org-hugo-base-dir "static") :parents)
;; Delete old markdown files
(dolist (file (directory-files-recursively "content" "\\.md$"))
(unless (file-exists-p (concat (file-name-sans-extension file) ".org"))
(delete-file file)))
;; Generate
(dolist (file (directory-files-recursively "content" "\\.org$"))
;; Open file
(find-file file)
;; Convert to markdown
(org-hugo-export-wim-to-md)
;; Store markdown file name: path/to/file.org -> file.md
(setq file-name (concat (file-name-sans-extension (file-name-nondirectory file)) ".md"))
;; Reset default-directory, it becomes the parent of the file
(setq default-directory working-directory)
;; Target markdown file
(setq target (concat (file-name-directory file) file-name))
;; Check if creating new file
(setq new (not (file-exists-p target)))
;; Make file writable
(unless new (set-file-modes target #o644))
;; Open temporary buffer
(with-temp-buffer
;; Load generated markdown
(insert-file-contents (concat org-hugo-base-dir "content/posts/" file-name))
;; Fix image paths: /ox-hugo/image.png -> image.png
(while (re-search-forward "/ox-hugo/\\(.*\\)" nil t)
(replace-match "\\1"))
;; Fix image formatting
;; Write to target unless generated markdown matches old content
;; It would be simpler to run file-equal-p,
;; but since we already have one of the files loaded that would be inefficient
(unless (and (not new) (equal (buffer-string) (with-temp-buffer
(insert-file-contents target)
(buffer-string))))
(write-region (point-min) (point-max) target nil)
))
;; Make file read-only
(set-file-modes target #o444)
;; Close file unless original buffer
(unless (eq (current-buffer) original-buffer)
(kill-buffer)))
;; Clean up temporary directory
(delete-directory org-hugo-base-dir t)
;; Switch back to original buffer
(switch-to-buffer original-buffer)

@ -1,4 +1,5 @@
#!/bin/bash
emacs -Q --script build.el
hugo
./stork_index.sh
stork build --input "public/stork.config.json" --output "public/home.st"
find public -name '*.html' -type f -exec tidy --quiet yes --drop-empty-elements no --hide-comments yes -mq '{}' \; &> /dev/null

@ -4,6 +4,7 @@ title = "Elnu's Blog"
theme = 'sakura-hugo-theme'
enableEmoji = true
paginate = 3
ignoreFiles = ['\.org$']
[params]
Author = 'Elnu'

@ -1,17 +0,0 @@
---
title: Elnus Blog
---
> 年暮ぬ笠きて草鞋はきながら
<a target="_blank" href="https://www.madoka-magica.com/tv/"><img src="kyoko.webp" style="float: right; margin: 0.5em"></a>
Hello! I go by Elnu on the internet, and this is my blog, I hope you find something of interest here. Im 17, Im interested in programming, GNU/Linux, studying Japanese, watching anime, drawing, and creative writing. Im horrible at doing things consistently.
For now, Ill be posting small Linux and programming-related posts explaining how to do various things that I happen to find useful. In the future, Ill post larger, more interesting posts. Stay tuned! (〃^▽^〃)
##### Quick links
:right_arrow: :books: [Post archive](/posts)\
:right_arrow: :flags: [#nihongo](/tags/nihongo) Japanese writing\
:right_arrow: :floppy_disk: [#programming](/tags/programming) Programming

@ -0,0 +1,17 @@
#+TITLE: Elnu's Blog
#+begin_quote
年暮ぬ笠きて草鞋はきながら
#+end_quote
Hello! I go by Elnu on the internet, and this is my blog, I hope you find something of interest here. I'm 17, I'm interested in programming, GNU/Linux, studying Japanese, watching anime, drawing, and creative writing. I'm horrible at doing things consistently.
For now, I'll be posting small Linux and programming-related posts explaining how to do various things that I happen to find useful. In the future, I'll post larger, more interesting posts. Stay tuned! (〃^▽^〃)
* Quick links
:PROPERTIES:
:CUSTOM_ID: quick-links
:END:
:right_arrow: :books: [[/posts][Post archive]]\\
:right_arrow: :flags: [[/tags/nihongo][#nihongo]] Japanese writing\\
:right_arrow: :floppy_disk: [[/tags/programming][#programming]] Programming

@ -1,34 +0,0 @@
---
title: "Automatically Update Matomo Excluded IPs"
date: 2022-02-28
tags:
- programming
description: "Going into Matomo to manually update excluded IPs to prevent your own visits from being counted is a pain. Learn how to write a command that updates them so you dont have to!"
---
> :warning: Edited from [my forum posts on Matomo forums](https://forum.matomo.org/t/cant-find-global-website-settings-global-excluded-ips-etc-in-database-nor-configuration-files/44888)
I have a dynamic IP address, and every time it changes I had to manually go into [Matomo](https://matomo.org/) (a free and open-source self-hosted Google Analytics alternative that is endorsed by the EU and has [<abbr title="General Data Protection Regulation">GDPR</abbr>](https://en.wikipedia.org/wiki/General_Data_Protection_Regulation)-compliant privacy tools), check to see if it logged any of my own visits, delete them in the GDPR tools, and update my IP in the general website settings. It was moderately annoying to do, and after doing this every once and a while for a year I wanted to find a way to automate this.
Unfortunately, I couldn't find where the global list of excluded IPs field is in neither the PHP configuration files nor the [Matomo database schema](https://developer.matomo.org/guides/database-schema).
However, I managed to find a workaround. While I couldn't find the global excluded IP field in any of the database tables, I was able to find the site-specific `excluded_ips` column in the `sites` table. Instead of using the global excluded IP field, I decided to make a script that automatically sets the excluded IPs for all of the available sites at once.
Here's what I did if anyone else encounters the same issue.
I made the following template SQL script, `excluded_ips_updater.sql`, that excludes all visits from an empty wildcard field, `{}`. This is where I format in my public IP address later. (In my database, all of the table names are prefixed with `matomo_`, but this might not be the case for you.)
```SQL
USE matomo;
UPDATE matomo_site SET excluded_ips="{}";
```
Then, in order for this to work, I run the following Bash command. What this does is `curl`s the contents of the [My IP API](https://api.my-ip.io/ip) to get our public IP address (the `-s` flag makes `curl` silent), replaces the `{}` in the SQL script with the public IP, and finally pipes into MySQL using my login credentials. Not that there is **no space** between the `-p` flag and your password.
```SH
sed "s/{}/$(curl -s https://api.my-ip.io/ip)/g" excluded_ips_updater.sql | mysql -u matomo -pmatomoUserPassword
```
And that's done! One can get this to run automatically on boot or every X amount of time using a cronjob or similar.
I'd still like to know whether or not there's a way to do this using the global excluded IPs list as it would be a lot more elegant, but this works for now. I hope this might be useful to anyone who ran into the same problem as I did.

@ -0,0 +1,33 @@
#+TITLE: Automatically Update Matomo Excluded IPs
#+DATE: 2022-02-28
#+FILETAGS: programming
#+DESCRIPTION: Going into Matomo to manually update excluded IPs to prevent your own visits from being counted is a pain. Learn how to write a command that updates them so you dont have to!
#+begin_quote
:warning: Edited from [[https://forum.matomo.org/t/cant-find-global-website-settings-global-excluded-ips-etc-in-database-nor-configuration-files/44888][my forum posts on Matomo forums]]
#+end_quote
I have a dynamic IP address, and every time it changes I had to manually go into [[https://matomo.org/][Matomo]] (a free and open-source self-hosted Google Analytics alternative that is endorsed by the EU and has [[https://en.wikipedia.org/wiki/General_Data_Protection_Regulation][GDPR]]-compliant privacy tools), check to see if it logged any of my own visits, delete them in the GDPR tools, and update my IP in the general website settings. It was moderately annoying to do, and after doing this every once and a while for a year I wanted to find a way to automate this.
Unfortunately, I couldn't find where the global list of excluded IPs field is in neither the PHP configuration files nor the [[https://developer.matomo.org/guides/database-schema][Matomo database schema]].
However, I managed to find a workaround. While I couldn't find the global excluded IP field in any of the database tables, I was able to find the site-specific =excluded_ips= column in the =sites= table. Instead of using the global excluded IP field, I decided to make a script that automatically sets the excluded IPs for all of the available sites at once.
Here's what I did if anyone else encounters the same issue.
I made the following template SQL script, =excluded_ips_updater.sql=, that excludes all visits from an empty wildcard field, ={}=. This is where I format in my public IP address later. (In my database, all of the table names are prefixed with =matomo_=, but this might not be the case for you.)
#+begin_src sql
USE matomo;
UPDATE matomo_site SET excluded_ips="{}";
#+end_src
Then, in order for this to work, I run the following Bash command. What this does is =curl=s the contents of the [[https://api.my-ip.io/ip][My IP API]] to get our public IP address (the =-s= flag makes =curl= silent), replaces the ={}= in the SQL script with the public IP, and finally pipes into MySQL using my login credentials. Not that there is *no space* between the =-p= flag and your password.
#+begin_src sh
sed "s/{}/$(curl -s https://api.my-ip.io/ip)/g" excluded_ips_updater.sql | mysql -u matomo -pmatomoUserPassword
#+end_src
And that's done! One can get this to run automatically on boot or every X amount of time using a cronjob or similar.
I'd still like to know whether or not there's a way to do this using the global excluded IPs list as it would be a lot more elegant, but this works for now. I hope this might be useful to anyone who ran into the same problem as I did.

@ -1,50 +0,0 @@
---
title: "Cleaning Up HTML with Tidy"
date: 2022-01-24
tags:
- programming
description: "Static site generators such as Hugo create incredibly messy HTML output. Luckily, HTML tidy can make your ugly markup pristine, even removing comments! Lets learn how to use tidy and configure it to your needs."
---
One issue with using static site generators that use templates like [Hugo](https://gohugo.io) is that their generated HTML files are often incredibly messy, with bad indentation, tons of unneeded whitespace, and overall unconcise formatting. While it *is* technically possible to fix this in your templates, it takes a lot of effort, makes your templates less readable, and frankly is a waste of time.
To my knowledge, Hugo and most other generators don't have built-in beautifiers. The reasoning for this is usually that since your HTML is hidden from your end-user, it doesn't matter if it isn't clean and not nicely formatted. While this is valid, if you're a obsessive perfectionist (like myself, unfortunately) you probably still want clean markup.
In addition, if your site or blog has a more technical audience that might possibly view your site's source, you're going to give a better impression if you have clean markup.
## HTML Tidy
`tidy` is the solution for this. Started in 2003 by Dave Raggett, it provides a simple command line tool with [a large number of configuration options](http://api.html-tidy.org/tidy/tidylib_api_5.2.0/tidy_config.html) for how you want your HTML to be "tidied." It is free and open source, with its source code available on [GitHub](https://github.com/htacg/tidy-html5).
On Debian-based GNU/Linux distributions, you can install it with:
```SH
sudo apt install tidy
```
For other platforms, see [HTML Tidy's official website](https://www.html-tidy.org/).
It's most simple usage is as follows, with first the output file being declared with the `-o` flag and then the input file at the end:
```
tidy -o output.html input.html
```
However, the default configuration will probably not be to your liking. We'll go over configuration in the next section.
## My `tidy` configuration for usage with Hugo
I have created a simple, one line command for first building my site with `hugo`, then cleaning up all of the generated files by first using `find` to get all `*.html` files in `public` (where Hugo dumps generated content), and then finally running `tidy` on each file.
```SH
hugo && find public -path "*.html" -type f -exec tidy --quiet yes --drop-empty-elements no -o {} {} \;
```
I have a couple configuration settings:
- `--quiet yes`: Since I'm parsing a large number of files, this prevents `tidy` from spitting out informational text every time the command is run.
- `--drop-empty-elements no`: In this site, I use the [FontAwesome](https://fontawesome.com/) icon set (which is fantastic, I definitely recommend it), which encodes icons as empty `<i>` tags with classes applied. By default, `tidy` removes any empty tags with no content, which removes all my icons, so I need to have this disabled.
And that's it! Now, every time I update my Hugo site, I can run this command and not have to worry about Hugo's messy HTML output.
I hope this helped anyone out there who dislikes dirty, unconcise HTML as much as I do!

@ -0,0 +1,51 @@
#+TITLE: Cleaning Up HTML with Tidy
#+DATE: 2022-01-24
#+FILETAGS: programming
#+DESCRIPTION: Static site generators such as Hugo create incredibly messy HTML output. Luckily, HTML tidy can make your ugly markup pristine, even removing comments! Lets learn how to use tidy and configure it to your needs.
One issue with using static site generators that use templates like [[https://gohugo.io][Hugo]] is that their generated HTML files are often incredibly messy, with bad indentation, tons of unneeded whitespace, and overall unconcise formatting. While it /is/ technically possible to fix this in your templates, it takes a lot of effort, makes your templates less readable, and frankly is a waste of time.
To my knowledge, Hugo and most other generators don't have built-in beautifiers. The reasoning for this is usually that since your HTML is hidden from your end-user, it doesn't matter if it isn't clean and not nicely formatted. While this is valid, if you're a obsessive perfectionist (like myself, unfortunately) you probably still want clean markup.
In addition, if your site or blog has a more technical audience that might possibly view your site's source, you're going to give a better impression if you have clean markup.
* HTML Tidy
:PROPERTIES:
:CUSTOM_ID: html-tidy
:END:
=tidy= is the solution for this. Started in 2003 by Dave Raggett, it provides a simple command line tool with [[http://api.html-tidy.org/tidy/tidylib_api_5.2.0/tidy_config.html][a large number of configuration options]] for how you want your HTML to be "tidied." It is free and open source, with its source code available on [[https://github.com/htacg/tidy-html5][GitHub]].
On Debian-based GNU/Linux distributions, you can install it with:
#+begin_src sh
sudo apt install tidy
#+end_src
For other platforms, see [[https://www.html-tidy.org/][HTML Tidy's official website]].
It's most simple usage is as follows, with first the output file being declared with the =-o= flag and then the input file at the end:
#+begin_example
tidy -o output.html input.html
#+end_example
However, the default configuration will probably not be to your liking. We'll go over configuration in the next section.
* My =tidy= configuration for usage with Hugo
:PROPERTIES:
:CUSTOM_ID: my-tidy-configuration-for-usage-with-hugo
:END:
I have created a simple, one line command for first building my site with =hugo=, then cleaning up all of the generated files by first using =find= to get all =*.html= files in =public= (where Hugo dumps generated content), and then finally running =tidy= on each file.
#+begin_src sh
hugo && find public -path "*.html" -type f -exec tidy --quiet yes --drop-empty-elements no -o {} {} \;
#+end_src
I have a couple configuration settings:
- =--quiet yes=: Since I'm parsing a large number of files, this prevents =tidy= from spitting out informational text every time the command is run.
- =--drop-empty-elements no=: In this site, I use the [[https://fontawesome.com/][FontAwesome]] icon set (which is fantastic, I definitely recommend it), which encodes icons as empty =<i>= tags with classes applied. By default, =tidy= removes any empty tags with no content, which removes all my icons, so I need to have this disabled.
And that's it! Now, every time I update my Hugo site, I can run this command and not have to worry about Hugo's messy HTML output.
I hope this helped anyone out there who dislikes dirty, unconcise HTML as much as I do!

@ -1,112 +0,0 @@
---
title: "Creating a Firefox PWA for Discord on Linux"
date: 2022-07-04
tags:
- programming
---
In this tutorial, Ill explain how to set up, install, and configure a Firefox [PWA](https://en.wikipedia.org/wiki/Progressive_web_application) (Progressive Web Application) for Discord that integrates into your system, and some background on why youd want to do so over the regular desktop version. Ive tested these steps on Arch Linux and Xubuntu 20.04.
## Background
If youve ever used Discord on Linux, you probably know that it is clunky and not nearly as polished or feature-complete as it is on, say, Windows.
Streaming has no audio, and while using [Soundux](https://soundux.rocks/) as a workaround for audio passthrough is a solution, it can be annoying to use, and doesnt always work. For example, in my experience, Soundux fails to pick up audio from Java Minecraft (and I believe by extension all Java applications). In addition, Ive recently had issues with audio output, with no sound being produced other than voices in VC.
Another major issue with desktop Discord is the fact it isnt sandboxed at all, meaning it has full access to your system at any time. In addition, if youre desktop is using X.org, Discord (and any other application, for that matter) is able to snoop on all key inputs, even if theyre from applications. Make of this what you will.
Not to mention the issues with the updater, which has been excellently covered in [this video by Brodie Robertson](https://www.youtube.com/watch?v=3OEfr7L-gUk).
There is of course a second way to use Discord on Linux, which is simply the web application. It does everything that the desktop application does, including streaming (albeit without audio), and it doesnt ever require being updated. The only potential downside is the lack of push-to-talk, but if like me this is something you dont use, then this isnt an issue. In addition, since its just a web page not a fully-fledged application it doesnt have the security/privacy concern of being able to access your entire system like the desktop version.
The only frustration I have with the web version is the fact that it doesnt look like a standalone application on your desktop, its just a tab in your browser. Being able to remove as much of the top bar as possible would be nice. This is where PWAs come into play.
Unfortunately, support for SSB (Site Specific Browser) mode and PWAs was [dropped](https://bugzilla.mozilla.org/show_bug.cgi?id=1682593) in Firefox 86 last year.
Luckily, **Progressive Web Apps for Firefox** ([filips123/PWAsForFirefox](https://github.com/filips123/PWAsForFirefox)) exists to fill the lapse in functionality.
> This project creates a custom modified Firefox runtime to allow websites to be installed as standalone apps and provides a console tool and browser extension to install, manage and use them.
## Installation
First, install the [PWAs for Firefox extension from the Firefox add-ons store](https://addons.mozilla.org/en-US/firefox/addon/pwas-for-firefox/). Once you open it up, it will guide you through the installation process, including installing the companion application. If youre on Arch Linux, its available on the AUR under `firefox-pwa` and `firefox-pwa-bin`. (`bin` for the binary precompiled version, go for this one if you dont want to compile it yourself.)
**If you are on Ubuntu or one of its derivatives,** the snap version of Firefox that came with your system will not be able to communicate with the companion application, Im assuming due to snaps containerization, at least in my testing on Xubuntu 22.04. See [this Stack Overflow answer](https://askubuntu.com/a/1404401) for a guide on switching to the standard Debian package version of Firefox.
## Create the Discord PWA
Navigate to Discord [https://discord.com](https://discord.com/channels/@me) (**not the app**, we need the root of the PWA to be the index page so all pages under the Discord domain are considered part of the PWA) and click on *Install current site* in the extension. Change the *Name* field of the PWA to just “Discord”, the default will include the tagline and be a bit overly long. Change the *Start URL* to [https://discord.com/app](https://discord.com/app) so the PWA opens to the app page, not the landing page. And... youre done! You can leave all the other fields as-is and press *Install web app*. Youve created a PWA for Firefox, its as simple as that. You can now open it from within the extension by clicking on the external link button.
However, theres more configuration to be done. Firefox PWA creates a Firefox profile for the PWAs thats completely separate from your main profile, so were going to have to do some configuration there. (By default, all of your PWAs share a single *Default* profile under the *Profiles* tab of the extension, but if you want to have different PWAs have different profiles you can configure it there.)
## Configure the PWA Firefox profile
Its up to you how want to configure the profile, but heres what I did and recommend.
- In the :gear: *General* portion, theres a new settings section, *Progressive Web Apps*. For me I checked *Open out of scope URLs in a default browser,* which makes it so non-Discord links you visit within the PWA profile are opened in your main Firefox profile instead of within the PWA. This is also important considering any external sites you visit within the PWA arent otherwise going to be added to your main profiles history.
- Under :jigsaw: *Extensions & Themes*, you You might also want to [choose a theme that matches Discords theming](https://addons.mozilla.org/en-US/firefox/addon/discord-dark-rebrand/).
- Under :lock: *Privacy & Security* / *History*, set that history to *Use custom settings* for history and uncheck all the boxes. This will make it so the profile wont keep any history (which is unnecessary in a PWA), but still will remember cookies so you dont have to log in again each time you reopen it.
If you shut down your computer with the PWA open, Firefox will think that your previous session didnt close properly and will reopen your old windows. I havent figured out a way to prevent this, but if you want to mess with advanced configuration, heres how.
Within the PWA, we dont have access to the URL bar, so to enter the preferences, press <kbd>Ctrl</kbd> + <kbd>Shift</kbd> + <kbd>I</kbd> to enter the developer tools. Enter the console and type in the following:
```JS
document.location.href = "about:config"
```
Firefox will make you type `allow pasting` beforehand.
## Launching the PWA from the command line
**If you are using a distro with a desktop environment,** Discord should already be in your applications drawer, so unless you want to be able to launch Discord from the command line, you can skip this step.
I have uninstalled the desktop Discord application `discord`, and I want to replace it with a shell script that launches the PWA. This will enable you to launch the PWA from the command line simply by typing `discord`, and also from an application launcher like [rofi](https://github.com/davatorium/rofi). This way, we dont have to rely on launching Discord from the PWAsForFirefox extension.
Luckily, PWAs for Firefox provides a CLI. First, we need to find the ID of the Discord PWA:
```SH
firefoxpwa profile list
```
This will output the following. After the entry for *Discord*, the string of letters and numbers is the site ID. Your ID will be different.
```
========================= Default ==========================
Description: Default profile for all sites
ID: 00000000000000000000000000
Sites:
- Discord: https://discord.com/ (01G705MTCT1Q1HHFM7DVDFAPCY)
```
You can now launch the PWA using the following command.
```SH
firefoxpwa site launch 01G705MTCT1Q1HHFM7DVDFAPCY
```
Lets create the launch script. Open `/usr/bin/discord` (if this exists already, uninstall the desktop Discord application) with your favorite text editor. Youll need to use `sudo`. Add the following to the file, replacing the site ID with the one you found earlier:
```SH
#!/usr/bin/env bash
firefoxpwa site launch 01G705MTCT1Q1HHFM7DVDFAPCY
```
Finally, make it an executable:
```SH
sudo chmod +x /usr/bin/discord
```
Youre done! Youve created a PWA for Discord that looks and behaves like a desktop application, but without a lot of the hassle involved with the actual desktop app.
Note that if you decide to reinstall the normal Electron-based desktop Discord application, youll have to move/delete `/usr/bin/discord` first, as desktop Discord needs to put its executable there.
## Conclusion
Of course, this isnt a perfect solution. Dragging and dropping files into the PWA doesnt work for uploading, there might be some improved voice chat performance in the normal desktop version not available in the browser/PWA version, push-to-talk doesnt work, and streaming still has no audio.
However, if youve already been using primarily the browser version of Discord, creating a PWA is a nice upgrade until Discord finally makes a good client. Heres to hoping that day actually comes.
Anyhow, I hope this was helpful!

@ -0,0 +1,124 @@
#+TITLE: Creating a Firefox PWA for Discord on Linux
#+DATE: 2022-07-04
#+FILETAGS: programming
In this tutorial, I'll explain how to set up, install, and configure a Firefox [[https://en.wikipedia.org/wiki/Progressive_web_application][PWA]] (Progressive Web Application) for Discord that integrates into your system, and some background on why you'd want to do so over the regular desktop version. I've tested these steps on Arch Linux and Xubuntu 20.04.
* Background
:PROPERTIES:
:CUSTOM_ID: background
:END:
If you've ever used Discord on Linux, you probably know that it is clunky and not nearly as polished or feature-complete as it is on, say, Windows.
Streaming has no audio, and while using [[https://soundux.rocks/][Soundux]] as a workaround for audio passthrough is a solution, it can be annoying to use, and doesn't always work. For example, in my experience, Soundux fails to pick up audio from Java Minecraft (and I believe by extension all Java applications). In addition, I've recently had issues with audio output, with no sound being produced other than voices in VC.
Another major issue with desktop Discord is the fact it isn't sandboxed at all, meaning it has full access to your system at any time. In addition, if you're desktop is using X.org, Discord (and any other application, for that matter) is able to snoop on all key inputs, even if they're from applications. Make of this what you will.
Not to mention the issues with the updater, which has been excellently covered in [[https://www.youtube.com/watch?v=3OEfr7L-gUk][this video by Brodie Robertson]].
There is of course a second way to use Discord on Linux, which is simply the web application. It does everything that the desktop application does, including streaming (albeit without audio), and it doesn't ever require being updated. The only potential downside is the lack of push-to-talk, but if like me this is something you don't use, then this isn't an issue. In addition, since it's just a web page not a fully-fledged application it doesn't have the security/privacy concern of being able to access your entire system like the desktop version.
The only frustration I have with the web version is the fact that it doesn't look like a standalone application on your desktop, it's just a tab in your browser. Being able to remove as much of the top bar as possible would be nice. This is where PWAs come into play.
Unfortunately, support for SSB (Site Specific Browser) mode and PWAs was [[https://bugzilla.mozilla.org/show_bug.cgi?id=1682593][dropped]] in Firefox 86 last year.
Luckily, *Progressive Web Apps for Firefox* ([[https://github.com/filips123/PWAsForFirefox][filips123/PWAsForFirefox]]) exists to fill the lapse in functionality.
#+begin_quote
This project creates a custom modified Firefox runtime to allow websites to be installed as standalone apps and provides a console tool and browser extension to install, manage and use them.
#+end_quote
* Installation
:PROPERTIES:
:CUSTOM_ID: installation
:END:
First, install the [[https://addons.mozilla.org/en-US/firefox/addon/pwas-for-firefox/][PWAs for Firefox extension from the Firefox add-ons store]]. Once you open it up, it will guide you through the installation process, including installing the companion application. If you're on Arch Linux, it's available on the AUR under =firefox-pwa= and =firefox-pwa-bin=. (=bin= for the binary precompiled version, go for this one if you don't want to compile it yourself.)
*If you are on Ubuntu or one of its derivatives,* the snap version of Firefox that came with your system will not be able to communicate with the companion application, I'm assuming due to snaps' containerization, at least in my testing on Xubuntu 22.04. See [[https://askubuntu.com/a/1404401][this Stack Overflow answer]] for a guide on switching to the standard Debian package version of Firefox.
* Create the Discord PWA
:PROPERTIES:
:CUSTOM_ID: create-the-discord-pwa
:END:
Navigate to Discord [[https://discord.com/channels/@me][https://discord.com]] (*not the app*, we need the root of the PWA to be the index page so all pages under the Discord domain are considered part of the PWA) and click on /Install current site/ in the extension. Change the /Name/ field of the PWA to just "Discord", the default will include the tagline and be a bit overly long. Change the /Start URL/ to [[https://discord.com/app]] so the PWA opens to the app page, not the landing page. And... you're done! You can leave all the other fields as-is and press /Install web app/. You've created a PWA for Firefox, it's as simple as that. You can now open it from within the extension by clicking on the external link button.
However, there's more configuration to be done. Firefox PWA creates a Firefox profile for the PWAs that's completely separate from your main profile, so we're going to have to do some configuration there. (By default, all of your PWAs share a single /Default/ profile under the /Profiles/ tab of the extension, but if you want to have different PWAs have different profiles you can configure it there.)
* Configure the PWA Firefox profile
:PROPERTIES:
:CUSTOM_ID: configure-the-pwa-firefox-profile
:END:
It's up to you how want to configure the profile, but here's what I did and recommend.
- In the :gear: /General/ portion, there's a new settings section, /Progressive Web Apps/. For me I checked /Open out of scope URLs in a default browser,/ which makes it so non-Discord links you visit within the PWA profile are opened in your main Firefox profile instead of within the PWA. This is also important considering any external sites you visit within the PWA aren't otherwise going to be added to your main profile's history.
- Under :jigsaw: /Extensions & Themes/, you You might also want to [[https://addons.mozilla.org/en-US/firefox/addon/discord-dark-rebrand/][choose a theme that matches Discord's theming]].
- Under :lock: /Privacy & Security/ / /History/, set that history to /Use custom settings/ for history and uncheck all the boxes. This will make it so the profile won't keep any history (which is unnecessary in a PWA), but still will remember cookies so you don't have to log in again each time you reopen it.
If you shut down your computer with the PWA open, Firefox will think that your previous session didn't close properly and will reopen your old windows. I haven't figured out a way to prevent this, but if you want to mess with advanced configuration, here's how.
Within the PWA, we don't have access to the URL bar, so to enter the preferences, press Ctrl + Shift + I to enter the developer tools. Enter the console and type in the following:
#+begin_src js
document.location.href = "about:config"
#+end_src
Firefox will make you type =allow pasting= beforehand.
* Launching the PWA from the command line
:PROPERTIES:
:CUSTOM_ID: launching-the-pwa-from-the-command-line
:END:
*If you are using a distro with a desktop environment,* Discord should already be in your applications drawer, so unless you want to be able to launch Discord from the command line, you can skip this step.
I have uninstalled the desktop Discord application =discord=, and I want to replace it with a shell script that launches the PWA. This will enable you to launch the PWA from the command line simply by typing =discord=, and also from an application launcher like [[https://github.com/davatorium/rofi][rofi]]. This way, we don't have to rely on launching Discord from the PWAsForFirefox extension.
Luckily, PWAs for Firefox provides a CLI. First, we need to find the ID of the Discord PWA:
#+begin_src sh
firefoxpwa profile list
#+end_src
This will output the following. After the entry for /Discord/, the string of letters and numbers is the site ID. Your ID will be different.
#+begin_example
========================= Default ==========================
Description: Default profile for all sites
ID: 00000000000000000000000000
Sites:
- Discord: https://discord.com/ (01G705MTCT1Q1HHFM7DVDFAPCY)
#+end_example
You can now launch the PWA using the following command.
#+begin_src sh
firefoxpwa site launch 01G705MTCT1Q1HHFM7DVDFAPCY
#+end_src
Let's create the launch script. Open =/usr/bin/discord= (if this exists already, uninstall the desktop Discord application) with your favorite text editor. You'll need to use =sudo=. Add the following to the file, replacing the site ID with the one you found earlier:
#+begin_src sh
#!/usr/bin/env bash
firefoxpwa site launch 01G705MTCT1Q1HHFM7DVDFAPCY
#+end_src
Finally, make it an executable:
#+begin_src sh
sudo chmod +x /usr/bin/discord
#+end_src
You're done! You've created a PWA for Discord that looks and behaves like a desktop application, but without a lot of the hassle involved with the actual desktop app.
Note that if you decide to reinstall the normal Electron-based desktop Discord application, you'll have to move/delete =/usr/bin/discord= first, as desktop Discord needs to put its executable there.
* Conclusion
:PROPERTIES:
:CUSTOM_ID: conclusion
:END:
Of course, this isn't a perfect solution. Dragging and dropping files into the PWA doesn't work for uploading, there might be some improved voice chat performance in the normal desktop version not available in the browser/PWA version, push-to-talk doesn't work, and streaming still has no audio.
However, if you've already been using primarily the browser version of Discord, creating a PWA is a nice upgrade until Discord finally makes a good client. Here's to hoping that day actually comes.
Anyhow, I hope this was helpful!

@ -1,116 +0,0 @@
---
title: "Final Fantasy Japanese Playthrough: Part 1"
date: 2022-01-27
tags:
- japanese
- ff
description: "An analysis of the Japanese opening text of Final Fantasy (1987) for the Famicom."
---
> **Disclaimer:** There may be mistakes in the Japanese content in this post, I am not an expert. This is simply some of my personal notes that I am making public as a blog post. Use this as reference with caution.
Today I decided to try the original [*Final Fantasy*](https://en.wikipedia.org/wiki/Final_Fantasy_(video_game)) game for the Famicom from 1987 in Japanese as vocabulary practice (and for fun, of course).
In this post, I'll be first be quickly going over what I used to run the game, and then text that I went over and the vocabulary I gained from it.
## Setup
To play, I first downloaded the revision B game ROM from [WoWroMs.com](https://wowroms.com/en/roms/nintendo-entertainment-system/final-fantasy-japan/22274.html). Not surprisingly, it's absolutely *tiny*: the entire ROM is less than a megabyte.
To run it, I used RetroArch, an open source emulation frontend. You can get it from either their [website](https://www.retroarch.com/) and also [Steam](https://store.steampowered.com/app/1118310/RetroArch) for free.
RetroArch itself is not an emulator, however: it just provides the frontend and tools to use emulators. In order to load an emulator into RetroArch, you need to install a *core*. Personally, I use the *Mesen* NES/Famicom core. On the standard version of RetroArch available on their website, you can install new cores directly inside of RetroArch through the core downloader. However, due to Steam's limitations, on the Steam version this menu isn't available. Instead, you get cores buy getting the (free) DLC on Steam, all of which are listed on RetroArch's store page. The Mesen core I use is available on Steam [here](https://store.steampowered.com/app/1205330/RetroArch__Mesen/).
## Content
![Final Fantasy Japanese opening text](final-fantasy-1.png)
I didn't spend too long on it, but I did read through the opening text (see screenshot) and get vocab from it.
The Famicom didn't have enough memory for kanji, so all the text is purely in kana (hiragana and katakana). Here's a transcription of the text as written in-game.
> このせかいは あんこくにつつまれている
>
> かぜはやみ うみはあれ だいちはくさっていく
>
> しかし ひとびとは1つのよげんをしんじ それをまっていた
>
> このよ あんこくにそまりしとき
>
> 4にんのひかりのせんし あらわれん
>
> ながいぼうけんのすえ 4にんのわかものがこのちにたどりついた
>
> そしてそのてには それぞれクリスタルにぎられていた
## Kanji and attempted translation
> この[世]{せ}[界]{かい}は [暗]{あん}[黒]{こく}に[包]{つつ}まれている
***This world is wrapped in darkness.***
> [風]{かぜ}は[闇]{やみ}* [海]{うみ}は[荒]{あ}れ [大]{だい}[地]{じ}は[腐]{くさ}っていく
***The wind is dark,***
*I interpreted「やみ」as 闇, darkness, here. It's also possible that it's 止み (止む), to be stopped (the wind), but it doesn't really make sense considering the next section says that the seas are stormy.
***the seas are stormy, and the Earth is rotting away.***
It's also important to mention that between the line sections divided by spaces, the verbs that look like they're being nominalized (e.g. [荒]{あ}れ for [荒]{あ}れる) are actually grammatically acting like the て-form of the verb to string the sentence together. From what I've heard and seen, this is more literary and is done to make the the opening text sound more "epic."
> しかし [人]{ひと}[々]{びと}は[]{ひと}つの[予]{よ}[言]{げん}を[信]{しん}じ それを[待]{ま}っていた
***However, the people believe one prophecy, and on it they waited.***
Again,「[信]{しん}じ」here is like the て-form, [信]{しん}て.
> この[世]{よ}、[暗]{あん}[黒]{こく}に[染]{そ}まりしとき
***In this world, tainted with darkness,***
Here, [染]{そ}まる is the passive form of [染]{そ}める, to dye. So literally, the world is *dyed* with darkness, which brings up imagery of a deep-rooted corruption. The only thing that I was confused about here was what exactly the conjugation「[染]{そ}まりしとき」is here.
> []{よ}[人]{にん}の[光]{ひかり}の[戦]{せん}[士]{し} [現]{あらわ}れん
***four warriors of light will appear.***
> [長]{なが}い[冒]{ぼう}[険]{けん}の[末]{すえ} []{よ}[人]{にん}の[若]{わか}[者]{もの}がこの[地]{ち}に[辿]{たど}り[着]{つ}いた
***At the end of a long adventure, four young travelers finally arrived in this region,***
> そして その[手]{て}にはそれぞれクリスタルが[握]{にぎ}られていた
***and in each of their hands was a crystal.***
## The official NES version
Now, let's see how accurate this was!
Here is the official NES version of the opening text.
![Final Fantasy English opening text](final-fantasy-2.png)
> The world is veiled in darkness. The wind stops, the see is wild, and the earth begins to rot.
>
> The people wait, their only hope, a prophecy...
>
> 'When the world is in darkness Four Warriors will come...'
>
> After a long journey, four young warriors arrive, each holding an ORB.
Overall, I was pretty close, but I made a couple mistakes.
1.「やみ」was in fact 止み, to stop. (Drago, you were right!)
2. Because of the lack of quotations in the Japanese version, I didn't realize that one of the lines was actually a recitation of the prophecy.
3. 「そまりしとき」was actually 染まりし時, so *when* the world is becomes tainted with darkness, specifying the time condition for the prophecy. I didn't recognize the construction here of *verb nominalization* + し (nominalization of する) + 時. Because I didn't realize that this line was a direction quotation of the prophecy, I didn't pick up on this, even though I should have. What the し is doing here is that it's rather like 〜にする, and turns it from being simply *the time when the world is tainted with darkness*, 染まり時, to *the time when the world **becomes** tainted with darkness*.
## Conclusion
This was a really fun practice. I honestly need to spend more time consuming native Japanese material; I honestly study way less than I should and my vocabulary game isn't where it should be. I put all of the new/forgotten vocabulary terms from this into my [Anki](https://apps.ankiweb.net/) deck.
I think I might make this a series of blog posts on this website, making Japanese study overview of things I pick up from Final Fantasy as I slowly work through the game. If I actually go through with that, you'll be able to see a list of posts on the Final Fantasy [#ff](/tags/ff) tag page.
See you in the next post!

@ -0,0 +1,152 @@
#+TITLE: Final Fantasy Japanese Playthrough: Part 1
#+DATE: 2022-01-27
#+FILETAGS: japanese ff
#+DESCRIPTION: An analysis of the Japanese opening text of Final Fantasy (1987) for the Famicom.
#+begin_quote
*Disclaimer:* There may be mistakes in the Japanese content in this post, I am not an expert. This is simply some of my personal notes that I am making public as a blog post. Use this as reference with caution.
#+end_quote
Today I decided to try the original [[https://en.wikipedia.org/wiki/Final_Fantasy_(video_game)][/Final Fantasy/]] game for the Famicom from 1987 in Japanese as vocabulary practice (and for fun, of course).
In this post, I'll be first be quickly going over what I used to run the game, and then text that I went over and the vocabulary I gained from it.
* Setup
:PROPERTIES:
:CUSTOM_ID: setup
:END:
To play, I first downloaded the revision B game ROM from [[https://wowroms.com/en/roms/nintendo-entertainment-system/final-fantasy-japan/22274.html][WoWroMs.com]]. Not surprisingly, it's absolutely /tiny/: the entire ROM is less than a megabyte.
To run it, I used RetroArch, an open source emulation frontend. You can get it from either their [[https://www.retroarch.com/][website]] and also [[https://store.steampowered.com/app/1118310/RetroArch][Steam]] for free.
RetroArch itself is not an emulator, however: it just provides the frontend and tools to use emulators. In order to load an emulator into RetroArch, you need to install a /core/. Personally, I use the /Mesen/ NES/Famicom core. On the standard version of RetroArch available on their website, you can install new cores directly inside of RetroArch through the core downloader. However, due to Steam's limitations, on the Steam version this menu isn't available. Instead, you get cores buy getting the (free) DLC on Steam, all of which are listed on RetroArch's store page. The Mesen core I use is available on Steam [[https://store.steampowered.com/app/1205330/RetroArch__Mesen/][here]].
* Content
:PROPERTIES:
:CUSTOM_ID: content
:END:
#+caption: Final Fantasy Japanese opening text
[[file:final-fantasy-1.png]]
I didn't spend too long on it, but I did read through the opening text (see screenshot) and get vocab from it.
The Famicom didn't have enough memory for kanji, so all the text is purely in kana (hiragana and katakana). Here's a transcription of the text as written in-game.
#+begin_quote
このせかいは あんこくにつつまれている
かぜはやみ うみはあれ だいちはくさっていく
しかし ひとびとは1つのよげんをしんじ それをまっていた
このよ あんこくにそまりしとき
4にんのひかりのせんし あらわれん
ながいぼうけんのすえ 4にんのわかものがこのちにたどりついた
そしてそのてには それぞれクリスタルにぎられていた
#+end_quote
* Kanji and attempted translation
:PROPERTIES:
:CUSTOM_ID: kanji-and-attempted-translation
:END:
#+begin_quote
この[世]{せ}[界]{かい}は [暗]{あん}[黒]{こく}に[包]{つつ}まれている
#+end_quote
*/This world is wrapped in darkness./*
#+begin_quote
[風]{かぜ}は[闇]{やみ}* [海]{うみ}は[荒]{あ}れ [大]{だい}[地]{じ}は[腐]{くさ}っていく
#+end_quote
*/The wind is dark,/*
*I interpreted「やみ」as 闇, darkness, here. It's also possible that it's 止み (止む), to be stopped (the wind), but it doesn't really make sense considering the next section says that the seas are stormy.
*/the seas are stormy, and the Earth is rotting away./*
It's also important to mention that between the line sections divided by spaces, the verbs that look like they're being nominalized (e.g. [荒]{あ}れ for [荒]{あ}れる) are actually grammatically acting like the て-form of the verb to string the sentence together. From what I've heard and seen, this is more literary and is done to make the the opening text sound more "epic."
#+begin_quote
しかし [人]{ひと}[々]{びと}は[]{ひと}つの[予]{よ}[言]{げん}を[信]{しん}じ それを[待]{ま}っていた
#+end_quote
*/However, the people believe one prophecy, and on it they waited./*
Again,「[信]{しん}じ」here is like the て-form, [信]{しん}て.
#+begin_quote
この[世]{よ}、[暗]{あん}[黒]{こく}に[染]{そ}まりしとき
#+end_quote
*/In this world, tainted with darkness,/*
Here, [染]{そ}まる is the passive form of [染]{そ}める, to dye. So literally, the world is /dyed/ with darkness, which brings up imagery of a deep-rooted corruption. The only thing that I was confused about here was what exactly the conjugation「[染]{そ}まりしとき」is here.
#+begin_quote
[]{よ}[人]{にん}の[光]{ひかり}の[戦]{せん}[士]{し} [現]{あらわ}れん
#+end_quote
*/four warriors of light will appear./*
#+begin_quote
[長]{なが}い[冒]{ぼう}[険]{けん}の[末]{すえ} []{よ}[人]{にん}の[若]{わか}[者]{もの}がこの[地]{ち}に[辿]{たど}り[着]{つ}いた
#+end_quote
*/At the end of a long adventure, four young travelers finally arrived in this region,/*
#+begin_quote
そして その[手]{て}にはそれぞれクリスタルが[握]{にぎ}られていた
#+end_quote
*/and in each of their hands was a crystal./*
* The official NES version
:PROPERTIES:
:CUSTOM_ID: the-official-nes-version
:END:
Now, let's see how accurate this was!
Here is the official NES version of the opening text.
#+caption: Final Fantasy English opening text
[[file:final-fantasy-2.png]]
#+begin_quote
The world is veiled in darkness. The wind stops, the see is wild, and the earth begins to rot.
The people wait, their only hope, a prophecy...
'When the world is in darkness Four Warriors will come...'
After a long journey, four young warriors arrive, each holding an ORB.
#+end_quote
Overall, I was pretty close, but I made a couple mistakes.
1. 「やみ」was in fact 止み, to stop. (Drago, you were right!)
2. [@2] Because of the lack of quotations in the Japanese version, I didn't realize that one of the lines was actually a recitation of the prophecy.
3. 「そまりしとき」was actually 染まりし時, so /when/ the world is becomes tainted with darkness, specifying the time condition for the prophecy. I didn't recognize the construction here of /verb nominalization/ + し (nominalization of する) + 時. Because I didn't realize that this line was a direction quotation of the prophecy, I didn't pick up on this, even though I should have. What the し is doing here is that it's rather like 〜にする, and turns it from being simply /the time when the world is tainted with darkness/, 染まり時, to /the time when the world *becomes* tainted with darkness/.
* Conclusion
:PROPERTIES:
:CUSTOM_ID: conclusion
:END:
This was a really fun practice. I honestly need to spend more time consuming native Japanese material; I honestly study way less than I should and my vocabulary game isn't where it should be. I put all of the new/forgotten vocabulary terms from this into my [[https://apps.ankiweb.net/][Anki]] deck.
I think I might make this a series of blog posts on this website, making Japanese study overview of things I pick up from Final Fantasy as I slowly work through the game. If I actually go through with that, you'll be able to see a list of posts on the Final Fantasy [[/tags/ff][#ff]] tag page.
See you in the next post!

@ -1,56 +0,0 @@
---
title: "Furigana in Markdown Using Regular Expressions"
date: 2022-01-23
tags:
- japanese
- programming
description: "The Markdown spec doesnt include support for furigana, more commonly known as ruby text, which can be troublesome if you want to write content in Japanese. In this tutorial, learn how to add furigana support to Hugo using regular expressions!"
---
> TL;DR: [Here](#creating-the-regular-expression)
### Background
As I was building ~~the current~~ *a previous* version of this website, I came across an issue. In the previous version of the site which I made with [Nuxt.js](https://nuxtjs.org/), a [Vue.js](https://vuejs.org/)-based JavaScript web development framework similar to the [React](https://reactjs.org/)-based [Next.js](https://nextjs.org/), I used [`markdown-it`](https://www.npmjs.com/package/markdown-it) for rendering my Markdown content. The great thing about `markdown-it` was how extensible it is: there are a vast number of available npm packages that extend its functionality beyond the base [CommonMark](https://commonmark.org/) specification.
One of the packages I used was [`furigana-markdown-it`](https://www.npmjs.com/package/furigana-markdown-it), which enabled [furigana](https://en.wikipedia.org/wiki/Furigana), or more widely known as [ruby characters](https://en.wikipedia.org/wiki/Ruby_character), which are reading information written beside logographic characters in East Asian languages. For example, in Japanese the reading for 猫, cat, is ねこ, and can be written with furigana as [猫]{ねこ}. The syntax for this was the main text in square parentheses, `[猫]`, followed by the furigana in curly brackets, {ねこ}, was quite convenient, and I wanted to be able to do the same thing in the new [Hugo](https://gohugo.io/) site.
Instead of `markdown-it`, Hugo by default uses [Goldmark](https://github.com/yuin/goldmark), a Markdown renderer written in Go (a language I don't know), and while extensible, I really didn't want to go through the effort to learn Go, figure out how to make a Goldmark extension, and get it working in Hugo. After looking into it some more, it turns out that in Hugo's templating there is a [`replaceRE`](https://gohugo.io/functions/replacere/) function that lets you find and replace content intelligently using regular expressions.
### Creating the regular expression
After watching [this](https://www.youtube.com/watch?v=rhzKDrUiJVk) useful tutorial by Web Dev Simplified on regular expressions to get an idea of how they work, I managed to create this regular expression:
```RE
\[([^\]]*)\]{([^\}]*)}
```
The first section, `\[([^\]]*)\]` creates a capturing group `()` around character surrounded by square brackets `[]`. The square brackets are escaped by the proceeding backslash (`\[`, `\]`) to make sure they aren't interpreted as regex syntax characters. Inside of the capturing group is `[^\]]*`. The negated set `[^\]]` means any character that isn't a right square bracket `]`, and the asterisk `*` repeats the previous token zero or more times in a row. In other words, the capturing group will end as soon as a right square bracket `]` is detected.
The second section, `{([^\}]*)`, is basically the same thing as the first, except the capturing group is surrounded by curly brackets `{}`. Again, they are escaped to make sure they aren't being interpreted as regex syntax characters.
You can test out the regular expression and see a breakdown of how all the parts of it work on [RegExr](https://regexr.com/6dspa), a super useful tool for building and testing regular expressions.
### Ruby text HTML syntax
The HTML syntax for furigana/ruby text is as follows. For more information, see the [MDN documentation](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/ruby).
```HTML
<ruby lang="ja"><rp>(</rp><rt>ねこ</rt><rp>)</rp></ruby>
```
For me, all of my ruby text is going to be in Japanese, so I've added the attribute `lang="ja"` to ensure that the Japanese (not Chinese) character variants are rendered. For some characters, the way they are written in Japan and China slightly differs. For example, for the Unicode character [U+76F4](https://en.wiktionary.org/wiki/直), it is rendered as the Chinese variant, 直, by default but as <span lang="ja"></span> when the language is explicitly specified to be Japanese, despite them being *the exact same Unicode character code.*
### Adding the regular expression to Hugo
In Hugo templates, one can display the rendered Markdown content of a given page using `{{ .Content }}`. What we need to do is pass `.Content` into the aforementioned `replaceRE` function. Since Hugo by default escapes HTML syntax, we need to then pipe everything into the `safeHTML` function. The `$1` and `$2` are placeholders for the first and second capture groups in the regular expression, respectively.
```HTML
{{ replaceRE `\[([^\]]*)\]{([^\}]*)}` `<ruby lang="ja">$1<rp>(</rp><rt>$2</rt><rp>)</rp></ruby>` .Content | safeHTML }}
```
All one needs to do now to get furigana rendering on all of one's page types is replace `{{ .Content }}` with this on all of their templates! To prevent code duplication, I put all of this into a [partial template](https://gohugo.io/templates/partials/).
### Conclusion
I hope you found this first blog post on this site helpful! If you're going to have any Japanese content in your site, being able to write ruby text in your markup is a must. While this tutorial was targeted toward Hugo, you can do this in **any static site generator that supports regular expressions in templates.**

@ -0,0 +1,64 @@
#+TITLE: Furigana in Markdown Using Regular Expressions
#+DATE: 2022-01-23
#+FILETAGS: japanese
#+DESCRIPTION: The Markdown spec doesnt include support for furigana, more commonly known as ruby text, which can be troublesome if you want to write content in Japanese. In this tutorial, learn how to add furigana support to Hugo using regular expressions!
#+begin_quote
TL;DR: [[#creating-the-regular-expression][Here]]
#+end_quote
* Background
:PROPERTIES:
:CUSTOM_ID: background
:END:
As I was building +the current+ /a previous/ version of this website, I came across an issue. In the previous version of the site which I made with [[https://nuxtjs.org/][Nuxt.js]], a [[https://vuejs.org/][Vue.js]]-based JavaScript web development framework similar to the [[https://reactjs.org/][React]]-based [[https://nextjs.org/][Next.js]], I used [[https://www.npmjs.com/package/markdown-it][=markdown-it=]] for rendering my Markdown content. The great thing about =markdown-it= was how extensible it is: there are a vast number of available npm packages that extend its functionality beyond the base [[https://commonmark.org/][CommonMark]] specification.
One of the packages I used was [[https://www.npmjs.com/package/furigana-markdown-it][=furigana-markdown-it=]], which enabled [[https://en.wikipedia.org/wiki/Furigana][furigana]], or more widely known as [[https://en.wikipedia.org/wiki/Ruby_character][ruby characters]], which are reading information written beside logographic characters in East Asian languages. For example, in Japanese the reading for 猫, cat, is ねこ, and can be written with furigana as [猫]{ねこ}. The syntax for this was the main text in square parentheses, =[猫]=, followed by the furigana in curly brackets, {ねこ}, was quite convenient, and I wanted to be able to do the same thing in the new [[https://gohugo.io/][Hugo]] site.
Instead of =markdown-it=, Hugo by default uses [[https://github.com/yuin/goldmark][Goldmark]], a Markdown renderer written in Go (a language I don't know), and while extensible, I really didn't want to go through the effort to learn Go, figure out how to make a Goldmark extension, and get it working in Hugo. After looking into it some more, it turns out that in Hugo's templating there is a [[https://gohugo.io/functions/replacere/][=replaceRE=]] function that lets you find and replace content intelligently using regular expressions.
* Creating the regular expression
:PROPERTIES:
:CUSTOM_ID: creating-the-regular-expression
:END:
After watching [[https://www.youtube.com/watch?v=rhzKDrUiJVk][this]] useful tutorial by Web Dev Simplified on regular expressions to get an idea of how they work, I managed to create this regular expression:
#+begin_example
\[([^\]]*)\]{([^\}]*)}
#+end_example
The first section, =\[([^\]]*)\]= creates a capturing group =()= around character surrounded by square brackets =[]=. The square brackets are escaped by the proceeding backslash (=\[=, =\]=) to make sure they aren't interpreted as regex syntax characters. Inside of the capturing group is =[^\]]*=. The negated set =[^\]]= means any character that isn't a right square bracket =]=, and the asterisk =*= repeats the previous token zero or more times in a row. In other words, the capturing group will end as soon as a right square bracket =]= is detected.
The second section, ={([^\}]*)=, is basically the same thing as the first, except the capturing group is surrounded by curly brackets ={}=. Again, they are escaped to make sure they aren't being interpreted as regex syntax characters.
You can test out the regular expression and see a breakdown of how all the parts of it work on [[https://regexr.com/6dspa][RegExr]], a super useful tool for building and testing regular expressions.
* Ruby text HTML syntax
:PROPERTIES:
:CUSTOM_ID: ruby-text-html-syntax
:END:
The HTML syntax for furigana/ruby text is as follows. For more information, see the [[https://developer.mozilla.org/en-US/docs/Web/HTML/Element/ruby][MDN documentation]].
#+begin_src html
<ruby lang="ja">猫<rp>(</rp><rt>ねこ</rt><rp>)</rp></ruby>
#+end_src
For me, all of my ruby text is going to be in Japanese, so I've added the attribute =lang="ja"= to ensure that the Japanese (not Chinese) character variants are rendered. For some characters, the way they are written in Japan and China slightly differs. For example, for the Unicode character [[https://en.wiktionary.org/wiki/直][U+76F4]], it is rendered as the Chinese variant, 直, by default but as 直 when the language is explicitly specified to be Japanese, despite them being /the exact same Unicode character code./
* Adding the regular expression to Hugo
:PROPERTIES:
:CUSTOM_ID: adding-the-regular-expression-to-hugo
:END:
In Hugo templates, one can display the rendered Markdown content of a given page using ={{ .Content }}=. What we need to do is pass =.Content= into the aforementioned =replaceRE= function. Since Hugo by default escapes HTML syntax, we need to then pipe everything into the =safeHTML= function. The =$1= and =$2= are placeholders for the first and second capture groups in the regular expression, respectively.
#+begin_example
{{ replaceRE `\[([^\]]*)\]{([^\}]*)}` `<ruby lang="ja">$1<rp>(</rp><rt>$2</rt><rp>)</rp></ruby>` .Content | safeHTML }}
#+end_example
All one needs to do now to get furigana rendering on all of one's page types is replace ={{ .Content }}= with this on all of their templates! To prevent code duplication, I put all of this into a [[https://gohugo.io/templates/partials/][partial template]].
* Conclusion
:PROPERTIES:
:CUSTOM_ID: conclusion
:END:
I hope you found this first blog post on this site helpful! If you're going to have any Japanese content in your site, being able to write ruby text in your markup is a must. While this tutorial was targeted toward Hugo, you can do this in *any static site generator that supports regular expressions in templates.*

@ -1,81 +0,0 @@
---
title: "Handling Multiple Possible Errors in Rust"
date: 2022-08-04
tags:
- programming
draft: true
---
## Background
Rust handles errors better than any other language Ive ever used. All possible errors are known at build time, and by design Rust forces you to at a bare minimum *acknowledge* that these errors are possible with `.unwrap()`, otherwise writing specific error handling logic.
### The `Result` enum
All of this is thanks to the `Result` enum, which is defined as follows:
```RS
enum Result<T, E> {
Ok(T),
Err(E),
}
```
`Result` is a generic type that requires two type parameters, `T`, the actual value the result is wrapping, and `E`, the error type in case something goes wrong. In the success case, the `Ok(T)` variant will be used, and in an error case, `Err(E)` will be used.
What makes this powerful is the fact that since `Result` wraps the actual result value, access to that value is prevented unless the possibility of an error is acknowledged.
### Handling `Result`
For the sake of example, say were writing a theoretical function, `acquire_banana`, that drives to the store, purchases a banana, and returns the banana.
We have a function called `drive_to_store`. It returns a `Result` that can either be nothing `()`, or a `NavigationError`.
```RS
fn drive_to_store() -> Result<(), NavigationError> { ... }
```
We then have the following function, `acquire_banana`. It also returns a `Result`, but this time it can either be a `Banana` or a `PurchaseError`.
```RS
fn purchase_banana() -> Result<Banana, PurchaseError> { ... }
```
In other languages, we might write the function something like this:
```RS
fn acquire_banana() -> Banana {
drive_to_store();
purchase_banana()
}
```
However, this doesnt work in Rust. There are two issues here. First, while `drive_to_store();` is in theory valid code (it returns an enum that we ignore), `Result`s **must** be handled. Otherwise, you will get compile warnings. Second, we cannot return `purchase_banana` from the function, because its return type is `Result<Banana, PurchaseError>`, not `Banana`.
The naïve solution to this would be using the `unwrap` method on the `Result`s, like this:
```RS
fn acquire_banana() -> Banana {
drive_to_store().unwrap();
purchase_banana().unwrap()
}
```
The `.unwrap()` method does exactly what it sounds like. If a `Result` is the `Ok(T)` variant, it returns `T`. However, the naïvety here lies in what happens on the `Err(E)` variant. In that case, it *panics*, printing the error and kills the thread/program. A well-designed program should take errors into account, so this solution is unacceptable.
The best solution is in this case is **error propagation** using the question mark `?` operator. It works similarly to `.unwrap()`, but instead of panicking on the `Err(E)` case, it returns `Err(E)` from the function. This lets you quickly unwrap results without convoluted `match` statements.
This lets you convert this:
```RS
let x = match get_result() {
Ok(value) => value,
Err(error) => return Err(error),
};
```
Into this:
```RS
let x = get_result()?;
```

@ -0,0 +1,84 @@
#+TITLE: Handling Multiple Possible Errors in Rust
#+DATE: 2022-08-04
#+FILETAGS: programming
#+HUGO_DRAFT: t
* Background
:PROPERTIES:
:CUSTOM_ID: background
:END:
Rust handles errors better than any other language I've ever used. All possible errors are known at build time, and by design Rust forces you to at a bare minimum /acknowledge/ that these errors are possible with =.unwrap()=, otherwise writing specific error handling logic.
** The =Result= enum
:PROPERTIES:
:CUSTOM_ID: the-result-enum
:END:
All of this is thanks to the =Result= enum, which is defined as follows:
#+begin_src rust
enum Result<T, E> {
Ok(T),
Err(E),
}
#+end_src
=Result= is a generic type that requires two type parameters, =T=, the actual value the result is wrapping, and =E=, the error type in case something goes wrong. In the success case, the =Ok(T)= variant will be used, and in an error case, =Err(E)= will be used.
What makes this powerful is the fact that since =Result= wraps the actual result value, access to that value is prevented unless the possibility of an error is acknowledged.
** Handling =Result=
:PROPERTIES:
:CUSTOM_ID: handling-result
:END:
For the sake of example, say we're writing a theoretical function, =acquire_banana=, that drives to the store, purchases a banana, and returns the banana.
We have a function called =drive_to_store=. It returns a =Result= that can either be nothing =()=, or a =NavigationError=.
#+begin_src rust
fn drive_to_store() -> Result<(), NavigationError> { ... }
#+end_src
We then have the following function, =acquire_banana=. It also returns a =Result=, but this time it can either be a =Banana= or a =PurchaseError=.
#+begin_src rust
fn purchase_banana() -> Result<Banana, PurchaseError> { ... }
#+end_src
In other languages, we might write the function something like this:
#+begin_src rust
fn acquire_banana() -> Banana {
drive_to_store();
purchase_banana()
}
#+end_src
However, this doesn't work in Rust. There are two issues here. First, while =drive_to_store();= is in theory valid code (it returns an enum that we ignore), =Result=s *must* be handled. Otherwise, you will get compile warnings. Second, we cannot return =purchase_banana= from the function, because its return type is =Result<Banana, PurchaseError>=, not =Banana=.
The naïve solution to this would be using the =unwrap= method on the =Result=s, like this:
#+begin_src rust
fn acquire_banana() -> Banana {
drive_to_store().unwrap();
purchase_banana().unwrap()
}
#+end_src
The =.unwrap()= method does exactly what it sounds like. If a =Result= is the =Ok(T)= variant, it returns =T=. However, the naïvety here lies in what happens on the =Err(E)= variant. In that case, it /panics/, printing the error and kills the thread/program. A well-designed program should take errors into account, so this solution is unacceptable.
The best solution is in this case is *error propagation* using the question mark =?= operator. It works similarly to =.unwrap()=, but instead of panicking on the =Err(E)= case, it returns =Err(E)= from the function. This lets you quickly unwrap results without convoluted =match= statements.
This lets you convert this:
#+begin_src rust
let x = match get_result() {
Ok(value) => value,
Err(error) => return Err(error),
};
#+end_src
Into this:
#+begin_src rust
let x = get_result()?;
#+end_src

@ -1,183 +0,0 @@
---
title: "How to Configure Vim for Writing"
date: 2022-06-13
tags:
- programming
- writing
- vim
description: "Learn how to configure Vim to be a writing machine by adding spellcheck, support for smart/typographic quotes, a custom status bar, and more!"
---
There are a lot of tools that are, for the most part, only used in the programming sphere that could do a lot of good if they received adoption among the wider non-technical community. Git and Markdown especially. Version control is useful to anyone who works on text files and needs to be able to revert back to a previous version, and Markdown is a simple and succinct way to format simple text documents — especially compared with Microsoft Office.
Another tool that could see wider adoption is Vim, especially among writers looking for a powerful yet minimal text editor, and are willing to invest some time into learning it.
I switched over to Vim a couple weeks ago from GNU nano and [VSCodium](https://vscodium.com/) (a free and open source build of VS Code), and I havent looked back since. However, out of the box, Vim isnt configured to excel at anything, besides perhaps basic configuration file editing. In this guide, Ill go over how to configure Vim for writing, assuming youre writing in Markdown files.
When youre done, your Vim will look something like this:
![Screenshot](screenshot.png)
I will cover the following, **assuming you are slightly familiar with basic editing in Vim:**
- How to set Vim to have soft line breaks at words, not characters, so no words are split across lines
- How to use smart/typographic quotes (i.e. `“”` instead of the standard ASCII `""`)
- How to enable spellcheck
- How to prevent Chinese, Japanese, and Korean (CJK) characters from being flagged as spelling mistakes
- How to patch your dictionary to include words with typographic apostrophes (by default, words such as `doesnt` will be flagged as misspelled but `does't` wont be)
- How to add a custom status bar that displays word count, character count, and estimated reading time
With the introduction out of the way, lets get right into it!
## Soft line breaks
Most Vim configurations are done in the users `~/.vimrc` file, which applies to all Vim sessions regardless of file type. However, we want our customizations to only apply to Markdown text files. Luckily, Vim has a feature that handles this. [Filetype plugins](https://vim.fandom.com/wiki/File_type_plugins) allow you to create Vim configurations that only apply to certain filetypes, and are stored in the directory `~/.vim/ftplugin/`. We are going to create one for our Markdown files: `~/.vim/ftplugin/markdown.vim`.
Inside this file, add the following:
```VIM
setlocal linebreak
```
We want to use `setlocal` (or its shorthand `setl`) over the usual `set` because this only sets `linebreak` on the current buffer. If we have multiple files open in Vim, we want to make sure that this setting only applies to open Markdown files.
<details>
<summary>Thanks to <a href="https://www.reddit.com/user/habamax/">u/habamax</a> for pointing this out!</summary>
<iframe id="reddit-embed" src="https://www.redditmedia.com/r/vim/comments/vc9oi2/how_to_configure_vim_for_writing/iccz7kq/?depth=1&amp;showmore=false&amp;embed=true&amp;showmedia=false" sandbox="allow-scripts allow-same-origin allow-popups" style="border: none;" scrolling="no" width="640" height="233"></iframe>
</details><br>
And youre done! Vim will now no longer split words across soft-wrapped lines. Now, lets move on to typographic quotes.
## Typographic quotes
Vim doesnt have any native support for typographic quotes, so well need to install the plugin [preservim/vim-textobj-quote](https://github.com/preservim/vim-textobj-quote). There are a few different ways to install plugins in Vim, but in my opinion the easiest one to use is Vundle. If you are on Arch Linux, there is an [AUR package](https://aur.archlinux.org/packages/vundle), otherwise, see the [quick start guide](https://github.com/VundleVim/Vundle.vim#quick-start) on Vundles GitHub repository.
Now that we have a package manager for Vim installed, we need to install the plugin itself. Open your `~/.vimrc`, or create it if it doesnt exist yet, and add the following:
```VIM
filetype off
set rtp+=~/.vim/bundle/Vundle.vim
call vundle#begin()
Plugin 'kana/vim-textobj-user' " essential dependency for typographic quotes
Plugin 'preservim/vim-textobj-quote' " typographic quotes
call vundle#end()
filetype plugin indent on
```
If you are unfamilar with Vundle, between `vundle#begin()` and `vundle#end()` is where all of the Vim plugins you want installed with Vundle are listed; the rest of the lines are various requirements for Vundle to work properly (for more information, see [Vundles quick start guide](https://github.com/VundleVim/Vundle.vim#quick-start)). By default, Vundle assumes the plugins to be GitHub repositories, so all you need to write down is the repository name. [kana/vim-textobj-user](https://github.com/kana/vim-textobj-user) is an essential dependency for our typographic quote plugin that will handle text objects.
Now that youve listed the necessary plugins, you need to have Vundle install them. Save and exit `~/.vimrc`, open Vim, type `:PluginInstall`, and press enter. If youve done everything correctly, Vundle should pull all of the necessary plugins from GitHub, and once its done you can type `:q` to exit the installation window.
The typographic quotes plugin is now installed, but it wont start up by default. Since we only want it to run on Markdown files, we open up `~/.vim/ftplugins/markdown.vim` again and add the following:
```VIM
call textobj#quote#init()
```
This will initialize the typographic quote plugin whenever you enter a Markdown file.
### Useful commands
- Sometimes you may want to override the plugin to type non-typographic quotes. To do this, press <kbd>Ctrl</kbd> + <kbd>V</kbd> (**V** for **V**erbatim) and then type the single or double quotation mark.
- This plugin can also convert preexisting text to typographic quotes. There are [instructions on how to do this on the plugins repository](https://github.com/preservim/vim-textobj-quote#replace-support), but I was having some difficulty getting them to work.
I ended up slightly using a slightly modified version of the instructions which maps <kbd>\c</kbd> to run the plugin replacing straight quotes with typographic “curly” quotes, and <kbd>\s</kbd> to do the inverse. (You can replace the backslash with any character, provided it isnt already mapped. The backslash key is handy, however, since its one of the few keys that isnt connected to any commands in Vims default configuration.)
```VIM
map \c <plug>ReplaceWithCurly
map \s <plug>ReplaceWithStraight
```
In normal mode, these commands replace all occurrences in the current paragraph, and in visual mode (how you do visual selection in Vim: press <kbd>v</kbd> while in normal mode) they replace all occurrences in the current selection. To replace throughout the entire document, Press <kbd>gg</kbd> to go to the top of the document, <kbd>v</kbd> to enter visual mode, <kbd>G</kbd> to jump to the last line of the document, <kbd>$</kbd> to go to the last character, selecting everything, and finally either <kbd>\c</kbd> or <kbd>\s</kbd> to run the plugin command. So, altogether: <kbd>ggvG$</kbd> + either <kbd>\c</kbd> for curly quotes or <kbd>\s</kbd> for straight quotes.
- For more information and advanced usage, [see the plugins README](https://github.com/preservim/vim-textobj-quote#vim-textobj-quote).
## Spellcheck
To enable spellcheck, add the following to your `~/.vim/ftplugin/.markdown.md`:
```VIM
set spell spelllang=en
set spelllang+=cjk " prevent CJK characters from being spellchecked
```
Of course, if you want spellchecking in another language, change `en` to whatever your language code is. The second line is optional, however if you often work with documents including Chinese, Japanese, or Korean (CJK) characters like me, this is handy since otherwise Vim will mark them all as spelling mistakes.
### Useful commands
- <kbd>zg</kbd> adds a word to the dictionary
- <kbd>z=</kbd> brings a list of possible corrections for misspelled words
- <kbd>zug</kbd> or <kbd>zuw</kbd> removes a word from the dictionary
- `:set nospell` disables spellchecking for the current document
- `:set spell` enables spellchecking for the current document
### Patching dictionary for typographic quotes
If you decided to skip the typographic quotes configuration from earlier, you can stop here, but otherwise were going to have to patch our Vim spellcheck dictionary to include words with typographic apostrophes/single quotes (e.g. `doesnt`) which will be marked as incorrect otherwise.
[This](https://vi.stackexchange.com/q/118) thread on the Vi and Vim Stack Exchange was very helpful, so if youre having any trouble please refer to the two answers there.
First, create the directory `~/.vim/spell` and enter it. Were going to need two dictionary files, which are available [here](http://wordlist.aspell.net/dicts/). Please check to see if there has been a newer version on the website and `wget` that instead, if the one Im using (2020.12.07) has become out of date. Im going to use the American English (setting `_LANG` to `en_US`) dictionary file, but Canadian (`en_CA`) and Australian (`en_AU`) English dictionaries are also available. If you need British English or larger word list, more archive downloads are available [here](https://sourceforge.net/projects/wordlist/files/speller/2020.12.07/), although I havent tested them.
```SH
$ mkdir -p ~/.vim/spell && cd ~/.vim/spell
$ _LANG=en_US # language variable for future commands, replace if needed
$ wget http://downloads.sourceforge.net/wordlist/hunspell-$_LANG-2020.12.07.zip
$ unzip *.zip $_LANG* && rm *.zip
```
In order to patch the dictionary, run the following command (see [this](https://vi.stackexchange.com/a/172) for an explanation of how it works). It takes all occurrences of words with apostrophes in the dictionary file and appends a version with the typographic version.
```SH
$ grep "'" $_LANG.dic | sed "s/'//g" >> $_LANG.dic
```
Then, still in the `~/.vim/spell` directory, start Vim, and run the `:mkspell! en en_US` command, which should create an `en.utf-8.spl` file. Youre done!
If you find more compatible dictionary files besides English that are confirmed working, please let me know and Ill add them to this article.
## Custom status bar
Now, for the cherry on top: a custom status bar!
Add the following to your `~/.vim/ftplugin/markdown.vim`:
```VIM
function! Characters()
return strchars(join(getline(1, '$'), "\n"))
endfunction
function! Words()
return wordcount().words
endfunction
function! Minutes()
let wpm = 200
return (Words() + wpm / 2) / wpm
endfunction
set laststatus=2 " enable status line
set statusline+=%=%{Words()}\ words,
set statusline+=\ %{Characters()}\ characters,\ about
set statusline+=\ %{Minutes()}\ minutes
" remove ugly white background
hi StatusLine ctermfg=0 ctermbg=none cterm=bold " 0 for the terminal color 0
```
I havent done any Vimscript before working on this configuration, so this is mostly cobbled this together from a few Stack Overflow threads. There might be better ways of doing some things!
The main things of note are the following:
- `strchars` is the only correct way to count the number of characters in a string. Many people suggest methods that count the number of bytes, but Unicode characters outside of ASCII will take up more than one byte and thus be counted as more than one character.
- If you need a byte counter in your status line, you can use `wordcount().bytes`.
- The `%=` in the first `set statusline+=` line makes everything following right-aligned.
- Spaces must be escaped using `\ ` when adding to the status line.
- By default, the status line background is bright white, so in the final line `ctermbg=none` removes this and makes it more subtle.
- I didnt want my status line to be brighter than my text and distracting, so I set its foreground color to `0` in the final line. In my terminal color palette, this is a subdued color, but you can try using any value between 0 and 15.
## Conclusion
Thats all for this tutorial! I hope it was useful to you. For the last couple of weeks, Ive been learning to use and configure Vim, and its been a lot of fun. I made this article to share what Ive learned, and also as a future reference for myself. Writing can be a good way of solidifying your knowledge, and I learned a lot writing this. This is my first “proper” blog post, so I hope it was alright. Ill be writing more in the future!
Oh, and one more bonus tip. The normal <kbd>j</kbd> and <kbd>k</kbd> navigation commands for going up or and down a line in Vim go over “hard” lines in the document, not soft-wrapped visual lines. If you want to go over visual lines as one often does when writing, prefix the command with <kbd>g</kbd> (for graphical, I guess?). So, for example, to go down one graphical line, use <kbd>gj</kbd>. You can even prefix the chord with a number to repeat it multiple times, so to go up three visual lines, use <kbd>3gk</kbd>! If you need to do this a lot, you might want to look into [creating a custom mapping](https://vim.fandom.com/wiki/Mapping_keys_in_Vim_-_Tutorial_(Part_1)).
See you in the next post!

@ -0,0 +1,203 @@
#+TITLE: How to Configure Vim for Writing
#+DATE: 2022-06-13
#+FILETAGS: programming writing vim
#+DESCRIPTION: Learn how to configure Vim to be a writing machine by adding spellcheck, support for smart/typographic quotes, a custom status bar, and more!
#+OPTIONS: html-postamble:nil
There are a lot of tools that are, for the most part, only used in the programming sphere that could do a lot of good if they received adoption among the wider non-technical community. Git and Markdown especially. Version control is useful to anyone who works on text files and needs to be able to revert back to a previous version, and Markdown is a simple and succinct way to format simple text documents --- especially compared with Microsoft Office.
Another tool that could see wider adoption is Vim, especially among writers looking for a powerful yet minimal text editor, and are willing to invest some time into learning it.
I switched over to Vim a couple weeks ago from GNU nano and [[https://vscodium.com/][VSCodium]] (a free and open source build of VS Code), and I haven't looked back since. However, out of the box, Vim isn't configured to excel at anything, besides perhaps basic configuration file editing. In this guide, I'll go over how to configure Vim for writing, assuming you're writing in Markdown files.
When you're done, your Vim will look something like this:
#+caption: Screenshot
[[file:screenshot.png]]
I will cover the following, *assuming you are slightly familiar with basic editing in Vim:*
- How to set Vim to have soft line breaks at words, not characters, so no words are split across lines
- How to use smart/typographic quotes (i.e. =“”= instead of the standard ASCII =""=)
- How to enable spellcheck
- How to prevent Chinese, Japanese, and Korean (CJK) characters from being flagged as spelling mistakes
- How to patch your dictionary to include words with typographic apostrophes (by default, words such as =doesnt= will be flagged as misspelled but =does't= won't be)
- How to add a custom status bar that displays word count, character count, and estimated reading time
With the introduction out of the way, let's get right into it!
* Soft line breaks
:PROPERTIES:
:CUSTOM_ID: soft-line-breaks
:END:
Most Vim configurations are done in the user's =~/.vimrc= file, which applies to all Vim sessions regardless of file type. However, we want our customizations to only apply to Markdown text files. Luckily, Vim has a feature that handles this. [[https://vim.fandom.com/wiki/File_type_plugins][Filetype plugins]] allow you to create Vim configurations that only apply to certain filetypes, and are stored in the directory =~/.vim/ftplugin/=. We are going to create one for our Markdown files: =~/.vim/ftplugin/markdown.vim=.
Inside this file, add the following:
#+begin_example
setlocal linebreak
#+end_example
We want to use =setlocal= (or its shorthand =setl=) over the usual =set= because this only sets =linebreak= on the current buffer. If we have multiple files open in Vim, we want to make sure that this setting only applies to open Markdown files.
#+begin_export html
<details>
<summary>
Thanks to u/habamax for pointing this out!
</summary>
<iframe id="reddit-embed" src="https://www.redditmedia.com/r/vim/comments/vc9oi2/how_to_configure_vim_for_writing/iccz7kq/?depth=1&amp;showmore=false&amp;embed=true&amp;showmedia=false" sandbox="allow-scripts allow-same-origin allow-popups" style="border: none;" scrolling="no" width="640" height="233">
</iframe>
</details>
#+end_export
And you're done! Vim will now no longer split words across soft-wrapped lines. Now, let's move on to typographic quotes.
* Typographic quotes
:PROPERTIES:
:CUSTOM_ID: typographic-quotes
:END:
Vim doesn't have any native support for typographic quotes, so we'll need to install the plugin [[https://github.com/preservim/vim-textobj-quote][preservim/vim-textobj-quote]]. There are a few different ways to install plugins in Vim, but in my opinion the easiest one to use is Vundle. If you are on Arch Linux, there is an [[https://aur.archlinux.org/packages/vundle][AUR package]], otherwise, see the [[https://github.com/VundleVim/Vundle.vim#quick-start][quick start guide]] on Vundle's GitHub repository.
Now that we have a package manager for Vim installed, we need to install the plugin itself. Open your =~/.vimrc=, or create it if it doesn't exist yet, and add the following:
#+begin_src vim
filetype off
set rtp+=~/.vim/bundle/Vundle.vim
call vundle#begin()
Plugin 'kana/vim-textobj-user' " essential dependency for typographic quotes
Plugin 'preservim/vim-textobj-quote' " typographic quotes
call vundle#end()
filetype plugin indent on
#+end_src
If you are unfamilar with Vundle, between =vundle#begin()= and =vundle#end()= is where all of the Vim plugins you want installed with Vundle are listed; the rest of the lines are various requirements for Vundle to work properly (for more information, see [[https://github.com/VundleVim/Vundle.vim#quick-start][Vundle's quick start guide]]). By default, Vundle assumes the plugins to be GitHub repositories, so all you need to write down is the repository name. [[https://github.com/kana/vim-textobj-user][kana/vim-textobj-user]] is an essential dependency for our typographic quote plugin that will handle text objects.
Now that you've listed the necessary plugins, you need to have Vundle install them. Save and exit =~/.vimrc=, open Vim, type =:PluginInstall=, and press enter. If you've done everything correctly, Vundle should pull all of the necessary plugins from GitHub, and once it's done you can type ~:q~ to exit the installation window.
The typographic quotes plugin is now installed, but it won't start up by default. Since we only want it to run on Markdown files, we open up =~/.vim/ftplugins/markdown.vim= again and add the following:
#+begin_src vim
call textobj#quote#init()
#+end_src
This will initialize the typographic quote plugin whenever you enter a Markdown file.
** Useful commands
:PROPERTIES:
:CUSTOM_ID: useful-commands
:END:
- Sometimes you may want to override the plugin to type non-typographic quotes. To do this, press ~Ctrl~ + ~V~ (V for Verbatim) and then type the single or double quotation mark.
- This plugin can also convert preexisting text to typographic quotes. There are [[https://github.com/preservim/vim-textobj-quote#replace-support][instructions on how to do this on the plugin's repository]], but I was having some difficulty getting them to work.
I ended up slightly using a slightly modified version of the instructions which maps ~\c~ to run the plugin replacing straight quotes with typographic "curly" quotes, and ~\s~ to do the inverse. (You can replace the backslash with any character, provided it isn't already mapped. The backslash key is handy, however, since it's one of the few keys that isn't connected to any commands in Vim's default configuration.)
#+begin_src vim
map \c <plug>ReplaceWithCurly
map \s <plug>ReplaceWithStraight
#+end_src
In normal mode, these commands replace all occurrences in the current paragraph, and in visual mode (how you do visual selection in Vim: press ~v~ while in normal mode) they replace all occurrences in the current selection. To replace throughout the entire document, Press ~gg~ to go to the top of the document, ~v~ to enter visual mode, ~G~ to jump to the last line of the document, ~$~ to go to the last character, selecting everything, and finally either ~\c~ or ~\s~ to run the plugin command. So, altogether: ~ggvG$~ + either ~\c~ for curly quotes or ~\s~ for straight quotes.
- For more information and advanced usage, [[https://github.com/preservim/vim-textobj-quote#vim-textobj-quote][see the plugin's README]].
* Spellcheck
:PROPERTIES:
:CUSTOM_ID: spellcheck
:END:
To enable spellcheck, add the following to your =~/.vim/ftplugin/.markdown.md=:
#+begin_src vim
set spell spelllang=en
set spelllang+=cjk " prevent CJK characters from being spellchecked
#+end_src
Of course, if you want spellchecking in another language, change =en= to whatever your language code is. The second line is optional, however if you often work with documents including Chinese, Japanese, or Korean (CJK) characters like me, this is handy since otherwise Vim will mark them all as spelling mistakes.
** Useful commands
:PROPERTIES:
:CUSTOM_ID: useful-commands-1
:END:
- ~zg~ adds a word to the dictionary
- ~z=~ brings a list of possible corrections for misspelled words
- ~zug~ or ~zuw~ removes a word from the dictionary
- ~:set nospell~ disables spellchecking for the current document
- ~:set spell~ enables spellchecking for the current document
** Patching dictionary for typographic quotes
:PROPERTIES:
:CUSTOM_ID: patching-dictionary-for-typographic-quotes
:END:
If you decided to skip the typographic quotes configuration from earlier, you can stop here, but otherwise we're going to have to patch our Vim spellcheck dictionary to include words with typographic apostrophes/single quotes (e.g. =doesnt=) which will be marked as incorrect otherwise.
[[https://vi.stackexchange.com/q/118][This]] thread on the Vi and Vim Stack Exchange was very helpful, so if you're having any trouble please refer to the two answers there.
First, create the directory =~/.vim/spell= and enter it. We're going to need two dictionary files, which are available [[http://wordlist.aspell.net/dicts/][here]]. Please check to see if there has been a newer version on the website and =wget= that instead, if the one I'm using (2020.12.07) has become out of date. I'm going to use the American English (setting =_LANG= to =en_US=) dictionary file, but Canadian (=en_CA=) and Australian (=en_AU=) English dictionaries are also available. If you need British English or larger word list, more archive downloads are available [[https://sourceforge.net/projects/wordlist/files/speller/2020.12.07/][here]], although I haven't tested them.
#+begin_src sh
$ mkdir -p ~/.vim/spell && cd ~/.vim/spell
$ _LANG=en_US # language variable for future commands, replace if needed
$ wget http://downloads.sourceforge.net/wordlist/hunspell-$_LANG-2020.12.07.zip
$ unzip *.zip $_LANG* && rm *.zip
#+end_src
In order to patch the dictionary, run the following command (see [[https://vi.stackexchange.com/a/172][this]] for an explanation of how it works). It takes all occurrences of words with apostrophes in the dictionary file and appends a version with the typographic version.
#+begin_src sh
$ grep "'" $_LANG.dic | sed "s/'//g" >> $_LANG.dic
#+end_src
Then, still in the =~/.vim/spell= directory, start Vim, and run the ~:mkspell! en en_US~ command, which should create an =en.utf-8.spl= file. You're done!
If you find more compatible dictionary files besides English that are confirmed working, please let me know and I'll add them to this article.
* Custom status bar
:PROPERTIES:
:CUSTOM_ID: custom-status-bar
:END:
Now, for the cherry on top: a custom status bar!
Add the following to your =~/.vim/ftplugin/markdown.vim=:
#+begin_src vim
function! Characters()
return strchars(join(getline(1, '$'), "\n"))
endfunction
function! Words()
return wordcount().words
endfunction
function! Minutes()
let wpm = 200
return (Words() + wpm / 2) / wpm
endfunction
set laststatus=2 " enable status line
set statusline+=%=%{Words()}\ words,
set statusline+=\ %{Characters()}\ characters,\ about
set statusline+=\ %{Minutes()}\ minutes
" remove ugly white background
hi StatusLine ctermfg=0 ctermbg=none cterm=bold " 0 for the terminal color 0
#+end_src
I haven't done any Vimscript before working on this configuration, so this is mostly cobbled this together from a few Stack Overflow threads. There might be better ways of doing some things!
The main things of note are the following:
- =strchars= is the only correct way to count the number of characters in a string. Many people suggest methods that count the number of bytes, but Unicode characters outside of ASCII will take up more than one byte and thus be counted as more than one character.
- If you need a byte counter in your status line, you can use =wordcount().bytes=.
- The =%== in the first =set statusline+== line makes everything following right-aligned.
- Spaces must be escaped using =\= when adding to the status line.
- By default, the status line background is bright white, so in the final line =ctermbg=none= removes this and makes it more subtle.
- I didn't want my status line to be brighter than my text and distracting, so I set its foreground color to =0= in the final line. In my terminal color palette, this is a subdued color, but you can try using any value between 0 and 15.
* Conclusion
:PROPERTIES:
:CUSTOM_ID: conclusion
:END:
That's all for this tutorial! I hope it was useful to you. For the last couple of weeks, I've been learning to use and configure Vim, and it's been a lot of fun. I made this article to share what I've learned, and also as a future reference for myself. Writing can be a good way of solidifying your knowledge, and I learned a lot writing this. This is my first "proper" blog post, so I hope it was alright. I'll be writing more in the future!
Oh, and one more bonus tip. The normal ~j~ and ~k~ navigation commands for going up or and down a line in Vim go over "hard" lines in the document, not soft-wrapped visual lines. If you want to go over visual lines as one often does when writing, prefix the command with ~g~ (for graphical, I guess?). So, for example, to go down one graphical line, use ~gj~. You can even prefix the chord with a number to repeat it multiple times, so to go up three visual lines, use ~3gk~! If you need to do this a lot, you might want to look into [[https://vim.fandom.com/wiki/Mapping_keys_in_Vim_-_Tutorial_(Part_1)][creating a custom mapping]].
See you in the next post!

@ -1,15 +0,0 @@
---
title: "Managing Rendering of LaTeX"
date: 2022-10-25
tags:
- programming
---
Previously Ive used `pdflatex` to render my LaTeX documents, but Ive just come across `latexmk`, which provides much more powerful options. Heres a list of the commands I make use of, taken from [this guide](https://mg.readthedocs.io/latexmk.html) by Matthias Geier (mgeier).
- `latexmk -pdf [file]` Generate a PDF file from a TeX file. The `-pdf` option prevents the additional generation of [DVI files](https://en.wikipedia.org/wiki/Device_independent_file_format), the machine-readable version of TeX. Omitting the file name will generate all of the files in the current directory.
- `latexmk -c [file]` Delete all extra temporary files created in the rendering process.
- `latexmk -C [file]` Delete all generated files, only leaving the original TeX files. (Clean directory.)
- `latexmk -pvc <file>` (File parameter only optional if there is only one TeX file in the directory.) Open up a previewer that automatically refreshes as you are editing your file! Previously, I was using the [xuhdev/vim-latex-live-preview](https://github.com/xuhdev/vim-latex-live-preview#usage) extension for Vim using the `:LLPStartPreview` command, but it was randomly `Failed to compile` errors, so it made debugging your markup difficult. By default (at least on my system), `-pvc` opens up the xdvik previewer. You can change this by [updating `~/.latexmkrc`](https://mg.readthedocs.io/latexmk.html#configuration-files).
Also a side note, when writing this up I came across [docopt](http://docopt.org/), a standard for writing CLI documentation. I had a general idea of the syntax already from seeing it used all over the place, but knowing that theres a standardized spec to refer to is nice. For example, I didnt realize until now that square brackets `[]` are used when arguments are optional, rather than the standard `<>`. The more you know.

@ -0,0 +1,12 @@
#+TITLE: Managing Rendering of LaTeX
#+DATE: 2022-10-25
#+FILETAGS: programming
Previously I've used =pdflatex= to render my LaTeX documents, but I've just come across =latexmk=, which provides much more powerful options. Here's a list of the commands I make use of, taken from [[https://mg.readthedocs.io/latexmk.html][this guide]] by Matthias Geier (mgeier).
- =latexmk -pdf [file]= Generate a PDF file from a TeX file. The =-pdf= option prevents the additional generation of [[https://en.wikipedia.org/wiki/Device_independent_file_format][DVI files]], the machine-readable version of TeX. Omitting the file name will generate all of the files in the current directory.
- =latexmk -c [file]= Delete all extra temporary files created in the rendering process.
- =latexmk -C [file]= Delete all generated files, only leaving the original TeX files. (Clean directory.)
- =latexmk -pvc <file>= (File parameter only optional if there is only one TeX file in the directory.) Open up a previewer that automatically refreshes as you are editing your file! Previously, I was using the [[https://github.com/xuhdev/vim-latex-live-preview#usage][xuhdev/vim-latex-live-preview]] extension for Vim using the =:LLPStartPreview= command, but it was randomly =Failed to compile= errors, so it made debugging your markup difficult. By default (at least on my system), =-pvc= opens up the xdvik previewer. You can change this by [[https://mg.readthedocs.io/latexmk.html#configuration-files][updating =~/.latexmkrc=]].
Also a side note, when writing this up I came across [[http://docopt.org/][docopt]], a standard for writing CLI documentation. I had a general idea of the syntax already from seeing it used all over the place, but knowing that there's a standardized spec to refer to is nice. For example, I didn't realize until now that square brackets =[]= are used when arguments are optional, rather than the standard =<>=. The more you know.

@ -1,15 +0,0 @@
---
title: "Soudan: a Comment System Built With Rust!"
date: 2022-07-24
tags:
- programming
---
I just finished working on Soudan, a Rust-based comment system!
Its now live on all posts on this site, feel free to give it a go in the comments at the bottom of the page! Markdown formatting is supported. If you submit your email address, it wont be public, and is only used to display your [Gravatar](https://gravatar.com/), if you have one.
Soudan is named after the word 相談 in Japanese, which means *discussion* or *consultation*.
The source code is available on ~~GitHub [here](https://github.com/ElnuDev/soudan)~~ my [Gogs](https://gogs.io/) instance [here](https://git.elnu.com/ElnuDev/soudan) under the GNU General Public License v3.0.

@ -0,0 +1,11 @@
#+TITLE: Soudan: a Comment System Built With Rust!
#+DATE: 2022-07-24
#+FILETAGS: programming
I just finished working on Soudan, a Rust-based comment system!
It's now live on all posts on this site, feel free to give it a go in the comments at the bottom of the page! Markdown formatting is supported. If you submit your email address, it won't be public, and is only used to display your [[https://gravatar.com/][Gravatar]], if you have one.
Soudan is named after the word 相談 in Japanese, which means /discussion/ or /consultation/.
The source code is available on +GitHub [[https://github.com/ElnuDev/soudan][here]]+ my [[https://gogs.io/][Gogs]] instance [[https://git.elnu.com/ElnuDev/soudan][here]] under the GNU General Public License v3.0.

@ -1,48 +0,0 @@
---
title: Tech maintenance
date: 2022-10-02
tags:
- programming
---
*[Cover photo](https://github.com/cat-milk/Anime-Girls-Holding-Programming-Books/blob/master/Linux/Sakurajima_Mai_Linux_Essentials.jpg) courtesy of [cat-milk/Anime-Girls-Holding-Programming-Books](https://github.com/cat-milk/Anime-Girls-Holding-Programming-Books).*
**This post is going to be a bit different than the usual programming posts. Its more for myself to organize my thoughts on things that need to be done.**
Overall, there are quite a few technological things that require maintenance. For a long time Ive just been doing whats easiest and what works, and now the side effects of that are piling up. Things Ive put off doing, etc. So heres a list of what needs to be maintained, in no particular order.
Im probably forgetting something, but this all I can think of for now. Ill be checking off and amending to this list with dates as I go along in the future.
#### Having a clear, fresh start with the server.
Currently, the server is running Ubuntu Desktop and still keeps quite a few unneeded GUI applications, which require updating. In addition, my desktop Linux distribution is now Arch Linux, so for having a more streamlined set up on desktop & server itd be best to put Arch Linux on it. (However, see [here](#look-into-nixoss-viability-as-an-arch-alternative-both-for-desktop-and-server-use).) Not to mention how much of a pain Debian package repositories are: I need to install brew on the server just to be able to get the latest version of Hugo.
#### Backups.
I need to have a solid, reliable backup system for both my server files and desktop files. Ideally, this would comprise of a secondary drive on the server on which periodic updates can be run (perhaps with rsync?) both from desktop and from the server. This drive should ideally by a terabyte, but something smaller could do in the meanwhile.
#### Beginning the transition away from GitHub.
For a variety of reasons already explained [here on the Software Freedom Conservancys website](https://sfconservancy.org/GiveUpGitHub/), its time to move away from GitHub. Git was originally intended to be a free and distributed version control system, but the prevalence of the sentiment “Git = GitHub” shows how Microsoft has exploited the FOSS community into becoming dependent on a completely proprietary platform, using its unfettered access to the code of developers around the world to its benefit by training GitHub Copilot on that data without consent. The obvious easy solution is simply setting up headless Git repositories with SSH access on ones own server as described [here](https://www.youtube.com/watch?v=ju9loeXNVW0) by Luke Smith. However, this only covers the easy backup aspect of GitHub as a service, not the web-based UI and repository aspect. This is where **Gitea** comes in. Its a FOSS self-hosted GitHub alternative that from what Ive seen seems a bit more lightweight than GitLab. However, even once Gitea is all set up and all my repositories are migrated to it, I unfortunately cannot delete my GitHub repositories right away. Soon, Ill need to make university applications to major in Computer Science, and its likely that a GitHub account will be expected to act as a portfolio. So, I should instead set up all my GitHub repositories a *mirrors* of my primary Gitea repos until they are no longer necessary. Speaking of portfolios, this leads to my next point.
#### Remake my website.
Yes, again, I know. My current website setup is a simple landing page at [elnu.com](https://elnu.com/) with links to [Tegaki Tuesday](https://tegakituesday.com/), etc. and some recently watched anime using [AniLists API](https://anilist.gitbook.io/anilist-apiv2-docs/), along with [blog.elnu.com](https://blog.elnu.com/) for miscellaneous content (been a while since I posted there). What I need to do is once I have all my public repositories on GitHub cleaned up and migrated to Gitea, I should make a portfolio list of all the various projects Ive worked on of note, since I dont really have something like that at the moment.
#### Set up an email server.
(TODO: look into [Luke Smiths video on the matter](https://www.youtube.com/watch?v=9zP7qooM4pY)) Currently, Im using ProtonMail. Its good and all, but unfortunately I have to pay in order to be able to use my own @elnu.com address, and my parents need vanity URLs for another domain name, so itd be rather annoying to have to pay for other accounts since my plan is only limited to one domain. In the past weve tried doing email through DreamHost, but it seemed like Gmail would shadow block all of those emails. Ive heard that self-hosting email can be a huge pain and Gmail (and potentially even setting up Cloudflare reverse DDNS) may be an issue, but its worth giving a go.
#### Look into NixOSs viability as an Arch alternative, both for desktop and server use.
My one point of frustration with Arch Linux is a “do it yourself distro” is the lack of a built-in way to keep track of configs. In your configuration process, you have to install various packages, run commands, and edit config files across your system. If you didnt keep a meticulous record of all of your steps, then you might have trouble settings everything up again on a second system. Even if you did, then you still have to make record of those tweaks that are made past the initial setup. The fact of the matter is, Im lazy. I havent bothered to create an install script for my current Arch configuration, or even a dotfiles repository.
Now, Ive just gotten a new laptop, a Lenovo ThinkPad X220, and Id like to have a painless method of keeping my system configurations synced between my desktop and laptop.
This is where NixOS comes in: a Linux distribution based on the Nix package manager that provides reproducible, declarative, and reliable system configuration. Instead of installing packages in the command line (though you can still do that if necessary for packages you only need temporarily), you declare them in the system configuration file `/etc/nixos/configuration.nix`. In this file, you can configure anything you can imagine: users and their permissions, what display manager and window manager/desktop environment you want (if any), how GRUB should be configured, etc.
In addition, NixOS provides Home Manager (which I havent much looked into yet), a similar system, except for particular users configuration. It gives much more fine-grain control, and lets you manage individual programs configurations, and even have programs installed for one particular user.
The magic of this is you can set up a Git repository for all of your Nix (`.nix`) configuration files and keep it synced between multiple devices, meaning you can maintain a consistent user experience across devices. Just partition your drive, copy over your configuration, build, and youre ready to go. And I havent even mentioned NixOSs rollback system!
Currently, I messing with NixOS in a virtual machine following [this](https://www.youtube.com/watch?v=AGVXJ-TIv3Y) (lengthy) tutorial by Matthias Benaets. Hopefully, once I get a configuration set up in there more or less matching my current desktop setup, I can get it transferred to & and set up on the ThinkPad! Well see; Im still not completely certain whether NixOS is practical as a desktop distribution.

@ -0,0 +1,57 @@
#+TITLE: Tech Maintenance
#+DATE: 2022-10-02
#+FILETAGS: programming
/[[https://github.com/cat-milk/Anime-Girls-Holding-Programming-Books/blob/master/Linux/Sakurajima_Mai_Linux_Essentials.jpg][Cover photo]] courtesy of [[https://github.com/cat-milk/Anime-Girls-Holding-Programming-Books][cat-milk/Anime-Girls-Holding-Programming-Books]]./
*This post is going to be a bit different than the usual programming posts. It's more for myself to organize my thoughts on things that need to be done.*
Overall, there are quite a few technological things that require maintenance. For a long time I've just been doing what's easiest and what works, and now the side effects of that are piling up. Things I've put off doing, etc. So here's a list of what needs to be maintained, in no particular order.
I'm probably forgetting something, but this all I can think of for now. I'll be checking off and amending to this list with dates as I go along in the future.
* Having a clear, fresh start with the server.
:PROPERTIES:
:CUSTOM_ID: having-a-clear-fresh-start-with-the-server.
:END:
Currently, the server is running Ubuntu Desktop and still keeps quite a few unneeded GUI applications, which require updating. In addition, my desktop Linux distribution is now Arch Linux, so for having a more streamlined set up on desktop & server it'd be best to put Arch Linux on it. (However, [[Look into NixOS's viability as an Arch alternative, both for desktop and server use.][here]].) Not to mention how much of a pain Debian package repositories are: I need to install brew on the server just to be able to get the latest version of Hugo.
* Backups.
:PROPERTIES:
:CUSTOM_ID: backups.
:END:
I need to have a solid, reliable backup system for both my server files and desktop files. Ideally, this would comprise of a secondary drive on the server on which periodic updates can be run (perhaps with rsync?) both from desktop and from the server. This drive should ideally by a terabyte, but something smaller could do in the meanwhile.
* Beginning the transition away from GitHub.
:PROPERTIES:
:CUSTOM_ID: beginning-the-transition-away-from-github.
:END:
For a variety of reasons already explained [[https://sfconservancy.org/GiveUpGitHub/][here on the Software Freedom Conservancy's website]], it's time to move away from GitHub. Git was originally intended to be a free and distributed version control system, but the prevalence of the sentiment "Git = GitHub" shows how Microsoft has exploited the FOSS community into becoming dependent on a completely proprietary platform, using its unfettered access to the code of developers around the world to its benefit by training GitHub Copilot on that data without consent. The obvious easy solution is simply setting up headless Git repositories with SSH access on one's own server as described [[https://www.youtube.com/watch?v=ju9loeXNVW0][here]] by Luke Smith. However, this only covers the easy backup aspect of GitHub as a service, not the web-based UI and repository aspect. This is where *Gitea* comes in. It's a FOSS self-hosted GitHub alternative that from what I've seen seems a bit more lightweight than GitLab. However, even once Gitea is all set up and all my repositories are migrated to it, I unfortunately cannot delete my GitHub repositories right away. Soon, I'll need to make university applications to major in Computer Science, and it's likely that a GitHub account will be expected to act as a portfolio. So, I should instead set up all my GitHub repositories a /mirrors/ of my primary Gitea repos until they are no longer necessary. Speaking of portfolios, this leads to my next point.
* Remake my website.
:PROPERTIES:
:CUSTOM_ID: remake-my-website.
:END:
Yes, again, I know. My current website setup is a simple landing page at [[https://elnu.com/][elnu.com]] with links to [[https://tegakituesday.com/][Tegaki Tuesday]], etc. and some recently watched anime using [[https://anilist.gitbook.io/anilist-apiv2-docs/][AniList's API]], along with [[https://blog.elnu.com/][blog.elnu.com]] for miscellaneous content (been a while since I posted there). What I need to do is once I have all my public repositories on GitHub cleaned up and migrated to Gitea, I should make a portfolio list of all the various projects I've worked on of note, since I don't really have something like that at the moment.
* Set up an email server.
:PROPERTIES:
:CUSTOM_ID: set-up-an-email-server.
:END:
(TODO: look into [[https://www.youtube.com/watch?v=9zP7qooM4pY][Luke Smith's video on the matter]]) Currently, I'm using ProtonMail. It's good and all, but unfortunately I have to pay in order to be able to use my own [cite/t:@elnu.com] address, and my parents need vanity URLs for another domain name, so it'd be rather annoying to have to pay for other accounts since my plan is only limited to one domain. In the past we've tried doing email through DreamHost, but it seemed like Gmail would shadow block all of those emails. I've heard that self-hosting email can be a huge pain and Gmail (and potentially even setting up Cloudflare reverse DDNS) may be an issue, but it's worth giving a go.
* Look into NixOS's viability as an Arch alternative, both for desktop and server use.
:PROPERTIES:
:CUSTOM_ID: look-into-nixoss-viability-as-an-arch-alternative-both-for-desktop-and-server-use.
:END:
My one point of frustration with Arch Linux is a "do it yourself distro" is the lack of a built-in way to keep track of configs. In your configuration process, you have to install various packages, run commands, and edit config files across your system. If you didn't keep a meticulous record of all of your steps, then you might have trouble settings everything up again on a second system. Even if you did, then you still have to make record of those tweaks that are made past the initial setup. The fact of the matter is, I'm lazy. I haven't bothered to create an install script for my current Arch configuration, or even a dotfiles repository.
Now, I've just gotten a new laptop, a Lenovo ThinkPad X220, and I'd like to have a painless method of keeping my system configurations synced between my desktop and laptop.
This is where NixOS comes in: a Linux distribution based on the Nix package manager that provides reproducible, declarative, and reliable system configuration. Instead of installing packages in the command line (though you can still do that if necessary for packages you only need temporarily), you declare them in the system configuration file =/etc/nixos/configuration.nix=. In this file, you can configure anything you can imagine: users and their permissions, what display manager and window manager/desktop environment you want (if any), how GRUB should be configured, etc.
In addition, NixOS provides Home Manager (which I haven't much looked into yet), a similar system, except for particular users' configuration. It gives much more fine-grain control, and lets you manage individual programs' configurations, and even have programs installed for one particular user.
The magic of this is you can set up a Git repository for all of your Nix (=.nix=) configuration files and keep it synced between multiple devices, meaning you can maintain a consistent user experience across devices. Just partition your drive, copy over your configuration, build, and you're ready to go. And I haven't even mentioned NixOS's rollback system!
Currently, I messing with NixOS in a virtual machine following [[https://www.youtube.com/watch?v=AGVXJ-TIv3Y][this]] (lengthy) tutorial by Matthias Benaets. Hopefully, once I get a configuration set up in there more or less matching my current desktop setup, I can get it transferred to & and set up on the ThinkPad! We'll see; I'm still not completely certain whether NixOS is practical as a desktop distribution.

@ -1,34 +1,33 @@
---
title: "ウォルマートの[悪]{あく}[習]{しゅう}[慣]{かん}"
date: 2022-07-04
tags:
- nihongo
slug: the-evils-of-walmart
hidden: true
---
#+TITLE: ウォルマートの[悪]{あく}[習]{しゅう}[慣]{かん}
#+DATE: 2022-07-04
#+FILETAGS: nihongo
#+HIDDEN: t
#+HUGO_SLUG: the-evils-of-walmart
ウォルマートはアメリカのスーパーの[会]{かい}[社]{しゃ}です。ウォルマートは[大]{おお}きい[店]{みせ}で[有]{ゆう}[名]{めい}だけど、[実]{じつ}はアマゾンに[次]{つ}ぐ[世]{せ}[界]{かい}[第]{だい}[二]{に}[位]{い}のオンライン[通]{つう}[販]{はん}[会]{がい}[社]{しゃ}です。そのため、ウォルマートの[行]{こう}[動]{どう}は[沢]{たく}[山]{さん}の[客]{きゃく}と[店]{てん}[員]{いん}に[影]{えい}[響]{きょう}していて、あるまずいことをしているのを[人]{ひと}[々]{びと}に[教]{おし}えてあげるのは[必]{ひつ}[要]{よう}だと[思]{おも}います。ウォルマートで[買]{か}い[物]{もの}する[人]{ひと}はウォルマートの[悪]{あく}[習]{しゅう}[慣]{かん}について[知]{し}っていば、[他]{ほか}のよりいい[商]{しょう}[習]{しゅう}[慣]{かん}がある[会]{かい}[社]{しゃ}でショッピングし[始]{はじ}めるかもしれないので、ウォルマートがよくさせるかもしれません。
[例]{たと}えば、2016[年]{ねん}にはウォルマートがバタリーケージからの[卵]{たまご}を2025[年]{ねん}までに[売]{う}るのを[止]{や}めると[言]{い}っていましたが、その「ケージフリー」なバタリーケージからじゃない[卵]{たまご}は[更]{さら}に[悪]{わる}いファームからです。そのファームでは、それぞれのニワトリは1〜1.5[平]{へい}[方]{ほう}フィートだけで[生]{い}きます。その[環]{かん}[境]{きょう}ではニワトリは[他]{ほか}のニワトリを[食]{た}べているとかをしてしまって、バタリーケージに[比]{くら}べて[死]{し}ぬ[可]{か}[能]{のう}[性]{せい}が[高]{たか}くなります。
それだけではありません。[最]{さい}[近]{きん}アメリカでは「オピオイドクライシス」があります。[普]{ふ}[通]{つう}、オピオイドは[鎮]{ちん}[痛]{つう}[剤]{ざい}[的]{てき}な[薬]{くすり}だけど、アメリカ[中]{じゅう}では[沢]{たく}[山]{さん}の[人]{ひと}[々]{びと}がそのオピオイドに[依]{い}[存]{ぞん}していて、しすぎて[死]{し}にます。ウォルマート[店]{みせ}には[薬]{やっ}[局]{きょく}があって、[去]{きょ}[年]{ねん}アメリカの[政]{せい}[府]{ふ}はそのような[薬]{くすり}を[買]{か}いやすすぎるからウォルマートと二つの[他]{ほか}の[薬]{やっ}[局]{きょく}[会]{がい}[社]{しゃ}のせいでクライシスがひどくなったと分かりました。
それだけではありません。[最]{さい}[近]{きん}アメリカでは「オピオイドクライシス」があります。[普]{ふ}[通]{つう}、オピオイドは[鎮]{ちん}[痛]{つう}[剤]{ざい}[的]{てき}な[薬]{くすり}だけど、アメリカ[中]{じゅう}では[沢]{たく}[山]{さん}の[人]{ひと}[々]{びと}がそのオピオイドに[依]{い}[存]{ぞん}していて、しすぎて[死]{し}にます。ウォルマート[店]{みせ}には[薬]{やっ}[局]{きょく}があって、[去]{きょ}[年]{ねん}アメリカの[政]{せい}[府]{ふ}はそのような[薬]{くすり}を[買]{か}いやすすぎるからウォルマートと二つの[他]{ほか}の[薬]{やっ}[局]{きょく}[会]{がい}[社]{しゃ}のせいでクライシスがひどくなったと分かりました。
それが[二]{ふた}つのウォルマートが[悪]{わる}い[理]{り}[由]{ゆう}ですよ。そのため、ウォルマートで[買]{か}い[物]{もの}に[行]{い}かないでください。
### [単]{たん}[語]{ご}[表]{ひょう}
| [単]{たん}[語]{ご} | [意]{い}[味]{み} |
| --- | --- |
| 〜に[次]{つ}ぐ | in rank after... |
| 〜[位]{い} | counter for ranking |
| [通]{つう}[販]{はん} | mail order |
| [行]{こう}[動]{どう} | behavior; conduct |
| [店]{てん}[員]{いん} | store employee |
| [影]{えい}[響]{きょう} | effect |
| [悪]{あく}[習]{しゅう}[慣]{かん} | evil practices; bad habit |
| [商]{しょう}[習]{しゅう}[慣]{かん} | business practices |
| [更]{さら}に〜 | even more... |
| [環]{かん}[境]{きょう} | environment; conditions |
| [鎮]{ちん}[痛]{つう}[剤]{ざい} | painkiller |
| [依]{い}[存]{ぞん} | dependence |
| [薬]{やっ}[局]{きょく} | pharmacy |
* [単]{たん}[語]{ご}[表]{ひょう}
:PROPERTIES:
:CUSTOM_ID: 単たん語ご表ひょう
:END:
| [単]{たん}[語]{ご} | [意]{い}[味]{み} |
|------------------------------------+---------------------------|
| 〜に[次]{つ}ぐ | in rank after... |
| 〜[位]{い} | counter for ranking |
| [通]{つう}[販]{はん} | mail order |
| [行]{こう}[動]{どう} | behavior; conduct |
| [店]{てん}[員]{いん} | store employee |
| [影]{えい}[響]{きょう} | effect |
| [悪]{あく}[習]{しゅう}[慣]{かん} | evil practices; bad habit |
| [商]{しょう}[習]{しゅう}[慣]{かん} | business practices |
| [更]{さら}に〜 | even more... |
| [環]{かん}[境]{きょう} | environment; conditions |
| [鎮]{ちん}[痛]{つう}[剤]{ざい} | painkiller |
| [依]{い}[存]{ぞん} | dependence |
| [薬]{やっ}[局]{きょく} | pharmacy |

@ -1,83 +0,0 @@
---
title: "The Joy of Data Processing"
date: 2022-07-27
tags:
- programming
- japanese
---
For a project Im working on (Ill make a post about it once its done), I needed a large list of Japanese words, with the requirements being that the words be short and without kanji (only in hiragana). In addition, ideally they should be simple words that the average Japanese learner would know, and must be in a machine-readable format that I can use in JavaScript.
Luckily, I found something that matches these criteria! A [JLPT N5 vocabulary list from JLPT Matome](https://www.jlptmatome.com/jlpt-n5-vocabulary-list/) that covers 549 words, which should hopefully be more than enough for my needs.
However, the list is in the form of HTML tables, and so its going to needs some work. In addition, the pronunciation is provided only in romaji, so well need to convert it to hiragana.
This is where the joy of data processing and web scraping comes in: refining a data source step-by-step until its something you can work with. This post is going to be less of a tutorial and more of a walk-through of a process that I found fun.
### Extracting the raw data
The first step is to convert the tables to a [CSV file](https://en.wikipedia.org/wiki/Comma-separated_values). If youre unfamiliar with CSV, its basically the most basic way of storing spreadsheet data in a file. On each line, theres a comma-separated list of values (hence its name, a *Comma-Separated Values* file), one for each column, and each line corresponds with a row.
There are various command-line applications and browser extensions that convert HTML to CSV, but what I ended up using is [this website](https://www.convertcsv.com/html-table-to-csv.htm). Using a website for conversions is cringe, I know, but when youre doing a one-off thing and dont need to write any scripts to do the job, utility websites are often the easiest way to get the job done.
Its pretty handy, and it does the URL fetching for you. The only annoyance was that the source was paginated onto 11 pages, so I had to do each page separately then put them together, but after that was all done I had a nice CSV file:
```CSV
1,あげる,ageru,to give
2,朝,asa,morning
3,封筒,fuutou,envelope
4,冬,fuyu,winter
5,五,go,five
...
```
Now, this is more data than we need. I only need the third column with the pronunciations (well convert these into hiragana later), so one can remove the first, second, and fourth column in some spreadsheet software like LibreOffice and then reexport a single-column CSV file with just the romaji. Once thats done, we have a text file that is just a list of pronunciations:
```CSV
ageru
asa
fuutou
fuyu
go
...
```
### Converting to hiragana
After a bit of research, I found [koozaki/romaji-conv](https://github.com/koozaki/romaji-conv), an [npm package](https://www.npmjs.com/package/@koozaki/romaji-conv) that does exactly what I need. Theres a [web-based demo](https://romaji-conv.koozaki.com/) that you can use if you want to quickly try it out or do a quick conversion, but it also has a CLI (command-line interface), which is what well use.
Assuming you already have Node.js and npm installed, you can globally install it with the following command (`i` is a an alias for `install`, `-g` installs the package globally to your system instead of to a particular projects `node_modules`):
```SH
npm i -g @koozaki/romaji-conv
```
We can use the following command to run romaji-conv on each line of our romaji file, `romaji.csv`, and push the resulting hiragana version to a new file, `hiragana.csv`. Previously, I wasnt familiar with `xargs` but I found this solution thanks to [this Stack Overflow answer](https://stackoverflow.com/a/29836986).
```SH
cat romaji.csv | xargs -L1 romaji-conv > hiragana.csv
```
### Converting into a JSON list
The final step is to convert this simple text file list into a JSON list/array that we can put directly into our JavaScript for use, in a format such as the following:
```JSON
["あげる", "あさ", "ふうとう", "ふゆ", "ご", ...]
```
To do this, all we need to do is a simple find-and-replace in any text editor, first replacing each new line `\n` with `", "`, and then finally adding the starting `["` and closing `"]`.
Were done!
### Closing thoughts
The things Ive done here by themselves are not in any way groundbreaking. I could have done some things more elegantly, and honestly none of this is anything to call home about. However, I wrote this post anyway because I wanted to show the power of step-by-step data processing and manipulation, and how one with a bit of time one can mutate data sources into whatever form is needed.
If you see data in tables or some other form online, dont give up! Getting it into the form you need isnt going to be as hard or time-consuming as you think.
If youre interested in Japanese text manipulation, please do check out Kojiro Ozakis romaji-conv on GitHub and give it a star! :star: Its super handy and easy to use, and is painfully underrated at only 8 stars (including mine) at the time of writing.
See you in the next post!
じゃーね!

@ -0,0 +1,87 @@
#+TITLE: The Joy of Data Processing
#+DATE: 2022-07-27
#+FILETAGS: programming japanese
For a project I'm working on (I'll make a post about it once it's done), I needed a large list of Japanese words, with the requirements being that the words be short and without kanji (only in hiragana). In addition, ideally they should be simple words that the average Japanese learner would know, and must be in a machine-readable format that I can use in JavaScript.
Luckily, I found something that matches these criteria! A [[https://www.jlptmatome.com/jlpt-n5-vocabulary-list/][JLPT N5 vocabulary list from JLPT Matome]] that covers 549 words, which should hopefully be more than enough for my needs.
However, the list is in the form of HTML tables, and so it's going to needs some work. In addition, the pronunciation is provided only in romaji, so we'll need to convert it to hiragana.
This is where the joy of data processing and web scraping comes in: refining a data source step-by-step until it's something you can work with. This post is going to be less of a tutorial and more of a walk-through of a process that I found fun.
* Extracting the raw data
:PROPERTIES:
:CUSTOM_ID: extracting-the-raw-data
:END:
The first step is to convert the tables to a [[https://en.wikipedia.org/wiki/Comma-separated_values][CSV file]]. If you're unfamiliar with CSV, it's basically the most basic way of storing spreadsheet data in a file. On each line, there's a comma-separated list of values (hence it's name, a /Comma-Separated Values/ file), one for each column, and each line corresponds with a row.
There are various command-line applications and browser extensions that convert HTML to CSV, but what I ended up using is [[https://www.convertcsv.com/html-table-to-csv.htm][this website]]. Using a website for conversions is cringe, I know, but when you're doing a one-off thing and don't need to write any scripts to do the job, utility websites are often the easiest way to get the job done.
It's pretty handy, and it does the URL fetching for you. The only annoyance was that the source was paginated onto 11 pages, so I had to do each page separately then put them together, but after that was all done I had a nice CSV file:
#+begin_example
1,あげる,ageru,to give
2,朝,asa,morning
3,封筒,fuutou,envelope
4,冬,fuyu,winter
5,五,go,five
...
#+end_example
Now, this is more data than we need. I only need the third column with the pronunciations (we'll convert these into hiragana later), so one can remove the first, second, and fourth column in some spreadsheet software like LibreOffice and then reexport a single-column CSV file with just the romaji. Once that's done, we have a text file that is just a list of pronunciations:
#+begin_example
ageru
asa
fuutou
fuyu
go
...
#+end_example
* Converting to hiragana
:PROPERTIES:
:CUSTOM_ID: converting-to-hiragana
:END:
After a bit of research, I found [[https://github.com/koozaki/romaji-conv][koozaki/romaji-conv]], an [[https://www.npmjs.com/package/@koozaki/romaji-conv][npm package]] that does exactly what I need. There's a [[https://romaji-conv.koozaki.com/][web-based demo]] that you can use if you want to quickly try it out or do a quick conversion, but it also has a CLI (command-line interface), which is what we'll use.
Assuming you already have Node.js and npm installed, you can globally install it with the following command (=i= is a an alias for =install=, =-g= installs the package globally to your system instead of to a particular project's =node_modules=):
#+begin_src sh
npm i -g @koozaki/romaji-conv
#+end_src
We can use the following command to run romaji-conv on each line of our romaji file, =romaji.csv=, and push the resulting hiragana version to a new file, =hiragana.csv=. Previously, I wasn't familiar with =xargs= but I found this solution thanks to [[https://stackoverflow.com/a/29836986][this Stack Overflow answer]].
#+begin_src sh
cat romaji.csv | xargs -L1 romaji-conv > hiragana.csv
#+end_src
* Converting into a JSON list
:PROPERTIES:
:CUSTOM_ID: converting-into-a-json-list
:END:
The final step is to convert this simple text file list into a JSON list/array that we can put directly into our JavaScript for use, in a format such as the following:
#+begin_example
["あげる", "あさ", "ふうとう", "ふゆ", "ご", ...]
#+end_example
To do this, all we need to do is a simple find-and-replace in any text editor, first replacing each new line =\n= with =", "=, and then finally adding the starting =["= and closing ="]=.
We're done!
* Closing thoughts
:PROPERTIES:
:CUSTOM_ID: closing-thoughts
:END:
The things I've done here by themselves are not in any way groundbreaking. I could have done some things more elegantly, and honestly none of this is anything to call home about. However, I wrote this post anyway because I wanted to show the power of step-by-step data processing and manipulation, and how one with a bit of time one can mutate data sources into whatever form is needed.
If you see data in tables or some other form online, don't give up! Getting it into the form you need isn't going to be as hard or time-consuming as you think.
If you're interested in Japanese text manipulation, please do check out Kojiro Ozaki's romaji-conv on GitHub and give it a star! :star: It's super handy and easy to use, and is painfully underrated at only 8 stars (including mine) at the time of writing.
See you in the next post!
じゃーね!

@ -1,12 +1,9 @@
---
title: "バナナはなんで[放]{ほう}[射]{しゃ}を[放]{ほう}[出]{しゅつ}しているか?"
date: 2022-06-25
tags:
- nihongo
description: バナナはなんで放射性が放出しているのを疑問に思うかもしれません。
slug: why-are-bananas-radioactive
hidden: true
---
#+TITLE: バナナはなんで[放]{ほう}[射]{しゃ}を[放]{ほう}[出]{しゅつ}しているか?
#+DATE: 2022-06-25
#+FILETAGS: nihongo
#+DESCRIPTION: バナナはなんで放射性が放出しているのを疑問に思うかもしれません。
#+HIDDEN: t
#+HUGO_SLUG: why-are-bananas-radioactive
バナナはなんで[放]{ほう}[射]{しゃ}[性]{せい}が[放]{ほう}[出]{しゅつ}しているのを[疑]{ぎ}[問]{もん}に[思]{お}うかもしれません。
@ -14,15 +11,17 @@ hidden: true
ガイガーカウンターが持っていばバナナを[前]{まえ}に[出]{だ}したら、その[放]{ほう}[射]{しゃ}が[見]{み}つけられますよ!しかし、[気]{き}にしないでください。バナナの[中]{なか}で[見]{み}つけられる[放]{ほう}[射]{しゃ}は[危]{き}[険]{けん}な[程]{ほど}ではありません。
### [単]{たん}[語]{ご}[表]{ひょう}
| [単]{たん}[語]{ご} | [意]{い}[味]{み} |
| ------------------------------------- | --------------------- |
| [放]{ほう}[射]{しゃ} | radiation |
| [放]{ほう}[射]{しゃ}[性]{せい} | radioactive |
| [放]{ほう}[出]{しゅつ}する | to emit radiation |
| [疑]{ぎ}[問]{もん}に[思]{おも}う | to wonder |
| カリウム  | Potassium |
| [同]{どう}[位]{い}[体]{たい} | isotope |
| そのため  | because of this |
| [程]{ほど}  | extent; degree |
* [単]{たん}[語]{ご}[表]{ひょう}
:PROPERTIES:
:CUSTOM_ID: 単たん語ご表ひょう
:END:
| [単]{たん}[語]{ご} | [意]{い}[味]{み} |
|------------------------------+-------------------|
| [放]{ほう}[射]{しゃ} | radiation |
| [放]{ほう}[射]{しゃ}[性]{せい} | radioactive |
| [放]{ほう}[出]{しゅつ}する | to emit radiation |
| [疑]{ぎ}[問]{もん}に[思]{おも}う | to wonder |
| カリウム | Potassium |
| [同]{どう}[位]{い}[体]{たい} | isotope |
| そのため | because of this |
| [程]{ほど} | extent; degree |

@ -0,0 +1,12 @@
{ pkgs ? import <nixpkgs> {} }:
with pkgs;
mkShell {
buildInputs = [
emacs-nox
hugo
stork
html-tidy
];
}

@ -1,34 +0,0 @@
#!/usr/bin/env bash
main() {
declare hugo_publish_dir=public
declare stork_config_file=stork.config.json # generated by hugo
declare stork_index_file=home.st # generated by stork (json suffix triggers gzip/br compression)
declare stork_arch=stork-ubuntu-20-04
declare stork_releases=https://files.stork-search.net/releases
declare stork_version=1.5.0
declare stork_exec=${stork_arch}-${stork_version}
declare stork_url=${stork_releases}/v${stork_version}/${stork_arch}
# Install Stork if it's not already installed.
if [[ ! -f "${stork_exec}" ]]; then
echo -e "\nInstalling Stork...\n"
wget --no-verbose "${stork_url}" ||
{ echo "Error: unable to wget ${stork_url}"; exit 1; }
mv "${stork_arch}" "${stork_exec}" ||
{ echo "Error: unable to mv ${stork_arch} ${stork_exec}"; exit 1; }
chmod +x "${stork_exec}" ||
{ echo "Error: unable to chmod ${stork_exec}"; exit 1; }
fi
# Build the Stork index.
echo -e "\nBuilding Stork index...\n"
./${stork_exec} build --input "${hugo_publish_dir}/${stork_config_file}" --output "${hugo_publish_dir}/${stork_index_file}" ||
{ echo "Error: unable to run stork"; exit 1; }
}
set -euo pipefail
main "$@"
Loading…
Cancel
Save