💾 Archived View for ecs.d2evs.net › feed.xml captured on 2024-05-12 at 14:49:29.

View Raw

More Information

⬅️ Previous capture (2024-05-10)

➡️ Next capture (2024-06-16)

-=-=-=-=-=-=-

<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet href="/feed.xsl" type="text/xsl"?>
<feed xmlns="http://www.w3.org/2005/Atom">
	<title>ecs's blog</title>
	<link href="gemini://ecs.d2evs.net" />
	<updated>
2024-03-28T12:00:00Z
	</updated>
	<id>gemini://ecs.d2evs.net</id>
	<entry>
		<title>the story of a man named charlie</title>
		<link rel="alternate" href="gemini://ecs.d2evs.net/posts/2024-03-28-charlie.gmi" />
		<id>gemini://ecs.d2evs.net/posts/2024-03-28-charlie.gmi</id>
		<updated>2024-03-28T12:00:00Z</updated>
		<content type="xhtml">
			<div xmlns="http://www.w3.org/1999/xhtml">
			<p>there's this song, right, it's called "M.T.A." (or charlie on the mta, depending on who you ask), and i think i'm getting a bit obsessed</p>
<p>it was a campaign song for a mayoral candidate in boston in the late '40s, written by some local folk musicians, and it's become part of the mbta's¹ lore. our fare cards are named after the protagonist of the song, charlie, who gets stuck on the t because he doesn't have a nickel for a then-recently-instituted exit fare that the mayoral candidate was opposing</p>
<p>¹ the greater boston public transit system, colloquially known as the t</p>
<p>anyways, so the really cool thing is that, in addition to performing the song live, the folk artists also did a recording of the song (along with a few other songs that didn't end up getting a life of their own), and that recording survives to this day</p>
<p><a href="http://aberman.org/MTA/PA/songs/MTAsong.mp3">the original recording of M.T.A.</a></p>
<p>this is especially neat cause it allows us to trace various small mutations to the lyrics starting from a relatively canonical baseline, with a couple caveats. first and most obviously, the recording is just straight-up missing a verse. nobody knows why, but it's definitively acknowledged that that verse exists, and we know what it is from various other accounts. secondly, and a bit more subtly: even to the extent that canonicality is a reasonable thing to assign to the lyrics of a folk song, i'm not fully convinced that i can trust the version they happened to record to match the version everything else is descended from</p>
<p>with that in mind, i uh. i have somewhere between 6 and 8 different versions of the lyrics on hand. we'll go in chronological order</p>
<p><a href="https://p.d2evs.net/No2blrF_y1N7d.txt">the original recording from 1949 ("original")</a></p>
<p><a href="https://p.d2evs.net/lV7Fep7yP~vXP.txt">the recording done by Will Holt. there're two different versions, both are described ("holt")</a></p>
<p><a href="https://p.d2evs.net/PbuhbZWYphiL4.txt">the recording done by the kingston trio ("kingston")</a></p>
<p><a href="https://p.d2evs.net/2CHYLvtIVO47x.txt">the recording done by Jackie Steiner, one of the original composers ("steiner")</a></p>
<p><a href="https://p.d2evs.net/UI234NKLPM9Pt.txt">the version from a paper linked below going over the song's history ("dreier", the first author of the paper)</a></p>
<p><a href="https://p.d2evs.net/9W61RW9EnOZu7.txt">and finally, the reason i started writing this post, an informational poster thingy from the mbta i found in Boston in Transit (2023) ("transit")</a></p>
<p>Jackie Steiner also lead a workshop sorta thing at GGG in 2009, and a recording of it is available online. she lead a sing-through of the song, and the version she sang there is equivalent to her other recording except for potentially being "through" rather than "to" in "as his train rolled on to/through greater boston", as well as having an additional french verse/chorus which i can't transcribe cause i don't french</p>
<p><a href="https://p.d2evs.net/RTVNVyT7yi0Jm.txt">here's a detailed description of each of the differences between all six</a></p>
<p>there's a few conclusions we can try to draw from this:</p>
<ul>
<li>we know from Dreier that kingston's recording is descended from holt's, which mostly matches the lyrical changes. kingston makes a few changes to the holt lyrics, but the only place where those changes match the other recordings is "charlie"/"poor charlie". i'm not particularly sure what to make of this - it could be that the random walk between holt and kingston happened to invert the random walk from the original to holt, or it could be that they both got it from some intermediate source with all the changes holt has except for dropping "poor"</li>
<li>holt and kingston are markedly different from the rest of the versions, their changes don't overlap except for "off the train" turning into "off that train", which happens everywhere except for the original and the transit version</li>
<li>dreier and steiner seem to be more closely related, with dreier likely closer to the original. all the locations where dreier disagrees with the consensus, it agrees with steiner. the exception is one line in the second verse which both dreier and steiner change, but which they replace with different lines</li>
<li>original and transit also seem to be closely related, with a similar lack of changes that overlap exclusively with other versions</li>
</ul>
<p>another note: i suspect that "car" in the "greater boston" verse was likely the original, despite 5 of the 6 versions using "train" instead. the fare increase described in the song was along the streetcar routes, and jp (where charlie changed for in the second verse) was (and still is) exclusively served by streetcars and buses, rather than rapid transit trains</p>
<p>i'm kinda curious where the mbta got their lyrics, since they seem like they might even be a bit more canonical than the original recording, but i wasn't able to track down the citation from Boston in Transit. if anyone decides to dig a bit further down this rabbit hole and ends up finding something, please let me know!</p>
<p>oh, and btw, i barely even touched on the rest of the history of this song. there's a surprising amount of stuff to it, i highly recommend Dreier's paper along with the rest of Arnold Berman's website, it's where i got most of my information for this exploration</p>
<p><a href="http://aberman.org/MTA/PA/images/Am.%20Music.pdf">Dreier's paper</a></p>
<p><a href="http://aberman.org/MTA/">Berman's website</a></p>
			</div>
		</content>
	</entry>
	<entry>
		<title>madeline: a line editing library for hare</title>
		<link rel="alternate" href="gemini://ecs.d2evs.net/posts/2024-02-13-madeline.gmi" />
		<id>gemini://ecs.d2evs.net/posts/2024-02-13-madeline.gmi</id>
		<updated>2024-02-13T12:00:00Z</updated>
		<content type="xhtml">
			<div xmlns="http://www.w3.org/1999/xhtml">
			<p>a year and five days ago, i decided to write a line editor. i've been using it in my daily-driver shell for nearly that long now, and i'm quite pleased with where i ended up. let's walk through it!</p>
<p>my main goal for madeline was to guarantee that, as a baseline, line-mode hare programs in the wild would have a comfortable and consistent line-mode ui. what `getopt::` does for command-line interfaces, i wanted madeline to do for line-mode interfaces</p>
<h2>rendering and unicode support</h2>
<p>(caveat: it's been at least six months since i seriously thought through the logic that lead me to what madeline does here today, so i might be misremembering some things)</p>
<p>madeline (currently) renders exclusively to a terminal, which means that the upper limit on its ability to support unicode is... limited, but i've done my best to make it work as well as it possibly can. this came with some significant challenges and caveats, and i'm still not sure i'm happy with where i ended up</p>
<p>the first issue is that we can't always be sure exactly how wide a string will be when rendered, where there'll be line breaks, or when the screen will scroll. some other line editors get around this by making assumptions about unicode rendering, but this leads to brittleness where the terminal can't fix its unicode handling because clients are relying on the broken behavior and clients can't fix their unicode handling because terminals are implementing broken behavior</p>
<p>to get around this, madeline (ab)uses the alternate screen buffer as a wcswidth() implementation. between renders, madeline stores how many rows below the start of the input the cursor currently is. don't worry if you don't understand everything here just yet, but the algorithm it uses to update the screen is:</p>
<p>- switch to the alternate screen and move the cursor back to the top-left corner</p>
<p>- write the rendered state, putting the escape sequence for saving the cursor position (\x1b7) in the place where the cursor will end up</p>
<p>- write the escape sequence for requesting the cursor position from the terminal (\x1b[6n)</p>
<p>- write the escape sequence for restoring the cursor position (\x1b8)</p>
<p>- write the escape sequence for requesting the cursor position from the terminal (\x1b[6n) again</p>
<p>- switch back to the normal screen</p>
<p>- scroll the cursor up X rows, where X is the stored cursor row</p>
<p>- store the cursor position after the \x1b8 to be used in the next render</p>
<p>- write Y newlines then scroll up Y rows, where Y was the cursor's position between finishing writing rendered state and restoring the cursor position</p>
<p>- re-write the rendered state, putting \x1b7 where the cursor goes and \x1b8 at the end</p>
<p>that's pretty complicated. let's break it down a bit and explain each step in detail</p>
<h3>switch to the alternate screen and move the cursor to the top-left corner</h3>
<p>the terminal provides two virtual screens, which you can swap back and forth. this is how, for example, vim can draw on the whole screen and fully disappear once you close it. it switches to the alternate screen when you start it up, and it avoids ever touching the normal buffer, which is where eg. the shell's scrollback is</p>
<p>madeline makes use of this to be able to render some stuff without messing with said scrollback. crucially, this allows us to move the cursor back up to the top of the screen, render some stuff there, and avoid having that mess with whatever history was originally up there</p>
<h3>write everything before the cursor, then \x1b7, then everything after</h3>
<p>the crucial insight here is that, now that we've moved the cursor up to the top-left corner, this is much less likely to cause the display to scroll, so we can accurately judge how tall everything is</p>
<p>to demonstrate this: imagine a five-column-wide, five-row-high screen, with characters guaranteed to all be exactly one column wide. our current state is five characters wide when rendered, and the user just pressed the "a" key, adding another column. we clear the current render, save the current position (5th row, 1st column), write all six characters out, and realize that we're still in the 1st column of the 5th row. the row hasn't changed, so clearly we don't need to clear anything but the current row when we next update the state, right? wrong. the only reason the row didn't change is that everything scrolled up a row when we got to the bottom-right corner of the screen</p>
<p>the only way to be able to actually tell how tall something is is to render it without any scrolling and measure the y position of the cursor at the end. this does mean that madeline's rendering breaks when the state is larger than the screen, which i'm tracking in this ticket:</p>
<p><a href="https://todo.sr.ht/~ecs/mrsh/14">input that's larger than the screen leads to ui bugs</a></p>
<p>the reason we store the cursor's position rather than immediately requesting it from the terminal is probably in order to deduplicate code, though this code is extremely fiddly and has been rewritten at least a dozen times, so there might also be some subtle bug it's fixing which took me five hours to find</p>
<h3>request the cursor position, then restore it to the saved position, request the position again, then switch back to the normal screen</h3>
<p>and now the reason we entered the alternate screen in the first place. i'll explain why we need the full height of the rendered state later, but we already know why we need the cursor's position: when we do the next render, we need to know how many rows to clear. once we've figured out all the information we need, we can go back to the normal screen and start actually rendering things</p>
<h3>scroll the cursor up X rows, where X is the last render's cursor height, and store this render's cursor height to be used next time</h3>
<p>i just described this lol. pretty straightforward, we're getting back to the place where we should start the current render</p>
<h3>write Y newlines then scroll up Y rows, where Y is the full height of the rendered state</h3>
<p>and now we get to the trickery. the reason we do this is to avoid any scrolling while doing the final render, which would break things for different reasons than it would break things earlier. joy!</p>
<h3>re-write the rendered state, putting \x1b7 where the cursor goes and \x1b8 at the end</h3>
<p>and this is the reason we can't have any scrolling. \x1b7 and \x1b8 save and restore the position of the cursor within the screen, which means that if the screen scrolls at all between them, the saved position won't actually line up with where the cursor wants to be</p>
<h3>anyways back to rendering and unicode</h3>
<p>the biggest upside of this approach to rendering is that we don't have to care at all about the specifics of text rendering, and we're guaranteed to get the correct results so long as the terminal supports all the features we need</p>
<p>however, this does mean that madeline is... not very portable. i've actually only ever tested it particularly strenuously in foot, and when i tried a few other terminals, it was pretty broken. the linux console doesn't implement the alternate screen, and i'm pretty sure the vt220 i have lying around got so angry at me that it started beeping loudly in indignation at the atrocities being committed</p>
<p>the only unicode-related thing i needed to implement myself was cursor movement - pressing the right arrow key should move the cursor forward one grapheme, not one byte or one codepoint. this means that, for example, if you type "i'm🏳️‍⚧️", press the left arrow key, then type a space, the space will be inserted between "i'm" and 🏳️‍⚧️, rather than going between the U+200d and the ⚧️. because hare doesn't yet have a unicode library, i wrote a small parser for a subset of the unicode data and included it along with an implementation of the unicode text segmentation algorithm</p>
<p><a href="https://www.unicode.org/reports/tr29/">uax #29, the unicode text segmentation algorithm</a></p>
<h2>ui niceties</h2>
<p>madeline provides all the readline-compatible keybindings which i frequently use, and any other keybindings that other people send patches for. i'm planning to provide support for user-configurable keybindings in the future, in addition to a vi mode, but i haven't written the code for either of those yet</p>
<p>in addition to this, madeline allows prompts dynamically generated by arbitrary hare functions (with a convenience function for fixed prompts), hints which appear after the user's input and can be completed by moving the cursor past the end of the input (with a built-in function that generates these from history, fish-style), a built-in history system which is optimized enough to be able to load my 80k-line shell history nearly instantly, and configurable autocomplete which makes use of a lexer to be able to properly handle items that require escaping</p>
<p>madeline makes light use of styles, limiting itself to only normal and dim+italic styles, the latter of which is used for hints and completions. because of how rendering works, colors are fully supported in prompts (no more needing to fuck around with \1 and \2 in order to fix multi-line inputs!)</p>
<h2>conclusion</h2>
<p>fuck um i dunno. feel free to check out imrsh, it's emersion's mrsh but with madeline instead of readline, and it's what i've been using as my daily driver for the past while. there's also rc, which is neat and i'd like to switch to it eventually but it doesn't have job control yet and i Really need job control</p>
<p><a href="https://git.sr.ht/~ecs/imrsh">imrsh</a></p>
<p><a href="https://git.sr.ht/~sircmpwn/rc">rc</a></p>
<p>ok bye</p>
			</div>
		</content>
	</entry>
	<entry>
		<title>the world's awkiest irc bot</title>
		<link rel="alternate" href="gemini://ecs.d2evs.net/posts/2024-02-07-awkbot.gmi" />
		<id>gemini://ecs.d2evs.net/posts/2024-02-07-awkbot.gmi</id>
		<updated>2024-02-07T12:00:00Z</updated>
		<content type="xhtml">
			<div xmlns="http://www.w3.org/1999/xhtml">
			<p>if you've hung out around me long enough, you'll know that i have a bit of an unhealthy love of awk. some of that is because it's just a nice language, but a lot of it can be traced back to this one exchange i had on irc a few years ago:</p>
<pre>
2021-09-22 22:02:51	@etj	hm
2021-09-22 22:02:54	@etj	acctually
2021-09-22 22:03:06	@etj	i think i know the right language for writing microbots in
2021-09-22 22:03:10	@ecs	do tell
2021-09-22 22:03:12	@etj	awk.
</pre>
<h2>but before i can tell you that story, i have to tell you *this* story</h2>
<p>on 2020-04-20, i decided to write an irc bot to display search results from duckduckgo. i'm not really sure why i wanted to do that, but i had this to say about it at the time</p>
<pre>
2020-04-20 16:50:54	@ecs	let's write a *useful* irc bot
2020-04-20 16:51:13	@ecs	something that'll interpret commands like:
2020-04-20 16:51:22	@ecs	!ddg this should show the top search result
</pre>
<p>for reasons that were funny at the time, she ended up being named "qta", and she soon grew to a healthy 1500loc. she could set reminders, forecast the weather, produce fascinating insights with a markov chain dynamically trained on messages sent in the channel, control an mpd server which i'm pretty sure only ever played metallica because look i don't know, and a bunch of other small things i ended up wanting her to do</p>
<p>on 2020-10-01, inspiration struck, and the first¹ of my jsbot clones was born</p>
<p>¹: technically second, but the scheme bot i wrote on a phone in the middle of a 6-week hiking trip and never actually used doesn't really count</p>
<p><a href="https://drewdevault.com/2021/03/29/The-worlds-dumbest-IRC-bot.html">Drew's blog post about jsbot (cw some kinda iffy language)</a></p>
<pre>
2020-10-01 00:39:57	@ecs	it would be nice to have a good™ embeddable scripting language
2020-10-01 00:40:36	@ecs	https://github.com/d5/tengo maybe
2020-10-01 00:41:17	@etj	!tengo when
2020-10-01 00:42:22	@ecs	https://github.com/traefik/yaegi huh
[snip]
2020-10-01 01:43:22	@ecs	!go fmt.Println("hello, world!")
2020-10-01 01:43:23	quaternia-test	=&gt; hello, world!
2020-10-01 01:43:27	@ecs	\o/
</pre>
<p>we developed some infrastructure for writing little irc bots inline in go, but it never worked particularly well. go is a kinda verbose language, and that's a problem when your code needs to fit within a 512-byte irc message. we also hacked together persistent data by manually appending go code to a file in /etc which got sourced on boot, with the idea if you wanted to eg. increment a number, you'd run `foo++` and then append `foo++` to /etc/quaternia/init.go</p>
<p>this worked about as well as you'd expect it to</p>
<h2>it got ~worse~ better</h2>
<p>a year later, etj was complaining about just how bad the !go persistence mechanism was. after having messed with it manually in order to remove a macro that expanded "lol" to "lingerie of love" (don't ask), they were thinking of just removing it entirely ("i really think that the scriptable bot that you write bots in is a bad idea" "or at least langbot shouldn't be the final destination for bots"), and i just thought that "it would be nice to switch to a non-go language", because "livecoding one line at a time over im protocols is the best software development method". etj came up with a half-serious proposal of using awk, and i was "not actually as horrified by that as i feel like i should be", so i got to work implementing it</p>
<p>after a few hours of hacking and a brief break to determine qta's birthday (valentine's day in 1970, apparently):</p>
<pre>
2021-09-23 03:00:47	@ecs	let's see if this works
2021-09-23 03:01:12	@ecs	.awk add test /^\.awkping$/ { print("pong") }
2021-09-23 03:01:12	+qta	success
2021-09-23 03:01:16	@ecs	.awkping
2021-09-23 03:01:16	+qta	pong
</pre>
<p>(note: the syntax for adding snippets has changed since these logs)</p>
<p>initially, i was worried about persistence</p>
<pre>
2021-09-23 03:07:25	@ecs	one caveat about this is that
2021-09-23 03:08:03	@ecs	.awk add foo BEGIN { foo = 0 } /bar/ { foo++; print foo; }
2021-09-23 03:08:03	+qta	success
2021-09-23 03:08:07	@ecs	bar
2021-09-23 03:08:07	+qta	1
2021-09-23 03:08:08	@ecs	bar
2021-09-23 03:08:09	+qta	1
2021-09-23 03:08:24	@ecs	it doesn't retain state
</pre>
<p>but i've since come around. both gobot and jsbot have issues with persistence being optional: it's possible to write programs which look like they work, but which lose data when the bot is rebooted. because awkbot's awk context isn't kept around between messages, you're forced to use the postgres database it provides bindings for if you want to keep any data around at all, and rebooting the bot is guaranteed to never break anything</p>
<p>once i'd written the initial version of awkbot, i slowly started making it more and more powerful in order to be able to rewrite more and more of the original bot in awk. i even managed to rewrite half of the bot itself in awk - the interface for adding, listing, and removing awk snippets was originally written in go, but once i added a function to run arbitrary sql queries from awk², i was able to delete those 127 lines of code</p>
<p>²: not a trivial task. goawk has ffi, but go functions that're callable from awk can only take in and return primitive types and strings, so i had to do some creative escaping in order to pull the argument and result arrays across that boundary</p>
<p>one substantial improvement i managed to make over jsbot is the ability to execute code at an arbitrary time, rather than being limited to replying to messages. you can call `at(date, cmd)` to add `cmd` to a table, marked with the timestamp `date`. every 10 seconds, the go code executes the snippet named "__ontick__", which looks through that table and executes any code whose timestamp is in the past. another hacked-together system sits on top of this for implementing repeating commands, allowing you to, for example, print "hi" every 10 minutes by running `.cron now "in 10m" '{ print "hi" }'`. don't ask how that works, you don't want to know</p>
<p>somewhere along the line we decided that it'd be a good idea to give awkbot an http client, so now there's some awk code to print out url titles, interface with the schedule api for my local public transit agency, check the weather, and a bunch more stuff. she also knows how to parse json, xml, and html, though i'm thinking of trying to rewrite some of that in awk</p>
<p>the old bot weighed in at around 2.2kloc at her peak, and i finished rewriting the last part (my rss reader³) in awk on 2022-12-10. the new bot consists of 156 snippets of awk code totalling 29,981 characters, and 428 lines (9.155 characters) of go code. she's grown a brainfuck interpreter, an implementation of the geohashing algorithm, half of a cube timer, and dozens of other things i don't have time to list</p>
<p>³: said rss reader once got me a politely worded email from someone whose blog i followed asking me to please stop hammering her rss feed once every 10 seconds. i fixed it, now qta only hammers rss feeds once every 10 minutes)</p>
<p>every so often i come back and hack on her some more, but for the most part qta's just become part of my life at this point. sometimes i decide to add another organizational tool to her, and on occasion some of them even get a bit of use. i recently sorted out some race conditions in the output-channel management⁴, because a youtube rss feed outage was causing her to yell at me really really loudly and incessantly in dms. i also just rewrote the geocoding, reverse geocoding, shlexing, and human-friendly datetime parsing bits in awk, which shaved off around 80 lines of go code and got rid of quite a few dependencies</p>
<p>⁴: the go code provides a setchan() function, which controls the channel that data is printed to, but because user code used to run inside a `print eval(...)`, that data wasn't actually sent to the pseudo-stdout until it finished evaluating, which meant that if you did something along the lines of `print "hi"; setchan("#a")`, the "hi" would be sent to #a. in order to solve this i added a "passthrough" parameter to eval(), which tells it to immediately write everything to the channel in addition to buffering it up to be returned as a string. then i discovered that it still didn't work because for some reason i was having goawk print things into an io.Pipe which a different goroutine was reading from, rather than just having an io.Writer which writes messages to irc. anyways it works now, though i didn't quite manage to fix it before youtube fixed their feeds</p>
<p>there's no actual moral to this story. you probably shouldn't ever use this code, but the source code is linked below if you're curious. bye!</p>
<p><a href="https://git.sr.ht/~ecs/awkbot"></a></p>
<h3>errata</h3>
<ul>
<li>on 2024-03-27, etj rewrote json parsing on awk</li>
<li>on 2024-04-03, i rewrote the tick code with repeating commands as the primitive exposed by __ontick__ and reminders implemented as a separate snippet that's run every 10s by __ontick__, which allowed me to get rid of the hacks behind `.cron`</li>
</ul>
			</div>
		</content>
	</entry>
	<entry>
		<title>how to write good awk</title>
		<link rel="alternate" href="gemini://ecs.d2evs.net/posts/2023-11-24-awk.gmi" />
		<id>gemini://ecs.d2evs.net/posts/2023-11-24-awk.gmi</id>
		<updated>2023-11-24T12:00:00Z</updated>
		<content type="xhtml">
			<div xmlns="http://www.w3.org/1999/xhtml">
			<p>awk is one of my favorite languages. it's not a good fit for all (or even most) tasks, but when it does work, it works *really* well</p>
<p>however, good awk code looks pretty different from good code in any other language. awk is by nature an event-based language, and idiomatic awk tries to keep as much of its logic in the action conditions as it can. in particular, try to avoid just putting a bunch of control flow into a single big action with an empty pattern. for example, this:</p>
<pre>
{
	if ($0 ~ /hi/) {
		print "hii"
	} else {
		print $0
	}
}
</pre>
<p>would be better as</p>
<pre>
/hi/ { print "hii"; next }
1
</pre>
<p>(the `1` here is a fallback which triggers unconditionally and uses the default action of printing $0. this would be clearer with an actual `true` literal, and arguably either `//` or `{ print }` are even better)</p>
<p>simple actions should go entirely on one line, and more complex actions should have one statement per line</p>
<p>you can and should take advantage of the fact that variables default to 0 in order to simplify boolean states. as an example, here's a simple gemtext parser:</p>
<pre>
/^```/ { preformatted = !preformatted; next }
preformatted { display_preformatted($0); next }

BEGIN { FS = "[ \t]" }
/^=&gt;/ {
	url = $2;
	$1 = "";
	$2 = "";
	sub(/^ */, "");
	display_url(url, $0);
	next;
}

{ display_text($0) }
</pre>
<p>sometimes you don't want to end patterns with `next`, as in this code which wraps indented lines in a &lt;pre&gt;:</p>
<pre>
/^\t/ &amp;&amp; !pre { print "&lt;pre&gt;"; pre = 1 }
/^\t/ { print substr($0, 2); next }
pre { print "&lt;/pre&gt;"; pre = 0 }
1
</pre>
<p>awk is also an old language, with its share of warts. in particular, while it doesn't have local variables, you can emulate them by adding extra arguments to a function and not passing them in:</p>
<pre>
function printn(x, i) { for (i = 1; i &lt;= x; i += 1) print $i }
{ i = 5; printn(2); print i }
</pre>
<p>you should make use of this for all local variables, even when it's not strictly necessary</p>
<p>i don't think i've written a single good conclusion to any of the posts i've written here</p>
			</div>
		</content>
	</entry>
	<entry>
		<title>pronouns and language</title>
		<link rel="alternate" href="gemini://ecs.d2evs.net/posts/2023-11-21-pronouns.gmi" />
		<id>gemini://ecs.d2evs.net/posts/2023-11-21-pronouns.gmi</id>
		<updated>2023-11-21T12:00:00Z</updated>
		<content type="xhtml">
			<div xmlns="http://www.w3.org/1999/xhtml">
			<p>a few months ago, i wanted to talk about a friend of mine while they weren't around. only problem was, that friend uses they/them pronouns, and i was talking in hebrew</p>
<p>now, hebrew has pretty ubiquitous grammatical gender: adjective and verb conjugation is gendered¹ in addition to pronouns like in english, and it hasn't historically had any neutral gender. there does exist a system for it, but it's not nearly as simple as spanish's "just use 'e' rather than 'o'/'a'", and it's not in widespread use. more concretely, i only know hebrew grammar implicitly by growing up with it, so i didn't have a great time trying to turn this page into a shift in my language use</p>
<p><a href="https://www.nonbinaryhebrew.com/grammar-systematics">the nonbinary hebrew project's grammar and systematics page</a></p>
<p>¹: in most contexts; eg. first-person future isn't gendered, and third-person feminine plural is rarely used</p>
<p>now, i happen to know that, given that the only two feasible options in this context were a masc grammatical gender and a fem one, my friend would've preferred the masc one. the person i was talking with didn't know this, so there was some awkwardness as we figured things out. that conversation moved on, but the interaction stuck with me</p>
<p>obviously he/him, she/her, and they/them aren't the only three pronouns that people use - what about more obscure neopronouns? they may not have an intrinsic meaning outside of the specific phonemes that're used within an english context</p>
<p>of course, the obvious answer is "if you're not sure, ask the person you're wanting to talk about how they'd like to be referred to in that language", but tbh that feels a bit unsatisfying. what if they're not around to be asked? what if they're not familiar with the intricacies of that language's gender system, and thus can't come up with a set of linguistics that matches their identity?</p>
<p>fae/faer, as an example, is a mutation of the english word faerie, which is itself a deliberately archaic spelling of "fairy". the hebrew word for fairy is "fe'a", but would some merger of that with either "hi" (she) or "hu" (he) capture the nuance of "fae" relative to he/she/they? "fae" rhymes with "they" in english, which gives it an alignment with gender-neutrality. so "fe", rhyming with "ate" from the nonbinary hebrew project, might make sense?</p>
<p>getting a guess for a translation of one pronoun out of an entire grammatical gender conjugation table that would need to be created took me, someone who already speaks hebrew, probably 10 minutes - is it really fair to ask every person who uses neopronouns to do that for you, for each non-english language you want to discuss them in?</p>
<p>we could try to build a canonical translation of most neopronouns into most languages, but that's a ton of work and learning new gender conjugations on the fly is less feasible for a language like hebrew than it is for english. maybe using some standard nonbinary conjugations and only switching out pronouns is reasonable?</p>
<p>but even that is only helpful in the best-case scenario, where we're okay with modifying the language to accommodate a gender it couldn't previously express. if we return to the example i started with, which presumes that the language can't accommodate the gender, we can see a different perspective on the problem: the conversion from gender identity to pronoun is inherently lossy, and it can lose different information in different languages</p>
<p>now, there are definitely people for whom neither the masc gender nor the fem one would be preferable - one workaround in this case that i'm familiar with is to alternate between them - but in plenty of cases, there is a preference there which could lead to an acceptable binary gendering if context requires it</p>
<p>i don't think there's anything that can particularly be done to improve this in practice - i'm not gonna go out and ask people to start saying "hi i'm so-and-so, i'm a masc-ish enby who uses they/them pronouns in english but if you're stuck in a language that needs to put me into a binary gender then i'd prefer masc rather than fem" because that'd be really really weird and overly verbose and the specifics of someone else's gender identity are frankly none of your business and it'd also give people an excuse to be like "oh well i don't feel like using singular they, he basically just gave me an excuse to misgender him, so doing that is probably fine"</p>
<p>so where does that leave us? i dunno. but more awareness of the intersection of enbies and non-english languages definitely can't hurt, i guess</p>
			</div>
		</content>
	</entry>
	<entry>
		<title>the readability of soft-wrapped text</title>
		<link rel="alternate" href="gemini://ecs.d2evs.net/posts/2023-11-20-wrapping.gmi" />
		<id>gemini://ecs.d2evs.net/posts/2023-11-20-wrapping.gmi</id>
		<updated>2023-11-20T12:00:00Z</updated>
		<content type="xhtml">
			<div xmlns="http://www.w3.org/1999/xhtml">
			<p>i think there's something important that's usually missed when discussing soft-wrapped text: text width</p>
<p>this might just be a me thing, but i find it really unpleasant to read anything long that's all that much wider than the standard 80/72 columns that hard-wrapped text is usually at. *something* between the person writing the text and the medium where it's being displayed should do this width-limiting, and when all the tooling for viewing text is built on the assumption that that'll happen as the text is written, it means that soft-wrapped text is really unpleasant to read</p>
<p>now, that doesn't mean it's impossible to get the benefits of soft-wrapping without breaking that backwards compatibility: format=flowed seems like a really good idea, and it'd be nice for it to catch on more</p>
<p>however, one thing that surprised me when i did an (admittedly not very thorough) look through gemini clients the other day in search of something more pleasant to use than gmnlm: none (neither) of them addressed this problem. and i do suspect this is a problem that other people have as well:</p>
<p><a href="https://www.makeworld.space/2023/08/bye_gemini.html">makeworld's "bye, gemini" post</a></p>
<p>among various other things, makeworld mentions that</p>
<blockquote>reading long form text in monospace really sucks, and of course that's all Amfora can do, operating in the terminal</blockquote>
<p>obviously i'm not makeworld, and i can't speak for them, but i pretty much exclusively read monospaced fonts and i don't find them to be particularly unpleasant for reading long-form text. amfora, however, doesn't wrap things any narrower than the window it's given</p>
<p>(update 2024-01-25: i misremembered this, amfora does wrap text, the reason i didn't like it was that it really wanted to make a ~/Downloads/. this probably changes the conclusion you should draw from this post, but i don't feel like rewriting it)</p>
<p>i very deliberately implemented this wrapping in hxj, and when i did, it instantly made reading the quote i was typing an order of magnitude more pleasant</p>
<p><a href="https://git.d2evs.net/~ecs/hxj">hxj, a cute lil tui typing test i wrote</a></p>
<p>another thing i decided to do is center the text, which i think is another underrated and good idea - it may just be that it's associated in my mind with things like reader mode, but i find that fairly narrow, centered text with a pretty large font size is a very straightforward way to significantly improve the readability of long-form text</p>
<p>i threw together a pretty hacky fork of adnano's astronaut, which limits text to 80 columns and centers it, and i've been finding it much more pleasant than gmnlm, which i used earlier</p>
<p><a href="https://git.d2evs.net/~ecs/astronaut">my astronaut fork, with centered and narrower text, along with a other minor improvements i found myself wanting</a></p>
			</div>
		</content>
	</entry>
	<entry>
		<title>on difficulty in video games</title>
		<link rel="alternate" href="gemini://ecs.d2evs.net/posts/2023-11-19-difficulty.gmi" />
		<id>gemini://ecs.d2evs.net/posts/2023-11-19-difficulty.gmi</id>
		<updated>2023-11-19T12:00:00Z</updated>
		<content type="xhtml">
			<div xmlns="http://www.w3.org/1999/xhtml">
			<p>first, a couple disclaimers: i'm not talking about the shitty social dynamics that often crop up online around difficult video games here. they're interesting and important and worth discussing, but they also aren't the focus of this post. in addition, most of my experience with difficult games comes from modded celeste, and some parts of this don't generalize very far beyond that. in particular, celeste is mainly about mechanical difficulty with relatively little emphasis on reaction time, which is very different from eg. puzzle or fighting games</p>
<h2>rage games and the difference between difficulty and frustration</h2>
<p>difficulty is often construed as being intrinsically linked to frustration, but i think that this analysis misses the mark in a number of ways. difficulty is definitely correlated with frustration - there's some frustration inherent to spending tens of hours grinding something out - but there's also a tremendous amount that can be done to reduce this frustration if the game designer chooses to do so</p>
<p>this particularly stuck out to me as i was playing through syobon action recently. when analyzed purely in terms of frame windows, there's nothing in syobon action which is particularly difficult relative to what i usually do in celeste - i'd probably call it yellow expert on the standard celeste difficulty scale - but everything seems optimized to cause as much frustration as possible. the input handling and physics are extremely janky in ways that are (presumably) deterministic and can technically be worked around but often leave you feeling like the game is at fault for your deaths, most of the obstacles are either invisible or otherwise unavoidable without prior knowledge, and the game makes it rather difficult to understand why you died when you die</p>
<p>another factor which can affect frustration is, roughly, separation between attempts. this is a pretty broad concept, and encompasses things like respawn times, music changes (a soundtrack that plays continuously between attempts can make a huge difference), maybe a boring bit of gameplay at the beginning - anything that breaks flow between attempts. for example, the first room of the 6d badeline fight (as well as of the badeline fight in everlasting farewell) is much more annoying because the music restarts every time you die</p>
<p>while this could be considered a subset of attempt separation from a theoretical perspective, ease of practicing later parts of a gameplay segment also plays an important role and has to be managed separately from a design perspective. there are two ways (that i can think of) to affect this:</p>
<p>the first is to work some sort of local nonlinearity into your level design: rather than doing A, then B, then C, then D in that specific order in order to win, allow the player to do A, B, C, and D in any order. vanilla celeste does this a fair bit: a-07 in 4a and 03 in 2b are two particularly clear-cut examples, though there are plenty more that do this to a lesser extent. because the order of the gameplay bits isn't fixed, you're free to vary it in order to practice any of them more easily</p>
<p><a href="https://berrycamp.github.io/celeste/ridge/a/a-07">4a a-07</a></p>
<p><a href="https://berrycamp.github.io/celeste/site/b/03">2b 03</a></p>
<p>however, making that work by itself, especially in higher-difficult contexts which would require a tremendous number of rearrangeable segments in order to still be practiceable, is a pretty heavy restriction on level designers, which is why the second approach is much more common: some sort of practice mode. in celeste this takes the form of savestates from the speedrun tool mod - note that its use when practicing long sections for goldens (to respawn you at the start of the section, farther back than you would normally be sent) and when practicing short sections for clears (to respawn you in the middle of a room, further forward than you would normally be sent) are both for this purpose</p>
<h2>practice mode</h2>
<p>at a high enough level of difficulty, a good practice mode becomes an integral part of the game</p>
<p>consider a (perhaps overly) simple model of a "difficult" game: a sequence of coin tosses, whose probability of success is 0% the first time and increases by 1% each time you try it until it reaches a maximum of 90%. basically, you "learn" each coin toss as you go through it more, but there's a limit on how well you can learn it. i simulated the game using two strategies: one of them tries each coin toss individually until it reaches the maximum of 90% consistency, then does full attempts until it succeeds, the other just starts from the beginning each time. how do the two compare? if we pretend that each toss takes one second:</p>
<ul>
<li>5 tosses: 1st 7.5m, 2nd 12m</li>
<li>10 tosses: 1st 15m, 2nd 1h</li>
<li>20 tosses: 1st 30m, 2nd 6h30m</li>
<li>30 tosses: 1st 48m, 2nd 25h</li>
<li>40 tosses: 1st 1h10m, 2nd 80h</li>
<li>50 tosses: 1st 1h45m, 2nd 240h</li>
<li>60 tosses: 1st 3h, 2nd 700h</li>
<li>70 tosses: 1st 6h, 2nd 2000h</li>
<li>80 tosses: 1st 15h, 2nd 5800h</li>
<li>90 tosses: 1st 38h, 2nd 17000h</li>
</ul>
<p><a href="https://p.d2evs.net/1582141222.ha">the source code for the simulation. no warranty, ymmv</a></p>
<p>obviously this isn't a perfect simulation either of difficult games or of optimal use of a practice mode, but it does paint a clear picture: as average clear times without practice mode spiral exponentially out of control, practice mode keep things surprisingly close to linear for a while longer</p>
<p>not only that, but the time spent playing with a practice mode is much more enjoyable as well. fundamentally, when playing a difficult game, you're actually doing three tasks:</p>
<ul>
<li>figuring out what you're supposed to do, then</li>
<li>building muscle memory for it, then</li>
<li>executing on that muscle memory</li>
</ul>
<p>practice mode allow you to separate those three tasks. once you know what you're supposed to do and you're comfortable with each part in isolation, longer attempts from the beginning build an enjoyable flow state, but when you're trying to figure out what's going on and what the mapper is asking you to do, that time at the beginning of the room feels wasted, making you frustrated instead of relaxed. muscle memory benefits from intensive repetition in a way that's not possible without just sticking a savestate before a difficult section of gameplay and drilling it over and over again until it's consistent</p>
<p>not all of these factors come into play in all contexts: putting map-specific things in muscle memory (as opposed to just generic tech like extended hypers or the various types of ultras) only becomes relevant at or above gm, and some maps are designed to be readable and mitigate the "figuring out what you're supposed to do" part. practice mode can also worsen the experience in some ways that aren't captured by this framing - in particular, knowing ahead of time how the room is supposed to play out can break some surprises. 9d has two different rooms which are much less fun if you use savestates, and one of them is hard enough that it practically requires them</p>
<p>celeste clearly wasn't originally designed with a practice mode in mind, and its savestate system grew organically in response to speedrunners' (and mod players') desires. it's quite nice as it is today, but i'd be curious to see how a game somewhat similar to high-level modded celeste could look if it was designed from the start to be that way. some ideas i've had in the past have been:</p>
<ul>
<li>being able to keep multiple savestates around. this is probably pretty easy to do in celeste today, it'd just require thinking through how the ui should work. this would prevent you from needing to completely redo all the work it took to create your state if you accidentally make a bad state</li>
<li>being able to easily place a savestate at any point in a given room's "happy path", for some definition of "happy path", without going from the beginning of the room through to the part you want a state at. for most maps, the best you could do is something like persistent (cross-map-entry) savestates that you can load at any point, but for a certain genre of map you could have a canonical tas which you can scrub through and load from any point. it'd also be nice to have a fully put-together interface for looking at this tas - sj's ivory has a pretty good system for this, but you need to manually control the camera rather than it automatically following madeline with the same logic as it uses for normal gameplay</li>
<li>loading the player in a bit before the state and automatically playing back the correct movement up through the state and letting you take over there</li>
</ul>
<p>one game design idea i've been toying with for the past while is a rhythm platformer: basically, celeste's physics, but inputs are all timed to a music track that restarts when you die, and you need to make it through a whole song without dying. i think this could work quite well, but it'd need a really really good practice mode in order to be feasible at all</p>
<p>i don't really have a good conclusion here ok bye</p>
			</div>
		</content>
	</entry>
	<entry>
		<title>in which i talk about my bike for a bit</title>
		<link rel="alternate" href="gemini://ecs.d2evs.net/posts/2023-11-18-bike.gmi" />
		<id>gemini://ecs.d2evs.net/posts/2023-11-18-bike.gmi</id>
		<updated>2023-11-18T12:00:00Z</updated>
		<content type="xhtml">
			<div xmlns="http://www.w3.org/1999/xhtml">
			<p>my bike broke down a few weeks ago¹, and for various reasons, i decided to replace the whole bike instead of just the drivetrain. i found an old 3-speed rudge sports for pretty cheap on craigslist, and i've been quite happy with it so far</p>
<p>¹: okay technically it's still rideable by some definition of that word, and also it's been in roughly the same state for a few years now, but a few weeks ago i decided to actually do something about it</p>
<p>that being said, there were two things i wanted to improve: the lights and the brakes</p>
<h2>lights</h2>
<p>the bike had a working dynamo hub in the front wheel, which i measured as producing ~6v ac at ~2w. this was with spinning the wheel manually, since i didn't bother to hook up the multimeter in a stable enough way to check while riding it. this power went through a switch on the front headlight and into two incandescent (!) bulbs, one in the front and one in the back, wired in parallel</p>
<p>needless to say, this is not very bright. so, i bought some cheap power leds and went over to a friend's house (they have a bunch of tools and stuff), and we</p>
<ul>
<li>wired together a full-bridge rectifier out of four diodes they had lying around</li>
<li>ran some tests on the leds to try to figure out how much power they could take before they blew. the answer was "far more than the dynamo can provide" (the test led started to overheat at around 6v and 1a, but still worked fine afterward)</li>
<li>hooked the rectifier up between the dynamo and the switch</li>
<li>gave me a headache cause the leds are really bright and the rectifier doesn't produce very good dc at the speed my friend was able to spin the wheel at by hand so it flickered at the worst frequency. we tried adding a capacitor to the circuit going from power to ground, but it didn't work for some reason? idk why. luckily it didn't do this at reasonable biking speeds</li>
<li>spent like far too long trying to come up with clever ways to mount them, then got tired, gave up, and just scotch taped them to the places where the incandescent bulbs used to be. they're still in enclosures, so the worst case scenario is that they fall out and become worse at producing light until i fix them, but i'll probably 3d print something to go in the bulb sockets eventually</li>
</ul>
<p>i then biked home at like 1am with no other lights, and it worked great! the route between our houses isn't very well lit², so it's probably the worst-case scenario for what this bike will see, and i still felt like it was producing enough light. i couldn't see how well the rear light was doing, but next time i'm biking with someone else at night i'll ask them (update 2024-01-25: yeah it's good)</p>
<p>²: there's a multi-use path that goes pretty much directly from my house to theirs, which is really nice, but also it has almost no lighting, which is less nice</p>
<p>there's something weirdly satisfying about knowing that the light you're using to see is coming directly from you. it's also really nice not to need to worry about charging my lights and bringing them with me when i bike places at night. the lamps themselves also look really nice - the headlight in particular is really big and gives the bike (even more of) a vintage vibe</p>
<p>(this line could be an image link if you poke me and ask for a picture!)</p>
<p>one thing i might do in the future is wire up a usb charging port on my bike. it'd be completely pointless, but i also think it'd be hilarious, and the switch already has three spots so surely it wouldn't be too hard to configure one of them to power the usb port</p>
<p>another thing i'd like to do is take another shot at wiring in a capacitor. this'd help with the flickering at slow speeds, and with a big enough capacitor it can also make the light work for a minute or two when i'm stopped. the law here doesn't require that, and i have two rear reflectors (one built into the bike, and another i screwed onto my rack) so i should still be visible when i'm stopped, but it'd still be nice. plus, i find it really annoying when other bikes have flashing lights (for getting attention or smth ig, not because the person who designed the lights was dealing with a grumpy ac power source and didn't feel like converting it to dc properly) and even though in practice this isn't a problem once i'm up to speed, it still feels a bit rude to flash my light at people as i'm building up that speed</p>
<p>one thing i definitely don't want to do is increase the brightness of the light. modern bike (and car) headlights are way, way too bright for urban use, and i'm routinely blinded whenever they pass by me going the opposite direction. i don't want to be part of that problem</p>
<h2>brakes</h2>
<p>a few hours after i got the bike, it started raining. nothing particularly heavy, but enough that i realized something when i went out to the hardware store to try to get some stuff: rim brakes on steel rims do Not work in the rain. i probably could've slowed down faster by just holding my coat open to increase air resistance</p>
<p>but apparently, using leather pads negates this. luckily i haven't needed to bike while it was rainy since then, but i installed some fibrax raincheaters last night as i was installing the led lights, and i'll update this post once i've had the opportunity to test them</p>
<p>update 2024-01-25: they're not great tbh. like, definitely much better than the old brakes, but still not ideal, even in the dry. i recently reconnected with an old friend who happens to be into old 3-speeds. he suggested replacing the caliper brakes with center-pull ones, and i might do that asp in the future</p>
<p>one other thing i'd like to do at some point in the future is lubricate or potentially replace the brake cables. it currently takes a nontrivial amount of force to apply the brakes, and it'd be nice if that wasn't the case. i'll probably try lubing them when i get around to relubing the rest of the bike, then look into replacing them if that doesn't work</p>
<h2>conclusions</h2>
<p>i really like this bike! the upright sitting position is much more comfortable than my old bike⁴, and the shifting experience is much nicer. while i'm sure you can get a derailleur adjusted properly, i've never succeeded. in contrast, despite the hub being slightly out of adjustment when i got the bike, i was able to easily fix it using the owner's manual that the previous owner was kind enough to xerox for me</p>
<p><a href="https://ecs.d2evs.net/raleigh_manual.txt">a manual ocr of the owner's manual in question</a></p>
<p>⁴: my wrists didn't enjoy having all my weight on them, and i'd often ride with the tips of my fingers barely touching the handlebars in order to be able to sit up straighter, which was also uncomfortable but in a different way</p>
<p>the shifter itself is really satisfying to use, much more than the (admittedly broken) twist-shifters on my old bike, and 3 speeds is plenty for my needs - frankly, it's better than the 21 speeds i had on my old bike, since all the extra gears just made it harder to find the right one</p>
<p>the internal hub gear also feels much more reliable than any derailleur i've used - both (presumably) in terms of maintenance and in terms of how quickly and comfortably shifts happen. my old bike sometimes (especially under heavy load, like when going up a hill or accelerating from a stop) took a few seconds or more to find the gear it was looking for, clicking the whole time and ending with a loud snap! as the chain jumps into place. in contrast, the hub gear has instantly, immediately, and near-silently performed every shift i've asked of it. the one caveat is that it sometimes doesn't succeed at shifting downward when stopped, though i can work around that by spinning the pedals backwards a bit, and it's still leagues better than a derailleur in that context</p>
<p>the bike also has an integrated steering lock, which seems neat. the previous owner didn't have a key, but i might try to reverse engineer one (if you know how locks work and have thoughts on how to do this, lemme know!). i'm not sure i'd necessarily trust it on its own, but it still seems like a neat thing to try to make work. i might also try to see if i can make it lock the front wheel as well, and maybe somehow integrate it with a chain. i currently have a u-lock clipped onto the seatpost, but it makes mounting and dismounting more annoying since i have a step-through frame - it'd be nice to replace it with a lock that's easier to carry around. alternatively, if you have suggestions for how to mount a u-lock on a 1970 rudge sports with a 19.5in ladies frame, lemme know :3</p>
<p>update 2024-01-25: found a better position for the u-lock, with the clip on the seatpost facing out and the lock folded as far back as it can go. it's not ideal, but it's good enough. the right way to get a key requires taking apart a bunch of the front fork, there's a number etched on the lock which can be given to a locksmith in order to get a key cut</p>
			</div>
		</content>
	</entry>
	<entry>
		<title>"a practical guide to evil" was a good web serial</title>
		<link rel="alternate" href="gemini://ecs.d2evs.net/posts/2022-11-11-pgte.gmi" />
		<id>gemini://ecs.d2evs.net/posts/2022-11-11-pgte.gmi</id>
		<updated>2022-11-11T12:00:00Z</updated>
		<content type="xhtml">
			<div xmlns="http://www.w3.org/1999/xhtml">
			<p><a href="https://practicalguidetoevil.wordpress.com">the wordpress site of the serial in question, may be down by the time you're reading this</a></p>
<p>a friend of mine introduced me to APGtE in early 2021, around a year before it was finished. it took me a while to catch up to what had been written as of then, but once i did it became a fixture of my monday and thursday nights. i missed ErraticErrata's writing over the last half a year, and i'm happy that pale lights started up recently.</p>
<p>as of now, the practical guide is still publicly available for free on the wordpress site linked above, but ErraticErrata announced a publishing deal with a sketchy-ass mobile app called yonder, where it will be sold on a per-chapter basis as a sequence of microtransactions. the serial would very much benefit from an editor, and i would love it if it were eventually published as either a normal ebook or a traditional paper book, but i really don't like this move as a whole.</p>
<p>this is excabarated by the fact that it seems that the wordpress site will be shut off on 2022-12-31, likely well before the series is done releasing on yonder. i also don't believe that there are any official plans to archive the comments from wordpress, which only makes things worse.</p>
<p>i wrote a small script a while back that scrapes the practical guide and builds pdfs of it for my personal use, but up until this point i've respected ErraticErrata's wishes and haven't distributed them publicly. however, with the move to yonder, i don't think it's fair to let these books disappear from the public internet. as such:</p>
<p><a href="https://p.d2evs.net/833604777.txt">the shell script i used to generate my pdfs</a></p>
<p>update 2024-01-25: i think ErraticErrata came to some sort of agreement with someone, the wordpress site is still up. there used to be a link to a tarball here as well, but i've removed it. i've kept the script around, but if you'd like me to remove it, shoot me an email or something idk</p>
<p>be aware that the script in question is an awful pile of hacks, ymmv. it depends on hq, weasyprint, and curl</p>
<p><a href="https://git.d2evs.net/~ecs/hq">hq, which is like jq for html</a></p>
<p>as of now, there don't seem to be any plans to do something similar for pale lights, and i haven't bothered to make a pdf for it since i'm reading it as it comes out, but i'll update this post if the situation there changes</p>
			</div>
		</content>
	</entry>
	<entry>
		<title>🏳️‍⚧️</title>
		<link rel="alternate" href="gemini://ecs.d2evs.net/posts/2022-08-07-U+1F3F3-U+FE0F-U+200D-U+26A7-U+FE0F.gmi" />
		<id>gemini://ecs.d2evs.net/posts/2022-08-07-U+1F3F3-U+FE0F-U+200D-U+26A7-U+FE0F.gmi</id>
		<updated>2022-08-07T12:00:00Z</updated>
		<content type="xhtml">
			<div xmlns="http://www.w3.org/1999/xhtml">
			<p>that is all</p>
			</div>
		</content>
	</entry>
</feed>