summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorsoaos <soaos@soaos.dev>2025-11-21 21:14:12 -0500
committersoaos <soaos@soaos.dev>2025-11-21 21:14:12 -0500
commitba76e77d935998e4b128053dcc61d2ed4884cdda (patch)
tree5464dccd475404b509f048f4525193a8ff36a715
parent6e3a7252608197b6571a56c9b07be09f254e8bae (diff)
zola migration
-rw-r--r--.gitignore1
-rw-r--r--.kateproject.build23
-rw-r--r--.radicle/native.yaml2
-rw-r--r--assets/UnifontExMono.woffbin3345136 -> 0 bytes
-rw-r--r--assets/badges/html.gifbin8500 -> 0 bytes
-rw-r--r--assets/badges/javascript.gifbin1694 -> 0 bytes
-rw-r--r--assets/badges/neovim.gifbin695 -> 0 bytes
-rw-r--r--assets/unifont.woff2bin1605860 -> 0 bytes
-rw-r--r--assets/unifont_upper.woff2bin283288 -> 0 bytes
-rw-r--r--blog/rockbox_stats/index.html560
-rw-r--r--blog/terminal_renderer_mkii/index.html189
-rw-r--r--config.toml18
-rw-r--r--content/_index.md39
-rw-r--r--content/blog/_index.md6
-rw-r--r--content/blog/blog.css (renamed from blog/blog.css)0
-rw-r--r--content/blog/rockbox_stats/index.md547
-rw-r--r--content/blog/rockbox_stats/log-setting.bmp (renamed from blog/rockbox_stats/log-setting.bmp)bin153666 -> 153666 bytes
-rw-r--r--content/blog/rockbox_stats/playback-settings.bmp (renamed from blog/rockbox_stats/playback-settings.bmp)bin153666 -> 153666 bytes
-rw-r--r--content/blog/rockbox_stats/player.bmp (renamed from blog/rockbox_stats/player.bmp)bin153666 -> 153666 bytes
-rw-r--r--content/blog/terminal_renderer_mkii/cover.png (renamed from blog/terminal_renderer_mkii/cover.png)bin6038269 -> 6038269 bytes
-rw-r--r--content/blog/terminal_renderer_mkii/david.png (renamed from blog/terminal_renderer_mkii/david.png)bin27218 -> 27218 bytes
-rw-r--r--content/blog/terminal_renderer_mkii/davidbayer.png (renamed from blog/terminal_renderer_mkii/davidbayer.png)bin4213 -> 4213 bytes
-rw-r--r--content/blog/terminal_renderer_mkii/davidthreshold.png (renamed from blog/terminal_renderer_mkii/davidthreshold.png)bin3324 -> 3324 bytes
-rw-r--r--content/blog/terminal_renderer_mkii/index.md167
-rw-r--r--content/heaven/_index.md4
-rw-r--r--content/heaven/angel.gif (renamed from heaven/angel.gif)bin6866 -> 6866 bytes
-rw-r--r--content/heaven/angel2.gif (renamed from heaven/angel2.gif)bin89484 -> 89484 bytes
-rw-r--r--content/heaven/angel3.gif (renamed from heaven/angel3.gif)bin8610 -> 8610 bytes
-rw-r--r--content/heaven/bg.jpg (renamed from heaven/bg.jpg)bin53321 -> 53321 bytes
-rw-r--r--content/heaven/everytime_we_touch_nightcore.ogg (renamed from heaven/everytime_we_touch_nightcore.ogg)bin1946392 -> 1946392 bytes
-rw-r--r--content/heaven/heaven.css (renamed from heaven/heaven.css)0
-rw-r--r--content/hell/Flying_Skeleton_Hell.gif (renamed from hell/Flying_Skeleton_Hell.gif)bin2004165 -> 2004165 bytes
-rw-r--r--content/hell/_index.md70
-rw-r--r--content/hell/bg.jpg (renamed from hell/bg.jpg)bin48691 -> 48691 bytes
-rw-r--r--content/hell/bigguy.gif (renamed from hell/bigguy.gif)bin11228 -> 11228 bytes
-rw-r--r--content/hell/comunismo.gif (renamed from hell/comunismo.gif)bin105570 -> 105570 bytes
-rw-r--r--content/hell/demon.gif (renamed from hell/demon.gif)bin8734 -> 8734 bytes
-rw-r--r--content/hell/demon2.gif (renamed from hell/demon2.gif)bin37169 -> 37169 bytes
-rw-r--r--content/hell/demon3.gif (renamed from hell/demon3.gif)bin27765 -> 27765 bytes
-rw-r--r--content/hell/demon4.gif (renamed from hell/demon4.gif)bin29208 -> 29208 bytes
-rw-r--r--content/hell/demon_face.gif (renamed from hell/demon_face.gif)bin22848 -> 22848 bytes
-rw-r--r--content/hell/evilmind.gif (renamed from hell/evilmind.gif)bin101192 -> 101192 bytes
-rw-r--r--content/hell/evilorb.gif (renamed from hell/evilorb.gif)bin6287 -> 6287 bytes
-rw-r--r--content/hell/firebreak.gif (renamed from hell/firebreak.gif)bin40755 -> 40755 bytes
-rw-r--r--content/hell/gay.gif (renamed from hell/gay.gif)bin41784 -> 41784 bytes
-rw-r--r--content/hell/gay2.gif (renamed from hell/gay2.gif)bin5601 -> 5601 bytes
-rw-r--r--content/hell/gaydudes.gif (renamed from hell/gaydudes.gif)bin101953 -> 101953 bytes
-rw-r--r--content/hell/hell.ogg (renamed from hell/hell.ogg)bin924991 -> 924991 bytes
-rw-r--r--content/hell/hellisreal.gif (renamed from hell/hellisreal.gif)bin59555 -> 59555 bytes
-rw-r--r--content/hell/hitler.gif (renamed from hell/hitler.gif)bin46349 -> 46349 bytes
-rw-r--r--content/hell/hitler2.gif (renamed from hell/hitler2.gif)bin45803 -> 45803 bytes
-rw-r--r--content/hell/hitler3.gif (renamed from hell/hitler3.gif)bin33592 -> 33592 bytes
-rw-r--r--content/hell/hot.gif (renamed from hell/hot.gif)bin4071 -> 4071 bytes
-rw-r--r--content/hell/kissing.jpg (renamed from hell/kissing.jpg)bin14011 -> 14011 bytes
-rw-r--r--content/hell/obama.gif (renamed from hell/obama.gif)bin1532992 -> 1532992 bytes
-rw-r--r--content/hell/pitchfork.gif (renamed from hell/pitchfork.gif)bin16727 -> 16727 bytes
-rw-r--r--content/hell/redfire.gif (renamed from hell/redfire.gif)bin21997 -> 21997 bytes
-rw-r--r--content/hell/skull.gif (renamed from hell/skull.gif)bin13021 -> 13021 bytes
-rw-r--r--content/hell/smallfire.gif (renamed from hell/smallfire.gif)bin5698 -> 5698 bytes
-rw-r--r--content/hell/torch.gif (renamed from hell/torch.gif)bin7960 -> 7960 bytes
-rw-r--r--content/projects/bevy_plugins/index.html (renamed from projects/bevy_plugins/index.html)0
-rw-r--r--content/projects/games/NIX_AVREA/index.html (renamed from projects/games/NIX_AVREA/index.html)0
-rw-r--r--content/projects/piss_daemon/index.html (renamed from projects/piss_daemon/index.html)0
-rw-r--r--content/projects/piss_daemon/statusbar.png (renamed from projects/piss_daemon/statusbar.png)bin4546 -> 4546 bytes
-rw-r--r--content/projects/project.css (renamed from projects/project.css)0
-rw-r--r--content/rockstats/index.html (renamed from rockstats/index.html)0
-rw-r--r--content/things_i_like/music/867.png (renamed from things_i_like/music/867.png)bin88542 -> 88542 bytes
-rw-r--r--content/things_i_like/music/act_ii.jpg (renamed from things_i_like/music/act_ii.jpg)bin33123 -> 33123 bytes
-rw-r--r--content/things_i_like/music/apollo.jpg (renamed from things_i_like/music/apollo.jpg)bin5380 -> 5380 bytes
-rw-r--r--content/things_i_like/music/atebts.jpg (renamed from things_i_like/music/atebts.jpg)bin12938 -> 12938 bytes
-rw-r--r--content/things_i_like/music/departure_songs.jpg (renamed from things_i_like/music/departure_songs.jpg)bin49939 -> 49939 bytes
-rw-r--r--content/things_i_like/music/index.html (renamed from things_i_like/music/index.html)0
-rw-r--r--content/things_i_like/music/jiminy.jpg (renamed from things_i_like/music/jiminy.jpg)bin135977 -> 135977 bytes
-rw-r--r--content/things_i_like/music/lysf.jpg (renamed from things_i_like/music/lysf.jpg)bin86650 -> 86650 bytes
-rw-r--r--content/things_i_like/music/twin_fantasy.jpg (renamed from things_i_like/music/twin_fantasy.jpg)bin20076 -> 20076 bytes
-rw-r--r--content/things_i_like/music/wetdream.png (renamed from things_i_like/music/wetdream.png)bin72338 -> 72338 bytes
-rw-r--r--hell/hell.css66
-rw-r--r--hell/index.html75
-rw-r--r--index.html105
-rw-r--r--static/98.css1040
-rw-r--r--static/assets/UnifontExMono.woff2 (renamed from assets/UnifontExMono.woff2)bin2002128 -> 2002128 bytes
-rw-r--r--static/assets/badges/cookies.pngbin0 -> 515 bytes
-rw-r--r--static/assets/badges/go2hell.gif (renamed from assets/badges/go2hell.gif)bin1577 -> 1577 bytes
-rw-r--r--static/assets/badges/indieweb.pngbin0 -> 524 bytes
-rw-r--r--static/assets/badges/javascript.pngbin0 -> 412 bytes
-rw-r--r--static/assets/badges/lynx.gif (renamed from assets/badges/lynx.gif)bin3303 -> 3303 bytes
-rw-r--r--static/assets/badges/midi_files_now.gif (renamed from assets/badges/midi_files_now.gif)bin1395 -> 1395 bytes
-rw-r--r--static/assets/badges/powered-by-debian.gif (renamed from assets/badges/powered-by-debian.gif)bin904 -> 904 bytes
-rw-r--r--static/assets/badges/soaos.pngbin0 -> 2563 bytes
-rw-r--r--static/assets/bg.jpg (renamed from assets/bg.jpg)bin31580 -> 31580 bytes
-rw-r--r--static/assets/construction.gif (renamed from assets/construction.gif)bin9933 -> 9933 bytes
-rw-r--r--static/assets/icon/button-down-active.svg5
-rw-r--r--static/assets/icon/button-down.svg8
-rw-r--r--static/assets/icon/button-left.svg8
-rw-r--r--static/assets/icon/button-right.svg8
-rw-r--r--static/assets/icon/button-up.svg8
-rw-r--r--static/assets/icon/checkmark-disabled.svg3
-rw-r--r--static/assets/icon/checkmark.svg3
-rw-r--r--static/assets/icon/close.svg3
-rw-r--r--static/assets/icon/groupbox-border.svg4
-rw-r--r--static/assets/icon/help.svg8
-rw-r--r--static/assets/icon/indicator-horizontal.svg6
-rw-r--r--static/assets/icon/indicator-rectangle-horizontal.svg6
-rw-r--r--static/assets/icon/maximize-disabled.svg4
-rw-r--r--static/assets/icon/maximize.svg3
-rw-r--r--static/assets/icon/minimize.svg3
-rw-r--r--static/assets/icon/radio-border-disabled.svg7
-rw-r--r--static/assets/icon/radio-border.svg8
-rw-r--r--static/assets/icon/radio-dot-disabled.svg3
-rw-r--r--static/assets/icon/radio-dot.svg3
-rw-r--r--static/assets/icon/restore.svg10
-rw-r--r--static/assets/icon/scrollbar-background.svg4
-rw-r--r--static/assets/icon/sunken-panel-border.svg11
-rw-r--r--static/favicon.png (renamed from favicon.png)bin4287 -> 4287 bytes
-rw-r--r--static/hell.css102
-rw-r--r--static/index.css (renamed from index.css)0
-rw-r--r--static/style.css184
-rw-r--r--static/style.css.bak224
-rw-r--r--style.css215
-rw-r--r--templates/base.html95
-rw-r--r--templates/blog.html38
-rw-r--r--templates/heaven.html (renamed from heaven/index.html)10
-rw-r--r--templates/hell.html22
-rw-r--r--templates/index.html2
-rw-r--r--templates/macros.html3
-rw-r--r--templates/page.html2
-rw-r--r--templates/post.html19
-rw-r--r--templates/section.html2
-rw-r--r--templates/shortcodes/soaosed.html3
-rw-r--r--templates/shortcodes/subtree.html8
-rw-r--r--templates/shortcodes/title_bar.html8
-rw-r--r--templates/shortcodes/tree_view.html5
-rw-r--r--templates/shortcodes/treelink.html8
-rw-r--r--templates/shortcodes/window.html3
-rw-r--r--templates/shortcodes/window_body.html1
135 files changed, 2775 insertions, 1217 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..d70ebaa
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+public \ No newline at end of file
diff --git a/.kateproject.build b/.kateproject.build
new file mode 100644
index 0000000..ac117bf
--- /dev/null
+++ b/.kateproject.build
@@ -0,0 +1,23 @@
+{
+ "Auto_generated": "This file is auto-generated. Any extra tags or formatting will be lost",
+ "target_sets": [
+ {
+ "cmake_config": "",
+ "directory": "/home/soaos/repo/soaos.dev",
+ "loaded_via_cmake": false,
+ "name": "Target Set",
+ "targets": [
+ {
+ "build_cmd": "cd %w && zola build",
+ "name": "Build",
+ "run_cmd": ""
+ },
+ {
+ "build_cmd": "",
+ "name": "Run Live Server",
+ "run_cmd": "cd %w && zola serve"
+ }
+ ]
+ }
+ ]
+}
diff --git a/.radicle/native.yaml b/.radicle/native.yaml
index 2b826b5..6ff3eb6 100644
--- a/.radicle/native.yaml
+++ b/.radicle/native.yaml
@@ -1 +1 @@
-shell: rm -rf /var/www/soaos.dev && mkdir /var/www/soaos.dev && cp * -r /var/www/soaos.dev
+shell: rm -rf /var/www/soaos.dev && mkdir /var/www/soaos.dev && zola build && cp public/* -r /var/www/soaos.dev
diff --git a/assets/UnifontExMono.woff b/assets/UnifontExMono.woff
deleted file mode 100644
index 89bc299..0000000
--- a/assets/UnifontExMono.woff
+++ /dev/null
Binary files differ
diff --git a/assets/badges/html.gif b/assets/badges/html.gif
deleted file mode 100644
index 106af2d..0000000
--- a/assets/badges/html.gif
+++ /dev/null
Binary files differ
diff --git a/assets/badges/javascript.gif b/assets/badges/javascript.gif
deleted file mode 100644
index 7937f54..0000000
--- a/assets/badges/javascript.gif
+++ /dev/null
Binary files differ
diff --git a/assets/badges/neovim.gif b/assets/badges/neovim.gif
deleted file mode 100644
index c3670e2..0000000
--- a/assets/badges/neovim.gif
+++ /dev/null
Binary files differ
diff --git a/assets/unifont.woff2 b/assets/unifont.woff2
deleted file mode 100644
index d6e201a..0000000
--- a/assets/unifont.woff2
+++ /dev/null
Binary files differ
diff --git a/assets/unifont_upper.woff2 b/assets/unifont_upper.woff2
deleted file mode 100644
index 47be929..0000000
--- a/assets/unifont_upper.woff2
+++ /dev/null
Binary files differ
diff --git a/blog/rockbox_stats/index.html b/blog/rockbox_stats/index.html
deleted file mode 100644
index 4aca821..0000000
--- a/blog/rockbox_stats/index.html
+++ /dev/null
@@ -1,560 +0,0 @@
-<!DOCTYPE html>
-
-<html>
-
-<head>
- <title></title>
- <link rel="stylesheet" href="/style.css">
- <link rel="stylesheet" href="/blog/blog.css">
-</head>
-
-<body>
- <div class="text-section">
- <a href="..">↰ Back</a>
- <a href="/">⌂ Home</a>
- </div>
- <article>
- <section>
- <div class="text-section">
- <!-- Header Section -->
- <h1>Rockbox Stat Tracking</h1>
- <p>September 22, 2025</p>
- <p>In this post I talk about how I went about setting up a <a href="/rockstats" target="_blank">stat visualization page</a> for my rockbox mp3 player.</p>
- </div>
- <figure class="cover-image">
-<svg width="100%" height="360" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" baseProfile="full" viewBox="0 0 640 360">
-<rect width="640" height="360" x="0" y="0" fill="rgb(0,0,0)" fill-opacity="0"></rect>
-<polyline points="427.2 233.9 440.6 240.7 455.6 240.7" fill="transparent" stroke="#e67e80" class="zr0-cls-0"></polyline>
-<polyline points="202.3 203.4 192.6 206.3 177.6 206.3" fill="transparent" stroke="#e69875" class="zr0-cls-0"></polyline>
-<polyline points="220 113.6 197.5 135.5 182.5 135.5" fill="transparent" stroke="#dbbc7f" class="zr0-cls-0"></polyline>
-<polyline points="273.2 69.5 190.3 117.4 190.3 117.4" fill="transparent" stroke="#a7c080" class="zr0-cls-0"></polyline>
-<polyline points="304.6 61 201.7 99.3 201.7 99.3" fill="transparent" stroke="#83c092" class="zr0-cls-0"></polyline>
-<polyline points="310.7 60.4 217.9 81.2 217.9 81.2" fill="transparent" stroke="#7fbbb3" class="zr0-cls-0"></polyline>
-<polyline points="315.4 60.1 242.2 63.1 242.2 63.1" fill="transparent" stroke="#d699b6" class="zr0-cls-0"></polyline>
-<polyline points="318.6 60 318.4 45 303.4 45" fill="transparent" stroke="#f85552" class="zr0-cls-0"></polyline>
-<path d="M320 60A120 120 0 1 1 223.6 251.5L320 180Z" fill="#e67e80" stroke="var(--bg0)" stroke-linejoin="round" ecmeta_series_index="0" ecmeta_data_index="0" ecmeta_ssr_type="chart" class="zr0-cls-1"></path>
-<path d="M223.6 251.5A120 120 0 0 1 203.6 150.7L320 180Z" fill="#e69875" stroke="var(--bg0)" stroke-linejoin="round" ecmeta_series_index="0" ecmeta_data_index="1" ecmeta_ssr_type="chart" class="zr0-cls-2"></path>
-<path d="M203.6 150.7A120 120 0 0 1 247.9 84.1L320 180Z" fill="#dbbc7f" stroke="var(--bg0)" stroke-linejoin="round" ecmeta_series_index="0" ecmeta_data_index="2" ecmeta_ssr_type="chart" class="zr0-cls-3"></path>
-<path d="M247.9 84.1A120 120 0 0 1 301.3 61.5L320 180Z" fill="#a7c080" stroke="var(--bg0)" stroke-linejoin="round" ecmeta_series_index="0" ecmeta_data_index="3" ecmeta_ssr_type="chart" class="zr0-cls-4"></path>
-<path d="M301.3 61.5A120 120 0 0 1 307.8 60.6L320 180Z" fill="#83c092" stroke="var(--bg0)" stroke-linejoin="round" ecmeta_series_index="0" ecmeta_data_index="4" ecmeta_ssr_type="chart" class="zr0-cls-5"></path>
-<path d="M307.8 60.6A120 120 0 0 1 313.6 60.2L320 180Z" fill="#7fbbb3" stroke="var(--bg0)" stroke-linejoin="round" ecmeta_series_index="0" ecmeta_data_index="5" ecmeta_ssr_type="chart" class="zr0-cls-6"></path>
-<path d="M313.6 60.2A120 120 0 0 1 317.2 60L320 180Z" fill="#d699b6" stroke="var(--bg0)" stroke-linejoin="round" ecmeta_series_index="0" ecmeta_data_index="6" ecmeta_ssr_type="chart" class="zr0-cls-7"></path>
-<path d="M317.2 60A120 120 0 0 1 320 60L320 180Z" fill="#f85552" stroke="var(--bg0)" stroke-linejoin="round" ecmeta_series_index="0" ecmeta_data_index="7" ecmeta_ssr_type="chart" class="zr0-cls-8"></path>
-<text dominant-baseline="central" text-anchor="start" style="font-size:16px;font-family:unifont;" xml:space="preserve" transform="translate(460.5907 240.6868)" fill="var(--fg)">Progressive Rock</text>
-<text dominant-baseline="central" text-anchor="end" style="font-size:16px;font-family:unifont;" transform="translate(172.5853 206.2935)" fill="var(--fg)">Alternative</text>
-<text dominant-baseline="central" text-anchor="end" style="font-size:16px;font-family:unifont;" transform="translate(177.5244 135.5094)" fill="var(--fg)">Post-Rock</text>
-<text dominant-baseline="central" text-anchor="end" style="font-size:16px;font-family:unifont;" transform="translate(185.3496 117.4094)" fill="var(--fg)">Post-Hardcore</text>
-<text dominant-baseline="central" text-anchor="end" style="font-size:16px;font-family:unifont;" transform="translate(196.7011 99.3094)" fill="var(--fg)">Post-Metal</text>
-<text dominant-baseline="central" text-anchor="end" style="font-size:16px;font-family:unifont;" transform="translate(212.8733 81.2094)" fill="var(--fg)">Rock</text>
-<text dominant-baseline="central" text-anchor="end" style="font-size:16px;font-family:unifont;" transform="translate(237.2337 63.1094)" fill="var(--fg)">Shoegaze</text>
-<text dominant-baseline="central" text-anchor="end" style="font-size:16px;font-family:unifont;" xml:space="preserve" transform="translate(298.4082 45.0094)" fill="var(--fg)">Progressive Metal</text>
-<style>
-.zr0-cls-0:hover {
-cursor:pointer;
-}
-.zr0-cls-1:hover {
-cursor:pointer;
-fill:rgba(253,138,140,1);
-}
-.zr0-cls-2:hover {
-cursor:pointer;
-fill:rgba(253,167,128,1);
-}
-.zr0-cls-3:hover {
-cursor:pointer;
-fill:rgba(240,206,139,1);
-}
-.zr0-cls-4:hover {
-cursor:pointer;
-fill:rgba(183,211,140,1);
-}
-.zr0-cls-5:hover {
-cursor:pointer;
-fill:rgba(144,211,160,1);
-}
-.zr0-cls-6:hover {
-cursor:pointer;
-fill:rgba(139,205,196,1);
-}
-.zr0-cls-7:hover {
-cursor:pointer;
-fill:rgba(235,168,200,1);
-}
-.zr0-cls-8:hover {
-cursor:pointer;
-fill:rgba(255,93,90,1);
-}
-
-
-
-</style>
-</svg>
- <figcaption>A static site generation experiment</figcaption>
- </figure>
- </section>
- <section class="text-section">
- <h2>Preamble: Digital Sovereignity & Rockbox</h2>
- <p>
- I've been building up a pretty sizeable collection of digital music
- over the last couple of years. I think there's a lot of value in owning
- the music I pay for and being able to choose how I listen to it.
- Purchasing music also allows me to support artists in a more direct
- and substantial way than the fractions of cents for using streaming services,
- but that's more of a happy consequence than some moral obligation I feel.
- </p>
- <p>
- Over the years, I've enjoyed listening to my music in a variety of ways.
- For years I kept all of my music files on all of my devices and used
- various local music clients depending on the platform, most notably mpd
- and ncmpcpp on linux. Eventually, as I charged headlong into the glorious
- world of self-hosting, I began using a central Jellyfin media server that
- I stream music and video from. It's super convenient, and works on all of
- my devices (including my TV!).
- </p>
- <p>
- My media server is great, and it's been the primary way I listen to music
- for a while now. But it has limitations. For example, I don't expose my media
- server to the internet, so I'm unable to stream from it while I'm out and
- about. And even if I could, the bandwidth requirements would be pretty high.
- I figured I would need a dedicated music player if I wanted to take my music
- library on the go, and settled on the HIFI Walker H2 after reading some
- online recommendations. The ability to install <a href="https://rockbox.org" target="_blank">Rockbox</a>, an open-source firmware,
- was a big factor in my decision. I couldn't tell you how the device works
- out of the box, since I flashed the firmware pretty much immediately once I got it,
- but I've been super impressed with how the device works while running Rockbox.
- </p>
- <p>
- <figure class="fig fig-right">
- <img src="player.bmp" alt="Screenshot of Rockbox player showing cool theme.">
- <figcaption>I'm using a modified version of the <a
- href="https://themes.rockbox.org/index.php?themeid=3266&target=aigoerosq"
- target="_blank">InfoMatrix-v2</a> theme, which looks great.</figcaption>
- </figure>
- Rockbox comes with many codecs for common audio formats including FLAC and MP3. The
- device boots extremely quickly, and the interface is snappy. Virtually every aspect
- of the user experience is tweakable and customizable to a crazy degree. I've even begun
- listening to music on my player even at home, since a device specifically for the
- purpose provides less distraction while I'm trying to be productive.
- </p>
- <p>
- All this to say I'm pretty much sold on Rockbox. But there's certain things I
- still miss from my days of being a user of popular services like Spotify with
- fancy APIs and data tracking. Things like Spotify wrapped or third-party apps
- for visualizing playback statistics are a fun way to see what my listening history
- looks like and could potentially be used to help find more music that I'd enjoy.
- This is why when I noticed that Rockbock has a playback logging feature, a little
- lightbulb lit up over my head.
- </p>
- </section>
- <section class="text-section">
- <h2>Generating and Parsing Logs</h2>
- <p>
- <figure class="fig fig-right">
- <img src="log-setting.bmp" alt="Logging">
- <figcaption>The logging feature can be accessed through the settings menu.</figcaption>
- </figure>
- Rockbox has a feature that logs playback information to a text file. This feature can
- be enabled by setting <b>Playback Settings > Logging</b> to "On". With this setting enabled, a
- new line gets added to the end of the <b>.rockbox/playback.log</b> file every time you play a track,
- containing info about what you played and when.
- </p>
- <p>
- The logging feature is actually already used by the LastFM scrobbler plugin that comes preloaded with
- Rockbox, which is probably the simplest way to get insights into your playback. However,
- I personally want to avoid using third-party services as much as possible, because it's more fun.
- </p>
- <p>
- If I take a look at a logfile generated after a bit of listening, I'll see that I've wound up with
- a series of lines that each look something like this:
- <figure class="fig">
- <pre><samp>1758478258:336689:336718:/&lt;microSD0&gt;/Music/This Is The Glasshouse/This Is The Glasshouse - 867/This Is The Glasshouse - 867 - 01 Streetlight By Streetlight.flac</samp></pre>
- <figcaption>An example of a log entry for "Streetlight by Streetlight" by This is the Glasshouse.
- </figcaption>
- </figure>
- </p>
- <p>
- I wasn't really able to find any information online about the format of these logs, but they appear
- to be simple enough to figure out. From what I can tell, each event is broken up into 4 pieces:
- <ol>
- <li><b>Timestamp:</b> The number of milliseconds since the UNIX epoch.
- <li><b>Playback Duration:</b> The amount of the song that was played, in milliseconds.
- <li><b>Total Track Length:</b> The length of the played track, in milliseconds.
- <li><b>File Path:</b> An absolute path to the file containing the track on the filesystem.
- </ol>
- All of this is enough to know what I was listening to and when. I can use the file path to check for
- audio tags which can help glean even more information about my listening habits.
- </p>
- <p>Now that I have this information and know how to interpret it, I'm ready to start processing it!</p>
- </section>
- <section class="text-section">
- <h2>Analyzing Playback History</h2>
- <p>
- In order to get some useful information out of my playback history, I think it's a good idea to start by
- building
- a database. I created a sqlite database with the following tables:
- <table class="schema-table">
- <thead>
- <tr>
- <th colspan="3">songs</th>
- </tr>
- </thead>
- <tbody>
- <tr>
- <td>id</td>
- <td>i64</td>
- <td>PK</td>
- </tr>
- <tr>
- <td>title</td>
- <td>String</td>
- <td></td>
- </tr>
- <tr>
- <td>artists</td>
- <td>JSON</td>
- <td></td>
- </tr>
- <tr>
- <td>album_id</td>
- <td>i64?</td>
- <td></td>
- </tr>
- <tr>
- <td>genre</td>
- <td>String?</td>
- <td></td>
- </tr>
- </tbody>
- </table>
- <table class="schema-table">
- <thead>
- <tr>
- <th colspan="3">albums</th>
- </tr>
- </thead>
- <tbody>
- <tr>
- <td>id</td>
- <td>i64</td>
- <td>PK</td>
- </tr>
- <tr>
- <td>title</td>
- <td>String</td>
- <td></td>
- </tr>
- <tr>
- <td>artist</td>
- <td>String</td>
- <td></td>
- </tr>
- <tr>
- <td>cover_art</td>
- <td>Blob?</td>
- <td></td>
- </tr>
- </tbody>
- </table>
- <table class="schema-table">
- <thead>
- <tr>
- <th colspan="3">history</th>
- </tr>
- </thead>
- <tbody>
- <tr>
- <td>id</td>
- <td>i64</td>
- <td>PK</td>
- </tr>
- <tr>
- <td>timestamp</td>
- <td>Datetime</td>
- <td></td>
- </tr>
- <tr>
- <td>duration</td>
- <td>i64</td>
- <td></td>
- </tr>
- <tr>
- <td>song_id</td>
- <td>i64</td>
- <td></td>
- </tr>
- </tbody>
- </table>
- <br>
- I can add more columns later, but this is a good place to start.
- </p>
- <p>
- Now, as I read through the logfile line-by-line, I can check if each album exists before
- inserting it into the database:
- <figure class="fig">
- <pre><code>for line in log_file.lines().flatten() {
- println!("{line}");
- // Skip comments
- if line.starts_with("#") {
- continue;
- }
- let chunks = line.split(":").collect::<Vec<_>>();
-
- let timestamp = DateTime::from_timestamp_secs(
- i64::from_str_radix(chunks[0], 10).context("Failed to parse timestamp")?,
- )
- .context("Failed to convert timestamp")?;
-
- // Load tags from file on device
- let file_path = chunks[chunks.len() - 1][1..]
- .split_once("/")
- .context("Missing file")?
- .1;
- let tags = Tag::new()
- .read_from_path(args.mount_point.join(file_path))
- .context("Failed to read audio tags")?;
-
- //...
-}</code></pre>
- <figcaption>Parsing log entry and loading audio metadata.</figcaption>
- </figure>
- <figure class="fig">
- <pre><code>if let Some(existing_album) =
- sqlx::query("SELECT id FROM albums WHERE title=$1 AND artist=$2")
- .bind(album_title)
- .bind(album_artist)
- .fetch_optional(&mut *db)
- .await
- .context("Failed to execute query to find existing album")?
-{
- let album_id: i64 = existing_album.get("id");
- info!("Album already exists, id {album_id}");
- //...
-} else {
- info!("Inserting new album: {album_title} by {album_artist}");
- //...
- let result = sqlx::query(
- "INSERT INTO albums (title, artist, cover_art) VALUES ($1, $2, $3);",
- )
- .bind(album_title)
- .bind(album_artist)
- .bind(cover)
- .execute(&mut *db)
- .await
- .context("Failed to execute query to insert album into database")?;
-
- //...
-}</code></pre>
- <figcaption>Checking for an album with matching artist and title before creating a new row in the
- database.</figcaption>
- </figure>
- I did something similar with the <b>songs</b> and <b>history</b> tables, basically building up a cache
- of history information and skipping anything that's already in the database on repeat runs.
- </p>
- <p>
- With this database constructed, it's pretty easy to get a bunch of different information
- about my listening. For example (forgive me if my SQL skills are kind of ass lol):
- <figure class="fig">
- <pre><code>SELECT
- songs.title AS song_title,
- songs.artists AS song_artists,
- songs.genre AS song_genre,
- albums.title AS album_title,
- albums.artist AS album_artist,
- history.timestamp AS timestamp,
- history.duration AS duration
-FROM history
-CROSS JOIN songs ON songs.id = history.song_id
-CROSS JOIN albums ON albums.id = songs.album_id
-ORDER BY timestamp DESC;</code></pre>
- <figcaption>Querying for a list of each history entry along with track metadata, sorted from most to
- least recent.</figcaption>
- </figure>
- <figure class="fig">
- <pre><code>SELECT
- songs.genre,
- SUM(history.duration) AS total_duration
-FROM history
-CROSS JOIN songs ON history.song_id = songs.id
-GROUP BY genre
-ORDER BY total_duration DESC
-LIMIT 10; </code></pre>
- <figcaption>Querying for the top 10 most listened genres by playtime.</figcaption>
- </figure>
- </p>
- <p>
- It's all well and good to be able to view this information using a database client,
- but it would be really cool if I could visualize this data somehow.
- </p>
- </section>
- <section class="text-section">
- <h2>Visualizing this Data Somehow</h2>
- <p>
- I wanted to make this data available on my website for people to view, and for a bunch of mostly trivial
- reasons I won't get into here, I have a couple of requirements for pages on this site:
- <ol>
- <li>Pages need to be static.
- <li>Pages need to be JavaScript-free.
- </ol>
- This means any chart rendering needs to be done automatically at build time before
- deploying. I don't currently use a static site generator for my site (just for fun),
- so I'm basically going to need to write one specifically to generate this page.
- </p>
- <p>
- I won't get too deep into the specifics of how I queried the database and generated each visualization
- on
- the page, but I can explain the visualizations I created using the queries from the previous section.
- For the
- listening history I wanted to generate a table displaying the information. To accomplish this, I first
- used a combination of <a href="https://crates.io/crates/sqlx" target="_blank">sqlx</a>'s ability to convert a row to a struct and <a href="https://crates.io/crates/serde" target="_blank">serde</a> to serialize
- the rows as JSON values.
- <figure class="fig">
- <pre><code>#[derive(Serialize, Deserialize, FromRow)]
-struct HistoryEntry {
- song_title: String,
- song_artists: Value,
- timestamp: DateTime&lt;Utc&gt;,
- duration: i64,
- album_title: String,
- album_artist: Option&lt;String&gt;,
- song_genre: Option&lt;String&gt;,
-}
-
-//...later
-let history = sqlx::query_as::&lt;_, HistoryEntry&gt;(
- /* SELECT... */
-).fetch_all(&mut *db).await;
-
-//...later still, tera context accepts
-let mut context = tera::Context::new();
-context.insert("history", &history);
-</code></pre>
- <figcaption>Struct definition for a history entry, allowing conversion from a sqlx row and
- de/serialization from/to JSON.</figcaption>
- </figure>
- </p>
- <p>
- In order to keep the generation as painless as possible, I decided to use the <a href="https://keats.github.io/tera" target="_blank">Tera</a> template
- engine, which allows me to define a template HTML file and substitute in values from
- a context which I can define before rendering. In the case of the table, I can just generate a <code>&lt;tr&gt;</code>
- matching the data for each item:
- <figure class="fig">
- <pre><code>{% macro history_table(history) %}
-&lt;h3&gt;Playback History&lt;/h3&gt;
-&lt;div class=&quot;table-container&quot;&gt;
- &lt;table&gt;
- &lt;thead&gt;
- &lt;tr&gt;
- &lt;th&gt;Timestamp&lt;/th&gt;
- &lt;th&gt;Played Duration&lt;/th&gt;
- &lt;th&gt;Title&lt;/th&gt;
- &lt;th&gt;Artists&lt;/th&gt;
- &lt;th&gt;Album&lt;/th&gt;
- &lt;th&gt;Genre&lt;/th&gt;
- &lt;/tr&gt;
- &lt;/thead&gt;
- &lt;tbody&gt;
- {% for item in history %}&lt;tr&gt;
- &lt;td&gt;{{ item.timestamp | date(format=&quot;%Y-%m-%d %H:%M:%S&quot;) }}&lt;/td&gt;
- &lt;td&gt;{{ item.duration | hms }}&lt;/td&gt;
- &lt;td&gt;{{ item.song_title }}&lt;/td&gt;
- &lt;td&gt;{{ item.song_artists }}&lt;/td&gt;
- &lt;td&gt;{{ item.album_title }}&lt;/td&gt;
- &lt;td&gt;{{ item.song_genre }}&lt;/td&gt;
- &lt;/tr&gt;
- {% endfor %}
- &lt;/tbody&gt;
- &lt;/table&gt;
-&lt;/div&gt;
-{% endmacro history_table %}</code></pre>
- <figcaption>
- A Tera macro for generating a table from a list of playback history items.
- I used a macro so I can re-use this later if I want to add time range views.
- (last month, year, etc.)
- </figcaption>
- </figure>
- </p>
- <p>
- I wrote similar macros for each of the visualizations I wanted to create. Most are
- easy, but for my top 10 genres I wanted to display a pie chart. I found a pretty decent
- data visualization crate called <a href="https://crates.io/crates/charming" target="_blank">charming</a> that's able to render to html, however
- the output contains javascript so it's a no-go for me. Luckily, it can also render to
- an SVG which I can embed nicely within the page.
- <figure class="fig">
-<svg width="100%" height="360" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" baseProfile="full" viewBox="0 0 640 360">
-<rect width="640" height="360" x="0" y="0" fill="rgb(0,0,0)" fill-opacity="0"></rect>
-<polyline points="427.2 233.9 440.6 240.7 455.6 240.7" fill="transparent" stroke="#e67e80" class="zr0-cls-0"></polyline>
-<polyline points="202.3 203.4 192.6 206.3 177.6 206.3" fill="transparent" stroke="#e69875" class="zr0-cls-0"></polyline>
-<polyline points="220 113.6 197.5 135.5 182.5 135.5" fill="transparent" stroke="#dbbc7f" class="zr0-cls-0"></polyline>
-<polyline points="273.2 69.5 190.3 117.4 190.3 117.4" fill="transparent" stroke="#a7c080" class="zr0-cls-0"></polyline>
-<polyline points="304.6 61 201.7 99.3 201.7 99.3" fill="transparent" stroke="#83c092" class="zr0-cls-0"></polyline>
-<polyline points="310.7 60.4 217.9 81.2 217.9 81.2" fill="transparent" stroke="#7fbbb3" class="zr0-cls-0"></polyline>
-<polyline points="315.4 60.1 242.2 63.1 242.2 63.1" fill="transparent" stroke="#d699b6" class="zr0-cls-0"></polyline>
-<polyline points="318.6 60 318.4 45 303.4 45" fill="transparent" stroke="#f85552" class="zr0-cls-0"></polyline>
-<path d="M320 60A120 120 0 1 1 223.6 251.5L320 180Z" fill="#e67e80" stroke="var(--bg0)" stroke-linejoin="round" ecmeta_series_index="0" ecmeta_data_index="0" ecmeta_ssr_type="chart" class="zr0-cls-1"></path>
-<path d="M223.6 251.5A120 120 0 0 1 203.6 150.7L320 180Z" fill="#e69875" stroke="var(--bg0)" stroke-linejoin="round" ecmeta_series_index="0" ecmeta_data_index="1" ecmeta_ssr_type="chart" class="zr0-cls-2"></path>
-<path d="M203.6 150.7A120 120 0 0 1 247.9 84.1L320 180Z" fill="#dbbc7f" stroke="var(--bg0)" stroke-linejoin="round" ecmeta_series_index="0" ecmeta_data_index="2" ecmeta_ssr_type="chart" class="zr0-cls-3"></path>
-<path d="M247.9 84.1A120 120 0 0 1 301.3 61.5L320 180Z" fill="#a7c080" stroke="var(--bg0)" stroke-linejoin="round" ecmeta_series_index="0" ecmeta_data_index="3" ecmeta_ssr_type="chart" class="zr0-cls-4"></path>
-<path d="M301.3 61.5A120 120 0 0 1 307.8 60.6L320 180Z" fill="#83c092" stroke="var(--bg0)" stroke-linejoin="round" ecmeta_series_index="0" ecmeta_data_index="4" ecmeta_ssr_type="chart" class="zr0-cls-5"></path>
-<path d="M307.8 60.6A120 120 0 0 1 313.6 60.2L320 180Z" fill="#7fbbb3" stroke="var(--bg0)" stroke-linejoin="round" ecmeta_series_index="0" ecmeta_data_index="5" ecmeta_ssr_type="chart" class="zr0-cls-6"></path>
-<path d="M313.6 60.2A120 120 0 0 1 317.2 60L320 180Z" fill="#d699b6" stroke="var(--bg0)" stroke-linejoin="round" ecmeta_series_index="0" ecmeta_data_index="6" ecmeta_ssr_type="chart" class="zr0-cls-7"></path>
-<path d="M317.2 60A120 120 0 0 1 320 60L320 180Z" fill="#f85552" stroke="var(--bg0)" stroke-linejoin="round" ecmeta_series_index="0" ecmeta_data_index="7" ecmeta_ssr_type="chart" class="zr0-cls-8"></path>
-<text dominant-baseline="central" text-anchor="start" style="font-size:16px;font-family:unifont;" xml:space="preserve" transform="translate(460.5907 240.6868)" fill="var(--fg)">Progressive Rock</text>
-<text dominant-baseline="central" text-anchor="end" style="font-size:16px;font-family:unifont;" transform="translate(172.5853 206.2935)" fill="var(--fg)">Alternative</text>
-<text dominant-baseline="central" text-anchor="end" style="font-size:16px;font-family:unifont;" transform="translate(177.5244 135.5094)" fill="var(--fg)">Post-Rock</text>
-<text dominant-baseline="central" text-anchor="end" style="font-size:16px;font-family:unifont;" transform="translate(185.3496 117.4094)" fill="var(--fg)">Post-Hardcore</text>
-<text dominant-baseline="central" text-anchor="end" style="font-size:16px;font-family:unifont;" transform="translate(196.7011 99.3094)" fill="var(--fg)">Post-Metal</text>
-<text dominant-baseline="central" text-anchor="end" style="font-size:16px;font-family:unifont;" transform="translate(212.8733 81.2094)" fill="var(--fg)">Rock</text>
-<text dominant-baseline="central" text-anchor="end" style="font-size:16px;font-family:unifont;" transform="translate(237.2337 63.1094)" fill="var(--fg)">Shoegaze</text>
-<text dominant-baseline="central" text-anchor="end" style="font-size:16px;font-family:unifont;" xml:space="preserve" transform="translate(298.4082 45.0094)" fill="var(--fg)">Progressive Metal</text>
-<style>
-.zr0-cls-0:hover {
-cursor:pointer;
-}
-.zr0-cls-1:hover {
-cursor:pointer;
-fill:rgba(253,138,140,1);
-}
-.zr0-cls-2:hover {
-cursor:pointer;
-fill:rgba(253,167,128,1);
-}
-.zr0-cls-3:hover {
-cursor:pointer;
-fill:rgba(240,206,139,1);
-}
-.zr0-cls-4:hover {
-cursor:pointer;
-fill:rgba(183,211,140,1);
-}
-.zr0-cls-5:hover {
-cursor:pointer;
-fill:rgba(144,211,160,1);
-}
-.zr0-cls-6:hover {
-cursor:pointer;
-fill:rgba(139,205,196,1);
-}
-.zr0-cls-7:hover {
-cursor:pointer;
-fill:rgba(235,168,200,1);
-}
-.zr0-cls-8:hover {
-cursor:pointer;
-fill:rgba(255,93,90,1);
-}
-
-
-
-</style>
-</svg>
- <figcaption>Here's one I generated just now.</figcaption>
- </figure>
- </p>
- <p>
- And that's pretty much all there is to it! The finished thing can be found <a href="/rockstats" target="_blank">here</a>.
- </p>
- </section>
- </article>
-</body>
-
-</html> \ No newline at end of file
diff --git a/blog/terminal_renderer_mkii/index.html b/blog/terminal_renderer_mkii/index.html
deleted file mode 100644
index 326c5d4..0000000
--- a/blog/terminal_renderer_mkii/index.html
+++ /dev/null
@@ -1,189 +0,0 @@
-<!DOCTYPE html>
-
-<html>
-
-<head>
- <title>Terminal Renderer Mk. II - Rendering to Text with Compute</title>
- <meta name="og:title" content="Terminal Renderer Mk. II - Rendering to Text with Compute"/>
- <meta name="og:description" content="This week I brought my terminal renderer to the next level by performing text rendering on the GPU."/>
- <meta name="og:image" content="https://soaos.dev/blog/terminal_renderer_mkii/cover.png"/>
- <link rel="stylesheet" href="/style.css">
- <link rel="stylesheet" href="/blog/blog.css">
-</head>
-
-<body>
- <div class="text-section">
- <a href="..">↰ Back</a>
- <a href="/">⌂ Home</a>
- </div>
- <article>
- <section>
- <div class="text-section">
- <!-- Header Section -->
- <h1>Terminal Renderer Mk. II - Rendering to Text with Compute</h1>
- <p>October 2, 2025</p>
- <p>This week I brought my terminal renderer to the next level by performing text rendering on the GPU.
- </p>
- </div>
- <figure class="cover-image">
- <img src="cover.png" alt="">
- <figcaption>The Stanford Dragon, outlined and rendered as Braille characters in a terminal emulator. <a href="https://tv.soaos.dev/w/fBnDAUPsTPHaoPeNNxBGch" target="_blank">
-Full video</a>
- </figcaption>
- </figure>
- </section>
- <section class="text-section">
- <h2>Context</h2>
- <h3>Unicode Braille</h3>
- <p>
- I first messed around with rendering images to the terminal with Braille characters in like 2022 I
- think? I wrote a simple CLI tool
- that applied a threshold to an input image and output it as Braille characters in the terminal. <a
- href="https://tv.soaos.dev/w/twpHAu4Jv8LJc9YjZbfw5g" target="_blank">Here's a recording I took back
- when I did it.</a>
- </p>
-
- <p>
- <figure class="fig fig-right">
- <div class="centered">
- <table class="schema-table">
- <tbody>
- <tr>
- <td>0</td>
- <td>3</td>
- </tr>
- <tr>
- <td>1</td>
- <td>4</td>
- </tr>
- <tr>
- <td>2</td>
- <td>5</td>
- </tr>
- <tr>
- <td>6</td>
- <td>7</td>
- </tr>
- </tbody>
- </table>
- </div>
- <figcaption>The corresponding bit position for each braille dot.</figcaption>
- </figure>
- This effect is pretty cool, and it was pretty easy to implement as well. The trick lies in how the
- <a href="https://en.wikipedia.org/wiki/Braille_Patterns#Block" target="_blank">Unicode Braille block</a>
- is laid out. Every 8-dot Braille combination happens to add up to 256 combinations, the perfect amount to
- fit in the range between <code>0x2800</code> (⠀) and <code>0x28FF</code> (⣿). In other words, every
- character
- within the block can be represented by changing the value of a single byte.
- </p>
- <p>
- The lowest 6 bits of the pattern map on to a 6-dot braille pattern. However, due
- to historical reasons the 8-dot values were tacked on after the fact, which adds
- a slightly annoying mapping to the conversion process. Either way, it's a lot easier
- than it could be to just read a pixel value, check its brightness, and then use a
- bitwise operation to set/clear a dot.
- </p>
- <h3>Ordered Dithering</h3>
- <p>
- Comparing the brightnes of a pixel against a constant threshold is a fine way to
- display black and white images, but it's far from ideal and often results in the loss
- of a lot of detail from the original image.
- </p>
- <figure class="fig fig-horizontal">
- <div class="horizontal-container">
- <img src="david.png" alt="" />
- <img src="davidthreshold.png" alt="" />
- <img src="davidbayer.png" alt="" />
- </div>
- <figcaption>From left to right: Original image, threshold, and ordered dither. <a
- href="https://en.wikipedia.org/wiki/Dither" target="_blank">Wikipedia</a></figcaption>
- </figure>
- <p>By using <a href="https://en.wikipedia.org/wiki/Ordered_dithering" target="_blank">ordered dithering</a>,
- we
- can preserve much more of the subtleties of the original image. While not the "truest" version of
- dithering possible,
- ordered dithering (and <i>Bayer</i> dithering in particular) provides a few advantages that make it very
- well suited to realtime computer graphics:
- <ul>
- <li>Each pixel is dithered independent of any other pixel in the image, making it extremely
- parallelizable and good for shaders.</li>
- <li>It's visually stable, changes to one part of the image won't disturb other areas.</li>
- <li>It's dead simple.</li>
- </ul>
- Feel free to read up on the specifics of threshold maps and stuff, but for the purposes of this little
- explanation it's
- enough to know that it's basically just a matrix of 𝓃⨉𝓃 values between 0 and 1, and then to determine
- whether a pixel (𝓍,𝓎)
- is white or black, you check the brightness against the threshold value at (𝓍%𝓃,𝓎%𝓃) in the map.
- </p>
- </section>
- <section class="text-section">
- <h2>The old way™</h2>
- <p>
- My first attempt at <i>realtime</i> terminal graphics with ordered dithering
- (<a href="https://tv.soaos.dev/w/dzHBnPJXtDBwtSvirgwTvY" target="_blank">I put a video up at the time</a>)
- ran entirely on the CPU. I pre-calculated the threshold map at the beginning of execution and ran each
- frame
- through a sequential function to dither it and convert it to Braille characters.
- </p>
- <p>
- To be honest, I never noticed
- any significant performance issues doing this, as you can imagine the image size required to fill a
- terminal
- screen is signficantly smaller than a normal window. However, I knew I could easily perform the
- dithering on the GPU
- as a post-processing effect, so I eventually wrote a shader to do that. In combination with another
- effect I used to
- add outlines to objects, I was able to significantly improve the visual fidelity of the experience. A
- good example of
- where the renderer was at until like a week ago can be seen in <a
- href="https://tv.soaos.dev/w/9Pf2tP3PYY5pJ3Cimhqs9x" target="_blank">this video</a>.
- </p>
- <p>
- Until now I hadn't really considered moving the text conversion to the GPU. I mean, <i>G</i>PU is for
- graphics,
- right? I just copied the <i>entire framebuffer</i> back onto the CPU after dithering
- and used the same sequential conversion algorithm. Then I had an idea that would drastically reduce the
- amount
- of copying necessary.
- </p>
- </section>
- <section class="text-section">
- <h2>Compute post-processing</h2>
- <p>
- What if, instead of extracting and copying the framebuffer every single frame, we "rendered" the text on
- the GPU
- and read <i>that</i> back instead? Assuming each pixel in a texture is 32 bits (RGBA8), and knowing that
- each braille
- character is a block of 8 pixels, could we not theoretically shave off <i>at least</i> 7/8 of the bytes
- copied?
- </p>
- <p>
- As it turns out, it's remarkably easy to do. I'm using the <a href="https://bevy.org"
- target="_blank">Bevy engine</a>,
- and hooking in a compute node to my existing post-processing render pipeline worked right out of the
- box.
- I allocated a storage buffer large enough to hold the necessary amount of characters, read it back each
- frame, and dumped
- the contents into the terminal.
- </p>
- <p>
- I used UTF-32 encoding on the storage buffer because I knew I could easily convert a "wide string" into
- UTF-8 before printing it, and
- 32 bits provides a consistent space to fill for each workgroup in the shader versus a variable-length
- encoding like UTF-8. <a href="https://tv.soaos.dev/w/fBnDAUPsTPHaoPeNNxBGch" target="_blank">Here's a video of the new renderer working</a>.
- Although now that I think about it, I could probably switch to using UTF-16 since all the Braille
- characters could be represented
- in 2 bytes, and that would be half the size of the UTF-32 text, which is half empty bytes anyways.
- </p>
- <p>
- Okay so I went and tried that but remembered that shaders only accept 32-bit primitive types, so it doesn't matter anyways. This little side quest has been a part of my
- broader efforts to revive a project I
- spent a lot of time on. I'm taking the opportunity to really dig in and rework some of the stuff I'm not
- totally happy with. So there might be quite a few of this kind of post in the near future. Stay tuned.
- </p>
- </section>
- </article>
-</body>
-
-</html>
diff --git a/config.toml b/config.toml
new file mode 100644
index 0000000..8ab9b67
--- /dev/null
+++ b/config.toml
@@ -0,0 +1,18 @@
+# The URL the site will be built for
+base_url = "https://soaos.dev"
+
+# Whether to automatically compile all Sass files in the sass directory
+compile_sass = false
+
+# Whether to build a search index to be used later on by a JavaScript library
+build_search_index = false
+
+[markdown]
+# Whether to do syntax highlighting
+# Theme can be customised by setting the `highlight_theme` variable to a theme supported by Zola
+highlight_code = true
+
+external_links_target_blank = true
+
+[extra]
+# Put all your custom variables here
diff --git a/content/_index.md b/content/_index.md
new file mode 100644
index 0000000..4e36d29
--- /dev/null
+++ b/content/_index.md
@@ -0,0 +1,39 @@
++++
+title = "soaos"
+template = "index.html"
++++
+
+{% window() %}
+{% title_bar() %}
+🧍 Welcome to {% soaosed() %}soaos{% end %}.dev
+{% end %}
+{% window_body() %}
+
+I'm an "artist" and professional software developer. In my free time I mostly work on eccentric software projects which you can read about here.
+
+{% tree_view(height="16rem") %}
+
+{% subtree(name="On this site", open=true) %}
+{{ treelink(text="📖 Blog", url="/blog") }}
+{{ treelink(text="♫ Rockbox Stats", url="/rockstats", wip=true, wip_tooltip="🦌 -this shit is under construction, pal!") }}
+{{ treelink(text="🦌 Things I Like", url="/things_i_like", wip=true, wip_tooltip="🦌 -honestly these things are redundant, everything is under construction") }}
+{% end %}
+
+{% subtree(name='<span class="flip" data-title="Soaos"><span class="hidden-selectable">Soaos</span></span> Services') %}
+{{ treelink(text="🌱 Source Code", url="https://app.radicle.xyz/nodes/seed.soaos.dev", blank=true) }}
+{{ treelink(text="📺 Videos", url="https://tv.soaos.dev/c/soaosdev", blank=true) }}
+{{ treelink(text="🗃 Web Archive", url="https://archive.soaos.dev", blank=true) }}
+{{ treelink(text="🔍 Search Engine", url="https://search.soaos.dev", blank=true) }}
+{{ treelink(text="♊ Gemini Site", url="gemini://soaos.dev", blank=true) }}
+{% end %}
+
+{% subtree(name="Find me on the Web", open=true)%}
+{{ treelink(text="📧 E-Mail: soaos@soaos.dev", url="mailto:soaos@soaos.dev", rel="me") }}
+{{ treelink(text="🐘 Mastodon: soaos@furry.engineer", url="http://soaos.dog", rel="me", blank=true) }}
+{{ treelink(text="🦀 SSB: @Y1EKP4PU77qby4lI+m5MN6+NcYdjTdRQlV6NmluevuY=.ed25519", url="https://ssb.soaos.dev/~core/ssb/#@Y1EKP4PU77qby4lI+m5MN6+NcYdjTdRQlV6NmluevuY=.ed25519", rel="me", blank=true) }}
+{{ treelink(text="⬡ ListenBrainz: soaos", url="https://listenbrainz.org/user/soaos/", rel="me", blank=true) }}
+{% end %}
+
+{% end %}
+{% end %}
+{% end %}
diff --git a/content/blog/_index.md b/content/blog/_index.md
new file mode 100644
index 0000000..44bbfa0
--- /dev/null
+++ b/content/blog/_index.md
@@ -0,0 +1,6 @@
++++
+title = "soaos blog"
+template = "blog.html"
+page_template = "post.html"
+sort_by = "date"
++++ \ No newline at end of file
diff --git a/blog/blog.css b/content/blog/blog.css
index 372964d..372964d 100644
--- a/blog/blog.css
+++ b/content/blog/blog.css
diff --git a/content/blog/rockbox_stats/index.md b/content/blog/rockbox_stats/index.md
new file mode 100644
index 0000000..626cbe9
--- /dev/null
+++ b/content/blog/rockbox_stats/index.md
@@ -0,0 +1,547 @@
++++
+title = "Rockbox Stat Tracking"
+date = "2025-09-02"
++++
+
+<article>
+<section>
+<div class="text-section">
+<p>In this post I talk about how I went about setting up a <a href="/rockstats" target="_blank">stat visualization page</a> for my rockbox mp3 player.</p>
+</div>
+<figure class="cover-image">
+<svg width="100%" height="360" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" baseProfile="full" viewBox="0 0 640 360">
+<rect width="640" height="360" x="0" y="0" fill="rgb(0,0,0)" fill-opacity="0"></rect>
+<polyline points="427.2 233.9 440.6 240.7 455.6 240.7" fill="transparent" stroke="#e67e80" class="zr0-cls-0"></polyline>
+<polyline points="202.3 203.4 192.6 206.3 177.6 206.3" fill="transparent" stroke="#e69875" class="zr0-cls-0"></polyline>
+<polyline points="220 113.6 197.5 135.5 182.5 135.5" fill="transparent" stroke="#dbbc7f" class="zr0-cls-0"></polyline>
+<polyline points="273.2 69.5 190.3 117.4 190.3 117.4" fill="transparent" stroke="#a7c080" class="zr0-cls-0"></polyline>
+<polyline points="304.6 61 201.7 99.3 201.7 99.3" fill="transparent" stroke="#83c092" class="zr0-cls-0"></polyline>
+<polyline points="310.7 60.4 217.9 81.2 217.9 81.2" fill="transparent" stroke="#7fbbb3" class="zr0-cls-0"></polyline>
+<polyline points="315.4 60.1 242.2 63.1 242.2 63.1" fill="transparent" stroke="#d699b6" class="zr0-cls-0"></polyline>
+<polyline points="318.6 60 318.4 45 303.4 45" fill="transparent" stroke="#f85552" class="zr0-cls-0"></polyline>
+<path d="M320 60A120 120 0 1 1 223.6 251.5L320 180Z" fill="#e67e80" stroke="var(--bg0)" stroke-linejoin="round" ecmeta_series_index="0" ecmeta_data_index="0" ecmeta_ssr_type="chart" class="zr0-cls-1"></path>
+<path d="M223.6 251.5A120 120 0 0 1 203.6 150.7L320 180Z" fill="#e69875" stroke="var(--bg0)" stroke-linejoin="round" ecmeta_series_index="0" ecmeta_data_index="1" ecmeta_ssr_type="chart" class="zr0-cls-2"></path>
+<path d="M203.6 150.7A120 120 0 0 1 247.9 84.1L320 180Z" fill="#dbbc7f" stroke="var(--bg0)" stroke-linejoin="round" ecmeta_series_index="0" ecmeta_data_index="2" ecmeta_ssr_type="chart" class="zr0-cls-3"></path>
+<path d="M247.9 84.1A120 120 0 0 1 301.3 61.5L320 180Z" fill="#a7c080" stroke="var(--bg0)" stroke-linejoin="round" ecmeta_series_index="0" ecmeta_data_index="3" ecmeta_ssr_type="chart" class="zr0-cls-4"></path>
+<path d="M301.3 61.5A120 120 0 0 1 307.8 60.6L320 180Z" fill="#83c092" stroke="var(--bg0)" stroke-linejoin="round" ecmeta_series_index="0" ecmeta_data_index="4" ecmeta_ssr_type="chart" class="zr0-cls-5"></path>
+<path d="M307.8 60.6A120 120 0 0 1 313.6 60.2L320 180Z" fill="#7fbbb3" stroke="var(--bg0)" stroke-linejoin="round" ecmeta_series_index="0" ecmeta_data_index="5" ecmeta_ssr_type="chart" class="zr0-cls-6"></path>
+<path d="M313.6 60.2A120 120 0 0 1 317.2 60L320 180Z" fill="#d699b6" stroke="var(--bg0)" stroke-linejoin="round" ecmeta_series_index="0" ecmeta_data_index="6" ecmeta_ssr_type="chart" class="zr0-cls-7"></path>
+<path d="M317.2 60A120 120 0 0 1 320 60L320 180Z" fill="#f85552" stroke="var(--bg0)" stroke-linejoin="round" ecmeta_series_index="0" ecmeta_data_index="7" ecmeta_ssr_type="chart" class="zr0-cls-8"></path>
+<text dominant-baseline="central" text-anchor="start" style="font-size:16px;font-family:unifont;" xml:space="preserve" transform="translate(460.5907 240.6868)" fill="var(--fg)">Progressive Rock</text>
+<text dominant-baseline="central" text-anchor="end" style="font-size:16px;font-family:unifont;" transform="translate(172.5853 206.2935)" fill="var(--fg)">Alternative</text>
+<text dominant-baseline="central" text-anchor="end" style="font-size:16px;font-family:unifont;" transform="translate(177.5244 135.5094)" fill="var(--fg)">Post-Rock</text>
+<text dominant-baseline="central" text-anchor="end" style="font-size:16px;font-family:unifont;" transform="translate(185.3496 117.4094)" fill="var(--fg)">Post-Hardcore</text>
+<text dominant-baseline="central" text-anchor="end" style="font-size:16px;font-family:unifont;" transform="translate(196.7011 99.3094)" fill="var(--fg)">Post-Metal</text>
+<text dominant-baseline="central" text-anchor="end" style="font-size:16px;font-family:unifont;" transform="translate(212.8733 81.2094)" fill="var(--fg)">Rock</text>
+<text dominant-baseline="central" text-anchor="end" style="font-size:16px;font-family:unifont;" transform="translate(237.2337 63.1094)" fill="var(--fg)">Shoegaze</text>
+<text dominant-baseline="central" text-anchor="end" style="font-size:16px;font-family:unifont;" xml:space="preserve" transform="translate(298.4082 45.0094)" fill="var(--fg)">Progressive Metal</text>
+<style>
+.zr0-cls-0:hover {
+cursor:pointer;
+}
+.zr0-cls-1:hover {
+cursor:pointer;
+fill:rgba(253,138,140,1);
+}
+.zr0-cls-2:hover {
+cursor:pointer;
+fill:rgba(253,167,128,1);
+}
+.zr0-cls-3:hover {
+cursor:pointer;
+fill:rgba(240,206,139,1);
+}
+.zr0-cls-4:hover {
+cursor:pointer;
+fill:rgba(183,211,140,1);
+}
+.zr0-cls-5:hover {
+cursor:pointer;
+fill:rgba(144,211,160,1);
+}
+.zr0-cls-6:hover {
+cursor:pointer;
+fill:rgba(139,205,196,1);
+}
+.zr0-cls-7:hover {
+cursor:pointer;
+fill:rgba(235,168,200,1);
+}
+.zr0-cls-8:hover {
+cursor:pointer;
+fill:rgba(255,93,90,1);
+}
+</style>
+</svg>
+<figcaption>A static site generation experiment</figcaption>
+</figure>
+</section>
+<section class="text-section">
+ <h2>Preamble: Digital Sovereignity & Rockbox</h2>
+ <p>
+ I've been building up a pretty sizeable collection of digital music
+ over the last couple of years. I think there's a lot of value in owning
+ the music I pay for and being able to choose how I listen to it.
+ Purchasing music also allows me to support artists in a more direct
+ and substantial way than the fractions of cents for using streaming services,
+ but that's more of a happy consequence than some moral obligation I feel.
+ </p>
+ <p>
+ Over the years, I've enjoyed listening to my music in a variety of ways.
+ For years I kept all of my music files on all of my devices and used
+ various local music clients depending on the platform, most notably mpd
+ and ncmpcpp on linux. Eventually, as I charged headlong into the glorious
+ world of self-hosting, I began using a central Jellyfin media server that
+ I stream music and video from. It's super convenient, and works on all of
+ my devices (including my TV!).
+ </p>
+ <p>
+ My media server is great, and it's been the primary way I listen to music
+ for a while now. But it has limitations. For example, I don't expose my media
+ server to the internet, so I'm unable to stream from it while I'm out and
+ about. And even if I could, the bandwidth requirements would be pretty high.
+ I figured I would need a dedicated music player if I wanted to take my music
+ library on the go, and settled on the HIFI Walker H2 after reading some
+ online recommendations. The ability to install <a href="https://rockbox.org" target="_blank">Rockbox</a>, an open-source firmware,
+ was a big factor in my decision. I couldn't tell you how the device works
+ out of the box, since I flashed the firmware pretty much immediately once I got it,
+ but I've been super impressed with how the device works while running Rockbox.
+ </p>
+ <p>
+ <figure class="fig fig-right">
+ <img src="player.bmp" alt="Screenshot of Rockbox player showing cool theme.">
+ <figcaption>I'm using a modified version of the <a
+ href="https://themes.rockbox.org/index.php?themeid=3266&target=aigoerosq"
+ target="_blank">InfoMatrix-v2</a> theme, which looks great.</figcaption>
+ </figure>
+ Rockbox comes with many codecs for common audio formats including FLAC and MP3. The
+ device boots extremely quickly, and the interface is snappy. Virtually every aspect
+ of the user experience is tweakable and customizable to a crazy degree. I've even begun
+ listening to music on my player even at home, since a device specifically for the
+ purpose provides less distraction while I'm trying to be productive.
+ </p>
+ <p>
+ All this to say I'm pretty much sold on Rockbox. But there's certain things I
+ still miss from my days of being a user of popular services like Spotify with
+ fancy APIs and data tracking. Things like Spotify wrapped or third-party apps
+ for visualizing playback statistics are a fun way to see what my listening history
+ looks like and could potentially be used to help find more music that I'd enjoy.
+ This is why when I noticed that Rockbock has a playback logging feature, a little
+ lightbulb lit up over my head.
+ </p>
+</section>
+<section class="text-section">
+ <h2>Generating and Parsing Logs</h2>
+ <p>
+ <figure class="fig fig-right">
+ <img src="log-setting.bmp" alt="Logging">
+ <figcaption>The logging feature can be accessed through the settings menu.</figcaption>
+ </figure>
+ Rockbox has a feature that logs playback information to a text file. This feature can
+ be enabled by setting <b>Playback Settings > Logging</b> to "On". With this setting enabled, a
+ new line gets added to the end of the <b>.rockbox/playback.log</b> file every time you play a track,
+ containing info about what you played and when.
+ </p>
+ <p>
+ The logging feature is actually already used by the LastFM scrobbler plugin that comes preloaded with
+ Rockbox, which is probably the simplest way to get insights into your playback. However,
+ I personally want to avoid using third-party services as much as possible, because it's more fun.
+ </p>
+ <p>
+ If I take a look at a logfile generated after a bit of listening, I'll see that I've wound up with
+ a series of lines that each look something like this:
+ <figure class="fig">
+ <pre><samp>1758478258:336689:336718:/&lt;microSD0&gt;/Music/This Is The Glasshouse/This Is The Glasshouse - 867/This Is The Glasshouse - 867 - 01 Streetlight By Streetlight.flac</samp></pre>
+ <figcaption>An example of a log entry for "Streetlight by Streetlight" by This is the Glasshouse.
+ </figcaption>
+ </figure>
+ </p>
+ <p>
+ I wasn't really able to find any information online about the format of these logs, but they appear
+ to be simple enough to figure out. From what I can tell, each event is broken up into 4 pieces:
+ <ol>
+ <li><b>Timestamp:</b> The number of milliseconds since the UNIX epoch.
+ <li><b>Playback Duration:</b> The amount of the song that was played, in milliseconds.
+ <li><b>Total Track Length:</b> The length of the played track, in milliseconds.
+ <li><b>File Path:</b> An absolute path to the file containing the track on the filesystem.
+ </ol>
+ All of this is enough to know what I was listening to and when. I can use the file path to check for
+ audio tags which can help glean even more information about my listening habits.
+ </p>
+ <p>Now that I have this information and know how to interpret it, I'm ready to start processing it!</p>
+</section>
+<section class="text-section">
+ <h2>Analyzing Playback History</h2>
+ <p>
+ In order to get some useful information out of my playback history, I think it's a good idea to start by
+ building
+ a database. I created a sqlite database with the following tables:
+ <div class="sunken-panel" style="width: min-content;">
+ <table class="schema-table">
+ <thead>
+ <tr>
+ <th colspan="3">songs</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td>id</td>
+ <td>i64</td>
+ <td>PK</td>
+ </tr>
+ <tr>
+ <td>title</td>
+ <td>String</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>artists</td>
+ <td>JSON</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>album_id</td>
+ <td>i64?</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>genre</td>
+ <td>String?</td>
+ <td></td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ <div class="sunken-panel" style="width: min-content;">
+ <table class="schema-table">
+ <thead>
+ <tr>
+ <th colspan="3">albums</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td>id</td>
+ <td>i64</td>
+ <td>PK</td>
+ </tr>
+ <tr>
+ <td>title</td>
+ <td>String</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>artist</td>
+ <td>String</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>cover_art</td>
+ <td>Blob?</td>
+ <td></td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ <div class="sunken-panel" style="width: min-content;">
+ <table class="schema-table">
+ <thead>
+ <tr>
+ <th colspan="3">history</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td>id</td>
+ <td>i64</td>
+ <td>PK</td>
+ </tr>
+ <tr>
+ <td>timestamp</td>
+ <td>Datetime</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>duration</td>
+ <td>i64</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>song_id</td>
+ <td>i64</td>
+ <td></td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ <br>
+ I can add more columns later, but this is a good place to start.
+ </p>
+ <p>
+ Now, as I read through the logfile line-by-line, I can check if each album exists before
+ inserting it into the database:
+ <figure class="fig">
+ <pre><code>for line in log_file.lines().flatten() {
+println!("{line}");
+// Skip comments
+if line.starts_with("#") {
+continue;
+}
+let chunks = line.split(":").collect::<Vec<_>>();
+
+let timestamp = DateTime::from_timestamp_secs(
+i64::from_str_radix(chunks[0], 10).context("Failed to parse timestamp")?,
+)
+.context("Failed to convert timestamp")?;
+
+// Load tags from file on device
+let file_path = chunks[chunks.len() - 1][1..]
+.split_once("/")
+.context("Missing file")?
+.1;
+let tags = Tag::new()
+.read_from_path(args.mount_point.join(file_path))
+.context("Failed to read audio tags")?;
+
+//...
+}</code></pre>
+ <figcaption>Parsing log entry and loading audio metadata.</figcaption>
+ </figure>
+ <figure class="fig">
+ <pre><code>if let Some(existing_album) =
+sqlx::query("SELECT id FROM albums WHERE title=$1 AND artist=$2")
+.bind(album_title)
+.bind(album_artist)
+.fetch_optional(&mut *db)
+.await
+.context("Failed to execute query to find existing album")?
+{
+let album_id: i64 = existing_album.get("id");
+info!("Album already exists, id {album_id}");
+//...
+} else {
+info!("Inserting new album: {album_title} by {album_artist}");
+//...
+let result = sqlx::query(
+"INSERT INTO albums (title, artist, cover_art) VALUES ($1, $2, $3);",
+)
+.bind(album_title)
+.bind(album_artist)
+.bind(cover)
+.execute(&mut *db)
+.await
+.context("Failed to execute query to insert album into database")?;
+
+//...
+}</code></pre>
+ <figcaption>Checking for an album with matching artist and title before creating a new row in the
+ database.</figcaption>
+ </figure>
+ I did something similar with the <b>songs</b> and <b>history</b> tables, basically building up a cache
+ of history information and skipping anything that's already in the database on repeat runs.
+ </p>
+ <p>
+ With this database constructed, it's pretty easy to get a bunch of different information
+ about my listening. For example (forgive me if my SQL skills are kind of ass lol):
+ <figure class="fig">
+ <pre><code>SELECT
+songs.title AS song_title,
+songs.artists AS song_artists,
+songs.genre AS song_genre,
+albums.title AS album_title,
+albums.artist AS album_artist,
+history.timestamp AS timestamp,
+history.duration AS duration
+FROM history
+CROSS JOIN songs ON songs.id = history.song_id
+CROSS JOIN albums ON albums.id = songs.album_id
+ORDER BY timestamp DESC;</code></pre>
+ <figcaption>Querying for a list of each history entry along with track metadata, sorted from most to
+ least recent.</figcaption>
+ </figure>
+ <figure class="fig">
+ <pre><code>SELECT
+songs.genre,
+SUM(history.duration) AS total_duration
+FROM history
+CROSS JOIN songs ON history.song_id = songs.id
+GROUP BY genre
+ORDER BY total_duration DESC
+LIMIT 10; </code></pre>
+ <figcaption>Querying for the top 10 most listened genres by playtime.</figcaption>
+ </figure>
+ </p>
+ <p>
+ It's all well and good to be able to view this information using a database client,
+ but it would be really cool if I could visualize this data somehow.
+ </p>
+</section>
+<section class="text-section">
+ <h2>Visualizing this Data Somehow</h2>
+ <p>
+ I wanted to make this data available on my website for people to view, and for a bunch of mostly trivial
+ reasons I won't get into here, I have a couple of requirements for pages on this site:
+ <ol>
+ <li>Pages need to be static.
+ <li>Pages need to be JavaScript-free.
+ </ol>
+ This means any chart rendering needs to be done automatically at build time before
+ deploying. I don't currently use a static site generator for my site (just for fun),
+ so I'm basically going to need to write one specifically to generate this page.
+ </p>
+ <p>
+ I won't get too deep into the specifics of how I queried the database and generated each visualization
+ on
+ the page, but I can explain the visualizations I created using the queries from the previous section.
+ For the
+ listening history I wanted to generate a table displaying the information. To accomplish this, I first
+ used a combination of <a href="https://crates.io/crates/sqlx" target="_blank">sqlx</a>'s ability to convert a row to a struct and <a href="https://crates.io/crates/serde" target="_blank">serde</a> to serialize
+ the rows as JSON values.
+ <figure class="fig">
+ <pre><code>#[derive(Serialize, Deserialize, FromRow)]
+struct HistoryEntry {
+song_title: String,
+song_artists: Value,
+timestamp: DateTime&lt;Utc&gt;,
+duration: i64,
+album_title: String,
+album_artist: Option&lt;String&gt;,
+song_genre: Option&lt;String&gt;,
+}
+
+//...later
+let history = sqlx::query_as::&lt;_, HistoryEntry&gt;(
+/* SELECT... */
+).fetch_all(&mut *db).await;
+
+//...later still, tera context accepts
+let mut context = tera::Context::new();
+context.insert("history", &history);
+</code></pre>
+ <figcaption>Struct definition for a history entry, allowing conversion from a sqlx row and
+ de/serialization from/to JSON.</figcaption>
+ </figure>
+ </p>
+ <p>
+ In order to keep the generation as painless as possible, I decided to use the <a href="https://keats.github.io/tera" target="_blank">Tera</a> template
+ engine, which allows me to define a template HTML file and substitute in values from
+ a context which I can define before rendering. In the case of the table, I can just generate a <code>&lt;tr&gt;</code>
+ matching the data for each item:
+ <figure class="fig">
+ <pre><code>{% macro history_table(history) %}
+&lt;h3&gt;Playback History&lt;/h3&gt;
+&lt;div class=&quot;table-container&quot;&gt;
+&lt;table&gt;
+&lt;thead&gt;
+ &lt;tr&gt;
+ &lt;th&gt;Timestamp&lt;/th&gt;
+ &lt;th&gt;Played Duration&lt;/th&gt;
+ &lt;th&gt;Title&lt;/th&gt;
+ &lt;th&gt;Artists&lt;/th&gt;
+ &lt;th&gt;Album&lt;/th&gt;
+ &lt;th&gt;Genre&lt;/th&gt;
+ &lt;/tr&gt;
+&lt;/thead&gt;
+&lt;tbody&gt;
+ {% for item in history %}&lt;tr&gt;
+ &lt;td&gt;{{ item.timestamp | date(format=&quot;%Y-%m-%d %H:%M:%S&quot;) }}&lt;/td&gt;
+ &lt;td&gt;{{ item.duration | hms }}&lt;/td&gt;
+ &lt;td&gt;{{ item.song_title }}&lt;/td&gt;
+ &lt;td&gt;{{ item.song_artists }}&lt;/td&gt;
+ &lt;td&gt;{{ item.album_title }}&lt;/td&gt;
+ &lt;td&gt;{{ item.song_genre }}&lt;/td&gt;
+ &lt;/tr&gt;
+ {% endfor %}
+&lt;/tbody&gt;
+&lt;/table&gt;
+&lt;/div&gt;
+{% endmacro history_table %}</code></pre>
+ <figcaption>
+ A Tera macro for generating a table from a list of playback history items.
+ I used a macro so I can re-use this later if I want to add time range views.
+ (last month, year, etc.)
+ </figcaption>
+ </figure>
+ </p>
+ <p>
+ I wrote similar macros for each of the visualizations I wanted to create. Most are
+ easy, but for my top 10 genres I wanted to display a pie chart. I found a pretty decent
+ data visualization crate called <a href="https://crates.io/crates/charming" target="_blank">charming</a> that's able to render to html, however
+ the output contains javascript so it's a no-go for me. Luckily, it can also render to
+ an SVG which I can embed nicely within the page.
+ <figure class="fig">
+<svg width="100%" height="360" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" baseProfile="full" viewBox="0 0 640 360">
+<rect width="640" height="360" x="0" y="0" fill="rgb(0,0,0)" fill-opacity="0"></rect>
+<polyline points="427.2 233.9 440.6 240.7 455.6 240.7" fill="transparent" stroke="#e67e80" class="zr0-cls-0"></polyline>
+<polyline points="202.3 203.4 192.6 206.3 177.6 206.3" fill="transparent" stroke="#e69875" class="zr0-cls-0"></polyline>
+<polyline points="220 113.6 197.5 135.5 182.5 135.5" fill="transparent" stroke="#dbbc7f" class="zr0-cls-0"></polyline>
+<polyline points="273.2 69.5 190.3 117.4 190.3 117.4" fill="transparent" stroke="#a7c080" class="zr0-cls-0"></polyline>
+<polyline points="304.6 61 201.7 99.3 201.7 99.3" fill="transparent" stroke="#83c092" class="zr0-cls-0"></polyline>
+<polyline points="310.7 60.4 217.9 81.2 217.9 81.2" fill="transparent" stroke="#7fbbb3" class="zr0-cls-0"></polyline>
+<polyline points="315.4 60.1 242.2 63.1 242.2 63.1" fill="transparent" stroke="#d699b6" class="zr0-cls-0"></polyline>
+<polyline points="318.6 60 318.4 45 303.4 45" fill="transparent" stroke="#f85552" class="zr0-cls-0"></polyline>
+<path d="M320 60A120 120 0 1 1 223.6 251.5L320 180Z" fill="#e67e80" stroke="var(--bg0)" stroke-linejoin="round" ecmeta_series_index="0" ecmeta_data_index="0" ecmeta_ssr_type="chart" class="zr0-cls-1"></path>
+<path d="M223.6 251.5A120 120 0 0 1 203.6 150.7L320 180Z" fill="#e69875" stroke="var(--bg0)" stroke-linejoin="round" ecmeta_series_index="0" ecmeta_data_index="1" ecmeta_ssr_type="chart" class="zr0-cls-2"></path>
+<path d="M203.6 150.7A120 120 0 0 1 247.9 84.1L320 180Z" fill="#dbbc7f" stroke="var(--bg0)" stroke-linejoin="round" ecmeta_series_index="0" ecmeta_data_index="2" ecmeta_ssr_type="chart" class="zr0-cls-3"></path>
+<path d="M247.9 84.1A120 120 0 0 1 301.3 61.5L320 180Z" fill="#a7c080" stroke="var(--bg0)" stroke-linejoin="round" ecmeta_series_index="0" ecmeta_data_index="3" ecmeta_ssr_type="chart" class="zr0-cls-4"></path>
+<path d="M301.3 61.5A120 120 0 0 1 307.8 60.6L320 180Z" fill="#83c092" stroke="var(--bg0)" stroke-linejoin="round" ecmeta_series_index="0" ecmeta_data_index="4" ecmeta_ssr_type="chart" class="zr0-cls-5"></path>
+<path d="M307.8 60.6A120 120 0 0 1 313.6 60.2L320 180Z" fill="#7fbbb3" stroke="var(--bg0)" stroke-linejoin="round" ecmeta_series_index="0" ecmeta_data_index="5" ecmeta_ssr_type="chart" class="zr0-cls-6"></path>
+<path d="M313.6 60.2A120 120 0 0 1 317.2 60L320 180Z" fill="#d699b6" stroke="var(--bg0)" stroke-linejoin="round" ecmeta_series_index="0" ecmeta_data_index="6" ecmeta_ssr_type="chart" class="zr0-cls-7"></path>
+<path d="M317.2 60A120 120 0 0 1 320 60L320 180Z" fill="#f85552" stroke="var(--bg0)" stroke-linejoin="round" ecmeta_series_index="0" ecmeta_data_index="7" ecmeta_ssr_type="chart" class="zr0-cls-8"></path>
+<text dominant-baseline="central" text-anchor="start" style="font-size:16px;font-family:unifont;" xml:space="preserve" transform="translate(460.5907 240.6868)" fill="var(--fg)">Progressive Rock</text>
+<text dominant-baseline="central" text-anchor="end" style="font-size:16px;font-family:unifont;" transform="translate(172.5853 206.2935)" fill="var(--fg)">Alternative</text>
+<text dominant-baseline="central" text-anchor="end" style="font-size:16px;font-family:unifont;" transform="translate(177.5244 135.5094)" fill="var(--fg)">Post-Rock</text>
+<text dominant-baseline="central" text-anchor="end" style="font-size:16px;font-family:unifont;" transform="translate(185.3496 117.4094)" fill="var(--fg)">Post-Hardcore</text>
+<text dominant-baseline="central" text-anchor="end" style="font-size:16px;font-family:unifont;" transform="translate(196.7011 99.3094)" fill="var(--fg)">Post-Metal</text>
+<text dominant-baseline="central" text-anchor="end" style="font-size:16px;font-family:unifont;" transform="translate(212.8733 81.2094)" fill="var(--fg)">Rock</text>
+<text dominant-baseline="central" text-anchor="end" style="font-size:16px;font-family:unifont;" transform="translate(237.2337 63.1094)" fill="var(--fg)">Shoegaze</text>
+<text dominant-baseline="central" text-anchor="end" style="font-size:16px;font-family:unifont;" xml:space="preserve" transform="translate(298.4082 45.0094)" fill="var(--fg)">Progressive Metal</text>
+<style>
+.zr0-cls-0:hover {
+cursor:pointer;
+}
+.zr0-cls-1:hover {
+cursor:pointer;
+fill:rgba(253,138,140,1);
+}
+.zr0-cls-2:hover {
+cursor:pointer;
+fill:rgba(253,167,128,1);
+}
+.zr0-cls-3:hover {
+cursor:pointer;
+fill:rgba(240,206,139,1);
+}
+.zr0-cls-4:hover {
+cursor:pointer;
+fill:rgba(183,211,140,1);
+}
+.zr0-cls-5:hover {
+cursor:pointer;
+fill:rgba(144,211,160,1);
+}
+.zr0-cls-6:hover {
+cursor:pointer;
+fill:rgba(139,205,196,1);
+}
+.zr0-cls-7:hover {
+cursor:pointer;
+fill:rgba(235,168,200,1);
+}
+.zr0-cls-8:hover {
+cursor:pointer;
+fill:rgba(255,93,90,1);
+}
+
+
+
+</style>
+</svg>
+ <figcaption>Here's one I generated just now.</figcaption>
+ </figure>
+ </p>
+ <p>
+ And that's pretty much all there is to it! The finished thing can be found <a href="/rockstats" target="_blank">here</a>.
+ </p>
+</section>
+</article> \ No newline at end of file
diff --git a/blog/rockbox_stats/log-setting.bmp b/content/blog/rockbox_stats/log-setting.bmp
index 29789fa..29789fa 100644
--- a/blog/rockbox_stats/log-setting.bmp
+++ b/content/blog/rockbox_stats/log-setting.bmp
Binary files differ
diff --git a/blog/rockbox_stats/playback-settings.bmp b/content/blog/rockbox_stats/playback-settings.bmp
index cee3cfb..cee3cfb 100644
--- a/blog/rockbox_stats/playback-settings.bmp
+++ b/content/blog/rockbox_stats/playback-settings.bmp
Binary files differ
diff --git a/blog/rockbox_stats/player.bmp b/content/blog/rockbox_stats/player.bmp
index 452a057..452a057 100644
--- a/blog/rockbox_stats/player.bmp
+++ b/content/blog/rockbox_stats/player.bmp
Binary files differ
diff --git a/blog/terminal_renderer_mkii/cover.png b/content/blog/terminal_renderer_mkii/cover.png
index b3ddfd9..b3ddfd9 100644
--- a/blog/terminal_renderer_mkii/cover.png
+++ b/content/blog/terminal_renderer_mkii/cover.png
Binary files differ
diff --git a/blog/terminal_renderer_mkii/david.png b/content/blog/terminal_renderer_mkii/david.png
index 6cfa884..6cfa884 100644
--- a/blog/terminal_renderer_mkii/david.png
+++ b/content/blog/terminal_renderer_mkii/david.png
Binary files differ
diff --git a/blog/terminal_renderer_mkii/davidbayer.png b/content/blog/terminal_renderer_mkii/davidbayer.png
index af4bfc4..af4bfc4 100644
--- a/blog/terminal_renderer_mkii/davidbayer.png
+++ b/content/blog/terminal_renderer_mkii/davidbayer.png
Binary files differ
diff --git a/blog/terminal_renderer_mkii/davidthreshold.png b/content/blog/terminal_renderer_mkii/davidthreshold.png
index 6c6e014..6c6e014 100644
--- a/blog/terminal_renderer_mkii/davidthreshold.png
+++ b/content/blog/terminal_renderer_mkii/davidthreshold.png
Binary files differ
diff --git a/content/blog/terminal_renderer_mkii/index.md b/content/blog/terminal_renderer_mkii/index.md
new file mode 100644
index 0000000..1797a4a
--- /dev/null
+++ b/content/blog/terminal_renderer_mkii/index.md
@@ -0,0 +1,167 @@
++++
+title = "Terminal Renderer Mk. II - Rendering to Text with Compute"
+date = "2025-10-02"
++++
+<section>
+<div class="text-section">
+<p>This week I brought my terminal renderer to the next level by performing text rendering on the GPU.
+</p>
+</div>
+<figure class="cover-image">
+<img src="cover.png" alt="" style="width:100%;">
+ <figcaption>The Stanford Dragon, outlined and rendered as Braille characters in a terminal emulator. <a href="https://tv.soaos.dev/w/fBnDAUPsTPHaoPeNNxBGch" target="_blank">
+Full video</a>
+</figcaption>
+</figure>
+</section>
+<section class="text-section">
+<h2>Context</h2>
+<h3>Unicode Braille</h3>
+<p>
+I first messed around with rendering images to the terminal with Braille characters in like 2022 I
+think? I wrote a simple CLI tool
+that applied a threshold to an input image and output it as Braille characters in the terminal. <a
+ href="https://tv.soaos.dev/w/twpHAu4Jv8LJc9YjZbfw5g" target="_blank">Here's a recording I took back
+ when I did it.</a>
+</p>
+
+<p>
+<figure class="fig fig-right">
+<div class="centered">
+ <table class="schema-table">
+ <tbody>
+ <tr>
+ <td>0</td>
+ <td>3</td>
+ </tr>
+ <tr>
+ <td>1</td>
+ <td>4</td>
+ </tr>
+ <tr>
+ <td>2</td>
+ <td>5</td>
+ </tr>
+ <tr>
+ <td>6</td>
+ <td>7</td>
+ </tr>
+ </tbody>
+ </table>
+</div>
+<figcaption>The corresponding bit position for each braille dot.</figcaption>
+</figure>
+This effect is pretty cool, and it was pretty easy to implement as well. The trick lies in how the
+<a href="https://en.wikipedia.org/wiki/Braille_Patterns#Block" target="_blank">Unicode Braille block</a>
+is laid out. Every 8-dot Braille combination happens to add up to 256 combinations, the perfect amount to
+fit in the range between <code>0x2800</code> (⠀) and <code>0x28FF</code> (⣿). In other words, every
+character
+within the block can be represented by changing the value of a single byte.
+</p>
+<p>
+The lowest 6 bits of the pattern map on to a 6-dot braille pattern. However, due
+to historical reasons the 8-dot values were tacked on after the fact, which adds
+a slightly annoying mapping to the conversion process. Either way, it's a lot easier
+than it could be to just read a pixel value, check its brightness, and then use a
+bitwise operation to set/clear a dot.
+</p>
+<h3>Ordered Dithering</h3>
+<p>
+Comparing the brightnes of a pixel against a constant threshold is a fine way to
+display black and white images, but it's far from ideal and often results in the loss
+of a lot of detail from the original image.
+</p>
+<figure class="fig fig-horizontal">
+<div class="horizontal-container">
+ <img src="david.png" alt="" />
+ <img src="davidthreshold.png" alt="" />
+ <img src="davidbayer.png" alt="" />
+</div>
+<figcaption>From left to right: Original image, threshold, and ordered dither. <a
+ href="https://en.wikipedia.org/wiki/Dither" target="_blank">Wikipedia</a></figcaption>
+</figure>
+<p>By using <a href="https://en.wikipedia.org/wiki/Ordered_dithering" target="_blank">ordered dithering</a>,
+we
+can preserve much more of the subtleties of the original image. While not the "truest" version of
+dithering possible,
+ordered dithering (and <i>Bayer</i> dithering in particular) provides a few advantages that make it very
+well suited to realtime computer graphics:
+<ul>
+<li>Each pixel is dithered independent of any other pixel in the image, making it extremely
+ parallelizable and good for shaders.</li>
+<li>It's visually stable, changes to one part of the image won't disturb other areas.</li>
+<li>It's dead simple.</li>
+</ul>
+Feel free to read up on the specifics of threshold maps and stuff, but for the purposes of this little
+explanation it's
+enough to know that it's basically just a matrix of 𝓃⨉𝓃 values between 0 and 1, and then to determine
+whether a pixel (𝓍,𝓎)
+is white or black, you check the brightness against the threshold value at (𝓍%𝓃,𝓎%𝓃) in the map.
+</p>
+</section>
+<section class="text-section">
+<h2>The old way™</h2>
+<p>
+My first attempt at <i>realtime</i> terminal graphics with ordered dithering
+(<a href="https://tv.soaos.dev/w/dzHBnPJXtDBwtSvirgwTvY" target="_blank">I put a video up at the time</a>)
+ran entirely on the CPU. I pre-calculated the threshold map at the beginning of execution and ran each
+frame
+through a sequential function to dither it and convert it to Braille characters.
+</p>
+<p>
+To be honest, I never noticed
+any significant performance issues doing this, as you can imagine the image size required to fill a
+terminal
+screen is signficantly smaller than a normal window. However, I knew I could easily perform the
+dithering on the GPU
+as a post-processing effect, so I eventually wrote a shader to do that. In combination with another
+effect I used to
+add outlines to objects, I was able to significantly improve the visual fidelity of the experience. A
+good example of
+where the renderer was at until like a week ago can be seen in <a
+ href="https://tv.soaos.dev/w/9Pf2tP3PYY5pJ3Cimhqs9x" target="_blank">this video</a>.
+</p>
+<p>
+Until now I hadn't really considered moving the text conversion to the GPU. I mean, <i>G</i>PU is for
+graphics,
+right? I just copied the <i>entire framebuffer</i> back onto the CPU after dithering
+and used the same sequential conversion algorithm. Then I had an idea that would drastically reduce the
+amount
+of copying necessary.
+</p>
+</section>
+<section class="text-section">
+<h2>Compute post-processing</h2>
+<p>
+What if, instead of extracting and copying the framebuffer every single frame, we "rendered" the text on
+the GPU
+and read <i>that</i> back instead? Assuming each pixel in a texture is 32 bits (RGBA8), and knowing that
+each braille
+character is a block of 8 pixels, could we not theoretically shave off <i>at least</i> 7/8 of the bytes
+copied?
+</p>
+<p>
+As it turns out, it's remarkably easy to do. I'm using the <a href="https://bevy.org"
+ target="_blank">Bevy engine</a>,
+and hooking in a compute node to my existing post-processing render pipeline worked right out of the
+box.
+I allocated a storage buffer large enough to hold the necessary amount of characters, read it back each
+frame, and dumped
+the contents into the terminal.
+</p>
+<p>
+I used UTF-32 encoding on the storage buffer because I knew I could easily convert a "wide string" into
+UTF-8 before printing it, and
+32 bits provides a consistent space to fill for each workgroup in the shader versus a variable-length
+ encoding like UTF-8. <a href="https://tv.soaos.dev/w/fBnDAUPsTPHaoPeNNxBGch" target="_blank">Here's a video of the new renderer working</a>.
+Although now that I think about it, I could probably switch to using UTF-16 since all the Braille
+characters could be represented
+in 2 bytes, and that would be half the size of the UTF-32 text, which is half empty bytes anyways.
+</p>
+<p>
+Okay so I went and tried that but remembered that shaders only accept 32-bit primitive types, so it doesn't matter anyways. This little side quest has been a part of my
+broader efforts to revive a project I
+spent a lot of time on. I'm taking the opportunity to really dig in and rework some of the stuff I'm not
+totally happy with. So there might be quite a few of this kind of post in the near future. Stay tuned.
+</p>
+</section>
diff --git a/content/heaven/_index.md b/content/heaven/_index.md
new file mode 100644
index 0000000..ce3b970
--- /dev/null
+++ b/content/heaven/_index.md
@@ -0,0 +1,4 @@
++++
+title = "heaven"
+template = "heaven.html"
++++ \ No newline at end of file
diff --git a/heaven/angel.gif b/content/heaven/angel.gif
index 1c591e8..1c591e8 100644
--- a/heaven/angel.gif
+++ b/content/heaven/angel.gif
Binary files differ
diff --git a/heaven/angel2.gif b/content/heaven/angel2.gif
index 54adc92..54adc92 100644
--- a/heaven/angel2.gif
+++ b/content/heaven/angel2.gif
Binary files differ
diff --git a/heaven/angel3.gif b/content/heaven/angel3.gif
index e6aedcb..e6aedcb 100644
--- a/heaven/angel3.gif
+++ b/content/heaven/angel3.gif
Binary files differ
diff --git a/heaven/bg.jpg b/content/heaven/bg.jpg
index 91a9238..91a9238 100644
--- a/heaven/bg.jpg
+++ b/content/heaven/bg.jpg
Binary files differ
diff --git a/heaven/everytime_we_touch_nightcore.ogg b/content/heaven/everytime_we_touch_nightcore.ogg
index 3ac875d..3ac875d 100644
--- a/heaven/everytime_we_touch_nightcore.ogg
+++ b/content/heaven/everytime_we_touch_nightcore.ogg
Binary files differ
diff --git a/heaven/heaven.css b/content/heaven/heaven.css
index 5968745..5968745 100644
--- a/heaven/heaven.css
+++ b/content/heaven/heaven.css
diff --git a/hell/Flying_Skeleton_Hell.gif b/content/hell/Flying_Skeleton_Hell.gif
index 7cfd429..7cfd429 100644
--- a/hell/Flying_Skeleton_Hell.gif
+++ b/content/hell/Flying_Skeleton_Hell.gif
Binary files differ
diff --git a/content/hell/_index.md b/content/hell/_index.md
new file mode 100644
index 0000000..310e63e
--- /dev/null
+++ b/content/hell/_index.md
@@ -0,0 +1,70 @@
++++
+title = "soaos"
+template = "hell.html"
++++
+
+<div>
+<details class="evil centered">
+<summary>
+ <img src="/hell/hot.gif" /> (please enable autoplay to hear music lol)
+</summary>
+<audio autoplay loop controls>
+ <source src="/hell/hell.ogg" />
+</audio>
+</details>
+<h1 class="evil centered">👿 WELLCOME INTO HELL... YOU SUCKER!!!!! 👿</h1>
+<div class="center-content">
+<img src="/hell/firebreak.gif" />
+</div>
+<div class="center-content">
+<div class="column">
+ <section style="max-width: 640px; position: relative; margin: 4em">
+ <img src="/hell/bigguy.gif" style="float: right" />
+ <p class="evil bold">
+ Hey there pal. It's me haha... the "big" "guy"... AKA satan... I
+ hope you like it here in hell, i worked hard on making it evil :D
+ </p>
+ <p class="evil bold">
+ idk what you did to wind up here, but now you're stuck here...
+ forever... with me haha ;)
+ </p>
+ <p class="evil bold">
+ so... Make yourself comfortable... haha if you can lol >:]
+ </p>
+ <p class="evil bold">
+ AND DON'T EFFING TRY ESCAPING!!! }:[ alright lucifer out
+ </p>
+<a
+ id="inverted-cross"
+ class="huge"
+ style="position: absolute; bottom: -0.5em; right: -0.5em"
+ href="/heaven"
+ >✝</a
+>
+</section>
+<img src="/hell/firebreak.gif" />
+<div style="position: relative">
+<img src="/hell/Flying_Skeleton_Hell.gif" />
+<img
+ src="/hell/hellisreal.gif"
+ style="position: absolute; top: 0; left: -200px; rotate: -15deg"
+/>
+</div>
+</div>
+</div>
+<div>
+<img src="/hell/demon.gif" />
+<img src="/hell/demon2.gif" />
+<img src="/hell/demon3.gif" />
+<img src="/hell/demon4.gif" />
+<img src="/hell/demon_face.gif" />
+<img src="/hell/skull.gif" />
+<img src="/hell/evilorb.gif" />
+<img src="/hell/pitchfork.gif" />
+<img src="/hell/smallfire.gif" />
+<img src="/hell/evilmind.gif" />
+</div>
+<div class="huge evil relative">
+<s class="struck">F*CK</s>SCREW YOU, SUCKER 💔🥀
+</div>
+</div>
diff --git a/hell/bg.jpg b/content/hell/bg.jpg
index 8d0fad5..8d0fad5 100644
--- a/hell/bg.jpg
+++ b/content/hell/bg.jpg
Binary files differ
diff --git a/hell/bigguy.gif b/content/hell/bigguy.gif
index c3314c7..c3314c7 100644
--- a/hell/bigguy.gif
+++ b/content/hell/bigguy.gif
Binary files differ
diff --git a/hell/comunismo.gif b/content/hell/comunismo.gif
index 18da593..18da593 100644
--- a/hell/comunismo.gif
+++ b/content/hell/comunismo.gif
Binary files differ
diff --git a/hell/demon.gif b/content/hell/demon.gif
index a5868d2..a5868d2 100644
--- a/hell/demon.gif
+++ b/content/hell/demon.gif
Binary files differ
diff --git a/hell/demon2.gif b/content/hell/demon2.gif
index ebbd919..ebbd919 100644
--- a/hell/demon2.gif
+++ b/content/hell/demon2.gif
Binary files differ
diff --git a/hell/demon3.gif b/content/hell/demon3.gif
index fc73814..fc73814 100644
--- a/hell/demon3.gif
+++ b/content/hell/demon3.gif
Binary files differ
diff --git a/hell/demon4.gif b/content/hell/demon4.gif
index debe626..debe626 100644
--- a/hell/demon4.gif
+++ b/content/hell/demon4.gif
Binary files differ
diff --git a/hell/demon_face.gif b/content/hell/demon_face.gif
index 5c71e60..5c71e60 100644
--- a/hell/demon_face.gif
+++ b/content/hell/demon_face.gif
Binary files differ
diff --git a/hell/evilmind.gif b/content/hell/evilmind.gif
index 1b6cb9b..1b6cb9b 100644
--- a/hell/evilmind.gif
+++ b/content/hell/evilmind.gif
Binary files differ
diff --git a/hell/evilorb.gif b/content/hell/evilorb.gif
index 003ef97..003ef97 100644
--- a/hell/evilorb.gif
+++ b/content/hell/evilorb.gif
Binary files differ
diff --git a/hell/firebreak.gif b/content/hell/firebreak.gif
index 981f1bf..981f1bf 100644
--- a/hell/firebreak.gif
+++ b/content/hell/firebreak.gif
Binary files differ
diff --git a/hell/gay.gif b/content/hell/gay.gif
index 156bbea..156bbea 100644
--- a/hell/gay.gif
+++ b/content/hell/gay.gif
Binary files differ
diff --git a/hell/gay2.gif b/content/hell/gay2.gif
index 1a00852..1a00852 100644
--- a/hell/gay2.gif
+++ b/content/hell/gay2.gif
Binary files differ
diff --git a/hell/gaydudes.gif b/content/hell/gaydudes.gif
index 65c4ed3..65c4ed3 100644
--- a/hell/gaydudes.gif
+++ b/content/hell/gaydudes.gif
Binary files differ
diff --git a/hell/hell.ogg b/content/hell/hell.ogg
index 0abf641..0abf641 100644
--- a/hell/hell.ogg
+++ b/content/hell/hell.ogg
Binary files differ
diff --git a/hell/hellisreal.gif b/content/hell/hellisreal.gif
index acbd894..acbd894 100644
--- a/hell/hellisreal.gif
+++ b/content/hell/hellisreal.gif
Binary files differ
diff --git a/hell/hitler.gif b/content/hell/hitler.gif
index 4edc5d8..4edc5d8 100644
--- a/hell/hitler.gif
+++ b/content/hell/hitler.gif
Binary files differ
diff --git a/hell/hitler2.gif b/content/hell/hitler2.gif
index 7fd7132..7fd7132 100644
--- a/hell/hitler2.gif
+++ b/content/hell/hitler2.gif
Binary files differ
diff --git a/hell/hitler3.gif b/content/hell/hitler3.gif
index 8edd36c..8edd36c 100644
--- a/hell/hitler3.gif
+++ b/content/hell/hitler3.gif
Binary files differ
diff --git a/hell/hot.gif b/content/hell/hot.gif
index 4c1660f..4c1660f 100644
--- a/hell/hot.gif
+++ b/content/hell/hot.gif
Binary files differ
diff --git a/hell/kissing.jpg b/content/hell/kissing.jpg
index b6190c6..b6190c6 100644
--- a/hell/kissing.jpg
+++ b/content/hell/kissing.jpg
Binary files differ
diff --git a/hell/obama.gif b/content/hell/obama.gif
index 4f42d27..4f42d27 100644
--- a/hell/obama.gif
+++ b/content/hell/obama.gif
Binary files differ
diff --git a/hell/pitchfork.gif b/content/hell/pitchfork.gif
index ab880fa..ab880fa 100644
--- a/hell/pitchfork.gif
+++ b/content/hell/pitchfork.gif
Binary files differ
diff --git a/hell/redfire.gif b/content/hell/redfire.gif
index 8f15a7a..8f15a7a 100644
--- a/hell/redfire.gif
+++ b/content/hell/redfire.gif
Binary files differ
diff --git a/hell/skull.gif b/content/hell/skull.gif
index 89ed718..89ed718 100644
--- a/hell/skull.gif
+++ b/content/hell/skull.gif
Binary files differ
diff --git a/hell/smallfire.gif b/content/hell/smallfire.gif
index 7b29fdf..7b29fdf 100644
--- a/hell/smallfire.gif
+++ b/content/hell/smallfire.gif
Binary files differ
diff --git a/hell/torch.gif b/content/hell/torch.gif
index c06066b..c06066b 100644
--- a/hell/torch.gif
+++ b/content/hell/torch.gif
Binary files differ
diff --git a/projects/bevy_plugins/index.html b/content/projects/bevy_plugins/index.html
index 5f5c427..5f5c427 100644
--- a/projects/bevy_plugins/index.html
+++ b/content/projects/bevy_plugins/index.html
diff --git a/projects/games/NIX_AVREA/index.html b/content/projects/games/NIX_AVREA/index.html
index 47c914b..47c914b 100644
--- a/projects/games/NIX_AVREA/index.html
+++ b/content/projects/games/NIX_AVREA/index.html
diff --git a/projects/piss_daemon/index.html b/content/projects/piss_daemon/index.html
index 5cb9481..5cb9481 100644
--- a/projects/piss_daemon/index.html
+++ b/content/projects/piss_daemon/index.html
diff --git a/projects/piss_daemon/statusbar.png b/content/projects/piss_daemon/statusbar.png
index b98e021..b98e021 100644
--- a/projects/piss_daemon/statusbar.png
+++ b/content/projects/piss_daemon/statusbar.png
Binary files differ
diff --git a/projects/project.css b/content/projects/project.css
index e69de29..e69de29 100644
--- a/projects/project.css
+++ b/content/projects/project.css
diff --git a/rockstats/index.html b/content/rockstats/index.html
index cfd916c..cfd916c 100644
--- a/rockstats/index.html
+++ b/content/rockstats/index.html
diff --git a/things_i_like/music/867.png b/content/things_i_like/music/867.png
index d416100..d416100 100644
--- a/things_i_like/music/867.png
+++ b/content/things_i_like/music/867.png
Binary files differ
diff --git a/things_i_like/music/act_ii.jpg b/content/things_i_like/music/act_ii.jpg
index 97b2e82..97b2e82 100644
--- a/things_i_like/music/act_ii.jpg
+++ b/content/things_i_like/music/act_ii.jpg
Binary files differ
diff --git a/things_i_like/music/apollo.jpg b/content/things_i_like/music/apollo.jpg
index 9c742ee..9c742ee 100644
--- a/things_i_like/music/apollo.jpg
+++ b/content/things_i_like/music/apollo.jpg
Binary files differ
diff --git a/things_i_like/music/atebts.jpg b/content/things_i_like/music/atebts.jpg
index ca68692..ca68692 100644
--- a/things_i_like/music/atebts.jpg
+++ b/content/things_i_like/music/atebts.jpg
Binary files differ
diff --git a/things_i_like/music/departure_songs.jpg b/content/things_i_like/music/departure_songs.jpg
index 2699d5d..2699d5d 100644
--- a/things_i_like/music/departure_songs.jpg
+++ b/content/things_i_like/music/departure_songs.jpg
Binary files differ
diff --git a/things_i_like/music/index.html b/content/things_i_like/music/index.html
index 46ebc1f..46ebc1f 100644
--- a/things_i_like/music/index.html
+++ b/content/things_i_like/music/index.html
diff --git a/things_i_like/music/jiminy.jpg b/content/things_i_like/music/jiminy.jpg
index a216de4..a216de4 100644
--- a/things_i_like/music/jiminy.jpg
+++ b/content/things_i_like/music/jiminy.jpg
Binary files differ
diff --git a/things_i_like/music/lysf.jpg b/content/things_i_like/music/lysf.jpg
index 8605559..8605559 100644
--- a/things_i_like/music/lysf.jpg
+++ b/content/things_i_like/music/lysf.jpg
Binary files differ
diff --git a/things_i_like/music/twin_fantasy.jpg b/content/things_i_like/music/twin_fantasy.jpg
index f83e135..f83e135 100644
--- a/things_i_like/music/twin_fantasy.jpg
+++ b/content/things_i_like/music/twin_fantasy.jpg
Binary files differ
diff --git a/things_i_like/music/wetdream.png b/content/things_i_like/music/wetdream.png
index 862544c..862544c 100644
--- a/things_i_like/music/wetdream.png
+++ b/content/things_i_like/music/wetdream.png
Binary files differ
diff --git a/hell/hell.css b/hell/hell.css
deleted file mode 100644
index 31119ed..0000000
--- a/hell/hell.css
+++ /dev/null
@@ -1,66 +0,0 @@
-html {
- background: url("bg.jpg");
- background-attachment: fixed;
- background-size: cover;
- image-rendering: pixelated;
-}
-
-@keyframes fire
-{
-0% {text-shadow: 0 0 20px #fefcc9,
- 10px -10px 30px #feec85,
- -20px -20px 40px #ffae34,
- 20px -40px 50px #ec760c,
- -20px -60px 60px #cd4606,
- 0 -80px 70px #973716,
- 10px -90px 80px #451b0e;}
-100% {text-shadow: 0 0 20px #fefcc9,
- 10px -10px 30px #fefcc9,
- -20px -20px 40px #feec85,
- 22px -42px 60px #ffae34,
- -22px -58px 50px #ec760c,
- 0 -82px 80px #cd4606,
- 10px -90px 80px #973716;}
-}
-
-.hellfire {
- color: var(--orange);
- animation: fire 1s ease-in-out infinite alternate;
-}
-
-massive-fucking-background-flame {
- width: 100%;
- height: 100%;
- position: fixed;
- top: 0;
- left: 0;
- z-index: -999;
- background-image: url("smallfire.gif");
- background-size: auto 128px;
- opacity: 0.25;
- --pan: 0 -128px;
- animation: pan linear 3s infinite
-}
-
-a {
- color: black;
-}
-
-#inverted-cross {
- display: inline-block;
- rotate: 180deg;
- transition: rotate 2s, color 0.5s;
- --glow-color: red;
- animation: glow 4s linear infinite;
-}
-
-#inverted-cross:hover {
- rotate: 0deg;
- --glow-color: white;
- color: var(--yellow);
- transition: rotate 2s, color 1s;
-}
-
-.evil {
- color: black;
-}
diff --git a/hell/index.html b/hell/index.html
deleted file mode 100644
index c03f0d0..0000000
--- a/hell/index.html
+++ /dev/null
@@ -1,75 +0,0 @@
-<!DOCTYPE html>
-
-<html lang="en">
-
-<head>
- <title>hell</title>
- <link rel="icon" href="smallfire.gif">
- <link rel="preload" href="/assets/unifont.woff2" as="font" type="font/woff2">
- <link rel="preload" href="/assets/unifont_upper.woff2" as="font" type="font/woff2">
- <link rel="stylesheet" href="/style.css">
- <link rel="stylesheet" href="hell.css">
- <meta charset="UTF-8">
-</head>
-
-<body>
- <massive-fucking-background-flame></massive-fucking-background-flame>
- <div>
- <details class="evil centered">
- <summary> <img src="hot.gif" />
- (please enable autoplay to hear music lol)</summary>
- <audio autoplay loop controls>
- <source src="hell.ogg">
- </audio>
- </details>
- <h1 class="evil centered">👿 WELLCOME INTO HELL... YOU SUCKER!!!!! 👿</h1>
- <div class="center-content">
- <img src="firebreak.gif" />
- </div>
- <div class="center-content">
- <div class="column">
-
- <section style="max-width: 640px; position: relative; margin: 4em">
- <img src="bigguy.gif" style="float: right;" />
- <p class="evil bold">
- Hey there pal. It's me haha... the "big" "guy"... AKA satan... I hope you like it here in
- hell, i worked hard on
- making it evil :D
- </p>
- <p class="evil bold">
- idk what you did to wind up here, but now you're stuck here... forever... with me haha ;)
- </p>
- <p class="evil bold">
- so... Make yourself comfortable... haha if you can lol >:]
- </p>
- <p class="evil bold">AND DON'T EFFING TRY ESCAPING!!! }:[ alright lucifer out</p>
-
- <a id="inverted-cross" class="huge" style="position: absolute; bottom: -0.5em; right: -0.5em;"
- href="/heaven">✝</a>
- </section>
- <img src="firebreak.gif" />
- <div style="position: relative;">
- <img src="Flying_Skeleton_Hell.gif" />
- <img src="hellisreal.gif" style="position: absolute; top: 0; left: -200px; rotate: -15deg;"/>
- </div>
- </div>
- </div>
- <div>
- <img src="demon.gif" />
- <img src="demon2.gif" />
- <img src="demon3.gif" />
- <img src="demon4.gif" />
- <img src="demon_face.gif" />
- <img src="skull.gif" />
- <img src="evilorb.gif" />
- <img src="pitchfork.gif" />
- <img src="smallfire.gif" />
- <img src="evilmind.gif" />
- </div>
- <div class="huge evil section centered">
- <s class="struck">F*CK</s>SCREW YOU, SUCKER 💔🥀
- </div>
- </div>
-</body>
-
-</html>
diff --git a/index.html b/index.html
deleted file mode 100644
index 42a9d2a..0000000
--- a/index.html
+++ /dev/null
@@ -1,105 +0,0 @@
-<!DOCTYPE html>
-<!-- Hey haha, yeah I wasn't kidding, I'm writing this whole site by hand. -->
-
-<html lang="en">
-
-<head>
- <title>soaos</title>
- <link rel="icon" href="favicon.png" />
- <link rel="preload" href="/assets/unifont.woff2" as="font" type="font/woff2" />
- <link rel="preload" href="/assets/unifont_upper.woff2" as="font" type="font/woff2" />
- <link rel="stylesheet" href="/style.css" />
- <link rel="stylesheet" href="/index.css" />
- <meta charset="UTF-8" />
-</head>
-
-<body>
- <div class="section">
- <div class="center-content">
- <div id="weird-fucking-header-container" title="🦌 -wuh">
- <h1 class="half whatever">𐂂🌲🌲🌲 soaos 🌲🌲🌲𐂂</h1>
- <h1 class="flip-x half whatever">𐂂🌲🌲🌲 soaos 🌲🌲🌲𐂂</h1>
- </div>
- </div>
- <p class="centered">
- I'm an "artist" and professional software developer.
- In my free time I mostly work on eccentric software projects which you can
- read about here.
- </p>
- </div>
-
- <div class="section">
- <div class="horizontal-container">
- <div class="centered">
- <h2>Stuff on this Site</h2>
- <ul class="no-bullets inline-block">
- <!-- <li>
- <a href="/projects">☿ Projects</a><span class="under-construction unselectable"
- title="🦌 -this shit is under construction, pal!"> ⚠</span>
- </li> -->
- <li>
- <a href="/blog">📖 Blog</a><span class="under-construction unselectable"
- title="🦌 -this shit is under construction!"> ⚠</span>
- </li>
- <li>
- <a href="/things_i_like">🦌 Things I Like</a><span class="under-construction unselectable"
- title="🦌 -honestly these things are redundant, everything is under construction"> ⚠</span>
- </li>
- <li>
- <a href="/rockstats">♫ Rockbox Stats</a>
- </li>
- </ul>
- </div>
-
- <div class="centered">
- <h2>Stuff on this Server</h2>
- <ul class="no-bullets inline-block">
- <li><a href="https://app.radicle.xyz/nodes/seed.soaos.dev" target="_blank">🌱 Source Code</a></li>
- <li><a href="https://tv.soaos.dev/c/soaosdev" target="_blank">📺 Videos</a></li>
- <li><a href="https://archive.soaos.dev" target="_blank">🗃 Web Archive</a></li>
- <li><a href="https://search.soaos.dev" target="_blank">🔍 Search Engine</a></li>
- <li><a href="gemini://soaos.dev">♊ Gemini Site</a></li>
- <!-- <li><a href="http://3uqdpspct5xocufs6d4wbdelxlqp4bmciwcgwmqv6c5f2gdsqvawgaad.onion">🧅 Tor Site</a></li>-->
- </ul>
- </div>
- <div class="centered">
- <h2>Find Me Elsewhere</h2>
- <ul class="no-bullets inline-block">
- <li><a href="mailto:soaos@soaos.dev" rel="me">📧 soaos@soaos.dev</a></li>
- <li><a href="http://soaos.dog" rel="me" target="_blank">🐘 soaos@furry.engineer</a></li>
- <li><a
- href="https://ssb.soaos.dev/~core/ssb/#@Y1EKP4PU77qby4lI+m5MN6+NcYdjTdRQlV6NmluevuY=.ed25519" target="_blank">~😎
- soaos</a></li>
- </ul>
- </div>
- </div>
- </div>
-
- <div class="section centered">
- <h2>Webrings</h2>
- <a class="evil" href="https://evilr.ing/soaos/previous">⛧</a>
- <a class="evil" href="https://evilr.ing">EVILRING</a>
- <a class="evil" href="https://evilr.ing/soaos/next">⛤</a>
- </div>
-
- <footer class="centered">
- <h2 class="hidden">Badges</h2>
- <ul id="badge-grid" class="no-bullets">
- <li><a href="https://neocities.org/" target="_blank"><img src="/assets/badges/html.gif"
- alt="Learn HTML Now!" /></a></li>
- <li><img src="/assets/badges/javascript.gif" alt="Javascript-Free Page" /></li>
- <li><a href="https://lynx.browser.org/" target="_blank"><img src="/assets/badges/lynx.gif"
- alt="Lynx Compatible" /></a></li>
- <li><a href="https://www.debian.org/" target="_blank"><img src="/assets/badges/powered-by-debian.gif"
- alt="Powered by Debian" /></a></li>
- <li><a href="https://neovim.io/" target="_blank"><img src="/assets/badges/neovim.gif"
- alt="Made with Neovim" /></a></li>
- <li><a href="/hell"><img src="/assets/badges/go2hell.gif" alt="Go 2 Hell Now!" /></a></li>
- </ul>
- <div>
- <div class="inline-block" title="🦌 -<3"><span class="red-on-white">█🍁█</span> 2025</div>
- </div>
- </footer>
-</body>
-
-</html>
diff --git a/static/98.css b/static/98.css
new file mode 100644
index 0000000..8012d93
--- /dev/null
+++ b/static/98.css
@@ -0,0 +1,1040 @@
+/*! 98.css v0.1.21 - https://github.com/jdan/98.css */
+/**
+ * 98.css
+ * Copyright (c) 2020 Jordan Scales <thatjdanisso.cool>
+ * https://github.com/jdan/98.css/blob/main/LICENSE
+ */
+
+@font-face {
+ font-family: unifont;
+ src: url("/assets/UnifontExMono.woff2");
+}
+
+:root {
+ /* Color */
+ --text-color: var(--fg);
+ --surface: var(--bg2);
+ --button-highlight: var(--bg4);
+ --button-face: var(--bg3);
+ --button-shadow: var(--bg-dim);
+ --window-frame: #0a0a0a;
+ --dialog-blue: var(--bg-blue);
+ --dialog-blue-light: var(--blue);
+ --dialog-gray: #808080;
+ --dialog-gray-light: #b5b5b5;
+ --link-blue: var(--blue);
+
+ /* Spacing */
+ --element-spacing: 8px;
+ --grouped-button-spacing: 4px;
+ --grouped-element-spacing: 6px;
+ --radio-width: 12px;
+ --checkbox-width: 13px;
+ --radio-label-spacing: 6px;
+ --range-track-height: 4px;
+ --range-spacing: 10px;
+
+ /* Some detailed computations for radio buttons and checkboxes */
+ --radio-total-width-precalc: var(--radio-width) + var(--radio-label-spacing);
+ --radio-total-width: calc(var(--radio-total-width-precalc));
+ --radio-left: calc(-1 * var(--radio-total-width-precalc));
+ --radio-dot-width: 4px;
+ --radio-dot-top: calc(var(--radio-width) / 2 - var(--radio-dot-width) / 2);
+ --radio-dot-left: calc(
+ -1 * (var(--radio-total-width-precalc)) + var(--radio-width) / 2 -
+ var(--radio-dot-width) / 2
+ );
+
+ --checkbox-total-width-precalc: var(--checkbox-width) +
+ var(--radio-label-spacing);
+ --checkbox-total-width: calc(var(--checkbox-total-width-precalc));
+ --checkbox-left: calc(-1 * var(--checkbox-total-width-precalc));
+ --checkmark-width: 7px;
+ --checkmark-left: 3px;
+
+ /* Borders */
+ --border-width: 1px;
+ --border-raised-outer:
+ inset -1px -1px var(--window-frame), inset 1px 1px var(--button-highlight);
+ --border-raised-inner:
+ inset -2px -2px var(--button-shadow), inset 2px 2px var(--button-face);
+ --border-sunken-outer:
+ inset -1px -1px var(--button-highlight), inset 1px 1px var(--window-frame);
+ --border-sunken-inner:
+ inset -2px -2px var(--button-face), inset 2px 2px var(--button-shadow);
+ --default-button-border-raised-outer:
+ inset -2px -2px var(--window-frame), inset 1px 1px var(--window-frame);
+ --default-button-border-raised-inner:
+ inset 2px 2px var(--button-highlight), inset -3px -3px var(--button-shadow),
+ inset 3px 3px var(--button-face);
+ --default-button-border-sunken-outer:
+ inset 2px 2px var(--window-frame), inset -1px -1px var(--window-frame);
+ --default-button-border-sunken-inner:
+ inset -2px -2px var(--button-highlight), inset 3px 3px var(--button-shadow),
+ inset -3px -3px var(--button-face);
+
+ /* Window borders flip button-face and button-highlight */
+ --border-window-outer:
+ inset -1px -1px var(--window-frame), inset 1px 1px var(--button-face);
+ --border-window-inner:
+ inset -2px -2px var(--button-shadow), inset 2px 2px var(--button-highlight);
+
+ /* Field borders (checkbox, input, etc) flip window-frame and button-shadow */
+ --border-field:
+ inset -1px -1px var(--button-highlight), inset 1px 1px var(--button-shadow),
+ inset -2px -2px var(--button-face), inset 2px 2px var(--window-frame);
+ --border-status-field:
+ inset -1px -1px var(--button-face), inset 1px 1px var(--button-shadow);
+
+ /* Tabs */
+ --border-tab:
+ inset -1px 0 var(--window-frame), inset 1px 1px var(--button-face),
+ inset -2px 0 var(--button-shadow), inset 2px 2px var(--button-highlight);
+}
+
+@font-face {
+ font-family: "Pixelated MS Sans Serif";
+ src: url("fonts/converted/ms_sans_serif.woff") format("woff");
+ src: url("fonts/converted/ms_sans_serif.woff2") format("woff2");
+ font-weight: normal;
+ font-style: normal;
+}
+
+@font-face {
+ font-family: "Pixelated MS Sans Serif";
+ src: url("fonts/converted/ms_sans_serif_bold.woff") format("woff");
+ src: url("fonts/converted/ms_sans_serif_bold.woff2") format("woff2");
+ font-weight: bold;
+ font-style: normal;
+}
+
+body {
+ font-family: unifont;
+ font-size: 1rem;
+ color: var(--text-color);
+}
+
+button,
+label,
+input,
+legend,
+textarea,
+select,
+option,
+table,
+ul.tree-view,
+.window,
+.title-bar,
+li[role="tab"] {
+ font-family: unifont;
+ -webkit-font-smoothing: none;
+ font-size: 1rem;
+}
+
+h1 {
+ font-size: 2rem;
+}
+
+h2 {
+ font-size: 1rem;
+}
+
+h3 {
+ font-size: 1rem;
+}
+
+h4 {
+ font-size: 1rem;
+}
+
+u {
+ text-decoration: none;
+ border-bottom: 0.5px solid #222222;
+}
+
+button,
+input[type="submit"],
+input[type="reset"] {
+ box-sizing: border-box;
+ border: none;
+ color: transparent;
+ text-shadow: 0 0 var(--text-color);
+ background: var(--surface);
+ box-shadow: var(--border-raised-outer), var(--border-raised-inner);
+ border-radius: 0;
+
+ min-width: 75px;
+ min-height: 23px;
+ padding: 0 12px;
+}
+
+button.default,
+input[type="submit"].default,
+input[type="reset"].default {
+ box-shadow:
+ var(--default-button-border-raised-outer),
+ var(--default-button-border-raised-inner);
+}
+
+.vertical-bar {
+ width: 4px;
+ height: 20px;
+ background: #c0c0c0;
+ box-shadow: var(--border-raised-outer), var(--border-raised-inner);
+}
+
+button:not(:disabled):active,
+input[type="submit"]:not(:disabled):active,
+input[type="reset"]:not(:disabled):active {
+ box-shadow: var(--border-sunken-outer), var(--border-sunken-inner);
+ text-shadow: 1px 1px var(--text-color);
+}
+
+button.default:not(:disabled):active,
+input[type="submit"].default:not(:disabled):active,
+input[type="reset"].default:not(:disabled):active {
+ box-shadow:
+ var(--default-button-border-sunken-outer),
+ var(--default-button-border-sunken-inner);
+}
+
+@media (not(hover)) {
+ button:not(:disabled):hover,
+ input[type="submit"]:not(:disabled):hover,
+ input[type="reset"]:not(:disabled):hover {
+ box-shadow: var(--border-sunken-outer), var(--border-sunken-inner);
+ }
+}
+
+button:focus,
+input[type="submit"]:focus,
+input[type="reset"]:focus {
+ outline: 1px dotted #000000;
+ outline-offset: -4px;
+}
+
+button::-moz-focus-inner,
+input[type="submit"]::-moz-focus-inner,
+input[type="reset"]::-moz-focus-inner {
+ border: 0;
+}
+
+:disabled,
+:disabled + label,
+input[readonly],
+input[readonly] + label {
+ color: var(--button-shadow);
+}
+
+button:disabled,
+input[type="submit"]:disabled,
+input[type="reset"]:disabled,
+:disabled + label {
+ text-shadow: 1px 1px 0 var(--button-highlight);
+}
+
+.window {
+ box-shadow: var(--border-window-outer), var(--border-window-inner);
+ background: var(--surface);
+ padding: 3px;
+}
+
+.title-bar {
+ background: linear-gradient(90deg, var(--bg0), var(--green));
+ padding: 3px 2px 3px 3px;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+
+.title-bar.inactive {
+ background: linear-gradient(
+ 90deg,
+ var(--dialog-gray),
+ var(--dialog-gray-light)
+ );
+}
+
+.title-bar-text {
+ font-weight: bold;
+ color: var(--fg);
+ letter-spacing: 0;
+ margin-right: 24px;
+}
+
+.title-bar-controls {
+ display: flex;
+}
+
+.title-bar-controls button {
+ padding: 0;
+ display: block;
+ min-width: 16px;
+ min-height: 14px;
+}
+
+.title-bar-controls button:active {
+ padding: 0;
+}
+
+.title-bar-controls button:focus {
+ outline: none;
+}
+
+.title-bar-controls button[aria-label="Minimize"],
+.title-bar-controls button[aria-label].minimize {
+ background-image: url("/assets/icon/minimize.svg");
+ background-repeat: no-repeat;
+ background-position: bottom 3px left 4px;
+}
+
+.title-bar-controls button[aria-label="Maximize"],
+.title-bar-controls button[aria-label].maximize {
+ background-image: url("/assets/icon/maximize.svg");
+ background-repeat: no-repeat;
+ background-position: top 2px left 3px;
+}
+
+.title-bar-controls button[aria-label="Maximize"]:disabled,
+.title-bar-controls button[aria-label].maximize:disabled {
+ background-image: url("/assets/icon/maximize-disabled.svg");
+ background-repeat: no-repeat;
+ background-position: top 2px left 3px;
+}
+
+.title-bar-controls button[aria-label="Restore"],
+.title-bar-controls button[aria-label].restore {
+ background-image: url("/assets/icon/restore.svg");
+ background-repeat: no-repeat;
+ background-position: top 2px left 3px;
+}
+
+.title-bar-controls button[aria-label="Help"],
+.title-bar-controls button[aria-label].help {
+ background-image: url("/assets/icon/help.svg");
+ background-repeat: no-repeat;
+ background-position: top 2px left 5px;
+}
+
+.title-bar-controls button[aria-label="Close"],
+.title-bar-controls button[aria-label].close {
+ margin-left: 2px;
+ background-image: url("/assets/icon/close.svg");
+ background-repeat: no-repeat;
+ background-position: top 3px left 4px;
+}
+
+.status-bar {
+ margin: 0px 1px;
+ display: flex;
+ gap: 1px;
+}
+
+.status-bar-field {
+ box-shadow: var(--border-status-field);
+ flex-grow: 1;
+ padding: 2px 3px;
+ margin: 0;
+}
+
+.window-body {
+ margin: var(--element-spacing);
+}
+
+fieldset {
+ border-image: url("/assets/icon/groupbox-border.svg") 2;
+ padding: calc(2 * var(--border-width) + var(--element-spacing));
+ padding-block-start: var(--element-spacing);
+ margin: 0;
+}
+
+legend {
+ background: var(--surface);
+}
+
+.field-row {
+ display: flex;
+ align-items: center;
+}
+
+[class^="field-row"] + [class^="field-row"] {
+ margin-top: var(--grouped-element-spacing);
+}
+
+.field-row > * + * {
+ margin-left: var(--grouped-element-spacing);
+}
+
+.field-row-stacked {
+ display: flex;
+ flex-direction: column;
+}
+
+.field-row-stacked * + * {
+ margin-top: var(--grouped-element-spacing);
+}
+
+label {
+ display: inline-flex;
+ align-items: center;
+ user-select: none;
+}
+
+input[type="radio"],
+input[type="checkbox"] {
+ appearance: none;
+ -webkit-appearance: none;
+ -moz-appearance: none;
+ margin: 0;
+ background: 0;
+ position: fixed;
+ opacity: 0;
+ border: none;
+}
+
+input[type="radio"] + label,
+input[type="checkbox"] + label {
+ line-height: 13px;
+}
+
+input[type="radio"] + label {
+ position: relative;
+ margin-left: var(--radio-total-width);
+}
+
+input[type="radio"] + label::before {
+ content: "";
+ position: absolute;
+ top: 0;
+ left: calc(-1 * (var(--radio-total-width-precalc)));
+ display: inline-block;
+ width: var(--radio-width);
+ height: var(--radio-width);
+ margin-right: var(--radio-label-spacing);
+ background: url("/assets/icon/radio-border.svg");
+}
+
+input[type="radio"]:active + label::before {
+ background: url("/assets/icon/radio-border-disabled.svg");
+}
+
+input[type="radio"]:checked + label::after {
+ content: "";
+ display: block;
+ width: var(--radio-dot-width);
+ height: var(--radio-dot-width);
+ top: var(--radio-dot-top);
+ left: var(--radio-dot-left);
+ position: absolute;
+ background: url("/assets/icon/radio-dot.svg");
+}
+
+input[type="radio"]:focus + label,
+input[type="checkbox"]:focus + label {
+ outline: 1px dotted #000000;
+}
+
+input[type="radio"][disabled] + label::before {
+ background: url("/assets/icon/radio-border-disabled.svg");
+}
+
+input[type="radio"][disabled]:checked + label::after {
+ background: url("/assets/icon/radio-dot-disabled.svg");
+}
+
+input[type="checkbox"] + label {
+ position: relative;
+ margin-left: var(--checkbox-total-width);
+}
+
+input[type="checkbox"] + label::before {
+ content: "";
+ position: absolute;
+ left: calc(-1 * (var(--checkbox-total-width-precalc)));
+ display: inline-block;
+ width: var(--checkbox-width);
+ height: var(--checkbox-width);
+ background: var(--button-highlight);
+ box-shadow: var(--border-field);
+ margin-right: var(--radio-label-spacing);
+}
+
+input[type="checkbox"]:active + label::before {
+ background: var(--surface);
+}
+
+input[type="checkbox"]:checked + label::after {
+ content: "";
+ display: block;
+ width: var(--checkmark-width);
+ height: var(--checkmark-width);
+ position: absolute;
+ left: calc(
+ -1 * (var(--checkbox-total-width-precalc)) + var(--checkmark-left)
+ );
+ background: url("/assets/icon/checkmark.svg");
+}
+
+input[type="checkbox"][disabled] + label::before {
+ background: var(--surface);
+}
+
+input[type="checkbox"][disabled]:checked + label::after {
+ background: url("/assets/icon/checkmark-disabled.svg");
+}
+
+input[type="text"],
+input[type="password"],
+input[type="email"],
+input[type="url"],
+input[type="tel"],
+input[type="number"],
+input[type="search"],
+select,
+textarea {
+ padding: 3px 4px;
+ border: none;
+ box-shadow: var(--border-field);
+ background-color: var(--button-highlight);
+ box-sizing: border-box;
+ -webkit-appearance: none;
+ -moz-appearance: none;
+ appearance: none;
+ border-radius: 0;
+}
+
+input[type="text"],
+input[type="password"],
+input[type="email"],
+input[type="url"],
+input[type="tel"],
+input[type="search"],
+select {
+ height: 21px;
+}
+input[type="number"] {
+ /* need this 1 pixel to fit the spinner controls in box */
+ height: 22px;
+}
+/* clears the ‘X’ from Internet Explorer */
+input[type="search"]::-ms-clear {
+ display: none;
+ width: 0;
+ height: 0;
+}
+input[type="search"]::-ms-reveal {
+ display: none;
+ width: 0;
+ height: 0;
+}
+/* clears the ‘X’ from Chrome */
+input[type="search"]::-webkit-search-decoration,
+input[type="search"]::-webkit-search-cancel-button,
+input[type="search"]::-webkit-search-results-button,
+input[type="search"]::-webkit-search-results-decoration {
+ display: none;
+}
+
+input[type="text"],
+input[type="password"],
+input[type="email"],
+input[type="url"],
+input[type="tel"],
+input[type="number"],
+input[type="search"] {
+ /* For some reason descenders are getting cut off without this */
+ line-height: 2;
+}
+
+input[type="email"]:disabled,
+input[type="url"]:disabled,
+input[type="tel"]:disabled,
+input[type="password"]:disabled,
+input[type="text"]:disabled,
+input[type="number"]:disabled,
+input[type="search"]:disabled,
+input[type="email"]:read-only,
+input[type="url"]:read-only,
+input[type="tel"]:read-only,
+input[type="password"]:read-only,
+input[type="text"]:read-only,
+input[type="number"]:read-only,
+input[type="search"]:read-only,
+textarea:disabled {
+ background-color: var(--surface);
+}
+
+select {
+ appearance: none;
+ -webkit-appearance: none;
+ -moz-appearance: none;
+ position: relative;
+ padding-right: 32px;
+ background-image: url("/assets/icon/button-down.svg");
+ background-position: top 2px right 2px;
+ background-repeat: no-repeat;
+ border-radius: 0;
+}
+
+select:focus,
+input[type="text"]:focus,
+input[type="password"]:focus,
+input[type="email"]:focus,
+input[type="url"]:focus,
+input[type="tel"]:focus,
+input[type="number"]:focus,
+input[type="search"]:focus,
+textarea:focus {
+ outline: none;
+}
+
+input[type="range"] {
+ -webkit-appearance: none;
+ width: 100%;
+ background: transparent;
+}
+
+input[type="range"]:focus {
+ outline: none;
+}
+
+input[type="range"]::-webkit-slider-thumb {
+ -webkit-appearance: none;
+ height: 21px;
+ width: 11px;
+ background: url("/assets/icon/indicator-horizontal.svg");
+ transform: translateY(-8px);
+ box-shadow: none;
+ border: none;
+}
+
+input[type="range"].has-box-indicator::-webkit-slider-thumb {
+ background: url("/assets/icon/indicator-rectangle-horizontal.svg");
+ transform: translateY(-10px);
+}
+
+input[type="range"]::-moz-range-thumb {
+ height: 21px;
+ width: 11px;
+ border: 0;
+ border-radius: 0;
+ background: url("/assets/icon/indicator-horizontal.svg");
+ transform: translateY(2px);
+}
+
+input[type="range"].has-box-indicator::-moz-range-thumb {
+ background: url("/assets/icon/indicator-rectangle-horizontal.svg");
+ transform: translateY(0px);
+}
+
+input[type="range"]::-webkit-slider-runnable-track {
+ width: 100%;
+ height: 2px;
+ box-sizing: border-box;
+ background: black;
+ border-right: 1px solid grey;
+ border-bottom: 1px solid grey;
+ box-shadow:
+ 1px 0 0 white,
+ 1px 1px 0 white,
+ 0 1px 0 white,
+ -1px 0 0 darkgrey,
+ -1px -1px 0 darkgrey,
+ 0 -1px 0 darkgrey,
+ -1px 1px 0 white,
+ 1px -1px darkgrey;
+}
+
+input[type="range"]::-moz-range-track {
+ width: 100%;
+ height: 2px;
+ box-sizing: border-box;
+ background: black;
+ border-right: 1px solid grey;
+ border-bottom: 1px solid grey;
+ box-shadow:
+ 1px 0 0 white,
+ 1px 1px 0 white,
+ 0 1px 0 white,
+ -1px 0 0 darkgrey,
+ -1px -1px 0 darkgrey,
+ 0 -1px 0 darkgrey,
+ -1px 1px 0 white,
+ 1px -1px darkgrey;
+}
+
+.is-vertical {
+ display: inline-block;
+ width: 4px;
+ height: 150px;
+ transform: translateY(50%);
+}
+
+.is-vertical > input[type="range"] {
+ width: 150px;
+ height: 4px;
+ margin: 0 calc(var(--grouped-element-spacing) + var(--range-spacing)) 0
+ var(--range-spacing);
+ transform-origin: left;
+ transform: rotate(270deg) translateX(calc(-50% + var(--element-spacing)));
+}
+
+.is-vertical > input[type="range"]::-webkit-slider-runnable-track {
+ border-left: 1px solid grey;
+ border-right: 0;
+ border-bottom: 1px solid grey;
+ box-shadow:
+ -1px 0 0 white,
+ -1px 1px 0 white,
+ 0 1px 0 white,
+ 1px 0 0 darkgrey,
+ 1px -1px 0 darkgrey,
+ 0 -1px 0 darkgrey,
+ 1px 1px 0 white,
+ -1px -1px darkgrey;
+}
+
+.is-vertical > input[type="range"]::-moz-range-track {
+ border-left: 1px solid grey;
+ border-right: 0;
+ border-bottom: 1px solid grey;
+ box-shadow:
+ -1px 0 0 white,
+ -1px 1px 0 white,
+ 0 1px 0 white,
+ 1px 0 0 darkgrey,
+ 1px -1px 0 darkgrey,
+ 0 -1px 0 darkgrey,
+ 1px 1px 0 white,
+ -1px -1px darkgrey;
+}
+
+.is-vertical > input[type="range"]::-webkit-slider-thumb {
+ transform: translateY(-8px) scaleX(-1);
+}
+
+.is-vertical > input[type="range"].has-box-indicator::-webkit-slider-thumb {
+ transform: translateY(-10px) scaleX(-1);
+}
+
+.is-vertical > input[type="range"]::-moz-range-thumb {
+ transform: translateY(2px) scaleX(-1);
+}
+
+.is-vertical > input[type="range"].has-box-indicator::-moz-range-thumb {
+ transform: translateY(0px) scaleX(-1);
+}
+
+select:focus {
+ color: var(--button-highlight);
+ background-color: var(--dialog-blue);
+}
+select:focus option {
+ color: #000;
+ background-color: #fff;
+}
+
+select:active {
+ background-image: url("/assets/icon/button-down-active.svg");
+}
+
+a {
+ color: var(--link-blue);
+}
+
+a:focus {
+ outline: 1px dotted var(--link-blue);
+}
+
+ul.tree-view {
+ display: block;
+ background: var(--button-highlight);
+ /* box-shadow: var(--border-field); */
+ padding: 6px;
+ margin: 0;
+}
+
+ul.tree-view li {
+ list-style-type: none;
+}
+
+ul.tree-view a {
+ text-decoration: none;
+ color: var(--blue);
+}
+
+ul.tree-view a:focus {
+ background-color: var(--dialog-blue);
+ color: var(--button-highlight);
+}
+
+ul.tree-view ul,
+ul.tree-view li {
+ margin-top: 3px;
+}
+
+ul.tree-view ul {
+ margin-left: 16px;
+ padding-left: 16px;
+ /* Goes down too far */
+ border-left: 1px dotted #808080;
+}
+
+ul.tree-view ul > li {
+ position: relative;
+}
+ul.tree-view ul > li::before {
+ content: "";
+ display: block;
+ position: absolute;
+ left: -16px;
+ top: 6px;
+ width: 12px;
+ border-bottom: 1px dotted #808080;
+}
+
+/* Cover the bottom of the left dotted border */
+ul.tree-view ul > li:last-child::after {
+ content: "";
+ display: block;
+ position: absolute;
+ left: -20px;
+ top: 7px;
+ bottom: 0px;
+ width: 8px;
+ background: var(--button-highlight);
+}
+
+ul.tree-view details {
+ margin-top: 0;
+}
+
+ul.tree-view details[open] summary {
+ margin-bottom: 0;
+}
+
+ul.tree-view ul details > summary:before {
+ margin-left: -22px;
+ position: relative;
+ z-index: 1;
+}
+
+ul.tree-view details > summary:before {
+ text-align: center;
+ display: block;
+ float: left;
+ content: "+";
+ border: 1px solid var(--bg5);
+ width: 11px;
+ height: 11px;
+ line-height: 10px;
+ margin-right: 5px;
+ background-color: var(--bg1);
+}
+
+ul.tree-view details[open] > summary:before {
+ content: "-";
+}
+
+ul.tree-view details > summary::marker,
+ul.tree-view details > summary::-webkit-details-marker {
+ content: "";
+}
+
+pre {
+ display: block;
+ background: var(--button-highlight);
+ box-shadow: var(--border-field);
+ padding: 12px 8px;
+ margin: 0;
+}
+
+code,
+code * {
+ font-family: monospace;
+}
+
+summary:focus {
+ outline: 1px dotted #000000;
+}
+
+::-webkit-scrollbar {
+ width: 16px;
+}
+::-webkit-scrollbar:horizontal {
+ height: 17px;
+}
+
+::-webkit-scrollbar-corner {
+ background: var(--button-face);
+}
+
+::-webkit-scrollbar-track {
+ background-image: url("/assets/icon/scrollbar-background.svg");
+}
+
+::-webkit-scrollbar-thumb {
+ background-color: var(--button-face);
+ box-shadow: var(--border-raised-outer), var(--border-raised-inner);
+}
+
+::-webkit-scrollbar-button:horizontal:start:decrement,
+::-webkit-scrollbar-button:horizontal:end:increment,
+::-webkit-scrollbar-button:vertical:start:decrement,
+::-webkit-scrollbar-button:vertical:end:increment {
+ display: block;
+}
+
+::-webkit-scrollbar-button:vertical:start {
+ height: 17px;
+ background-image: url("/assets/icon/button-up.svg");
+}
+::-webkit-scrollbar-button:vertical:end {
+ height: 17px;
+ background-image: url("/assets/icon/button-down.svg");
+}
+::-webkit-scrollbar-button:horizontal:start {
+ width: 16px;
+ background-image: url("/assets/icon/button-left.svg");
+}
+::-webkit-scrollbar-button:horizontal:end {
+ width: 16px;
+ background-image: url("/assets/icon/button-right.svg");
+}
+
+.window[role="tabpanel"] {
+ position: relative;
+ z-index: 2;
+}
+
+menu[role="tablist"] {
+ position: relative;
+ margin: 0 0 -2px 0;
+ text-indent: 0;
+ list-style-type: none;
+ display: flex;
+ padding-left: 3px;
+}
+
+menu[role="tablist"] > li {
+ border-top-left-radius: 3px;
+ border-top-right-radius: 3px;
+ box-shadow: var(--border-tab);
+ z-index: 1;
+}
+
+menu[role="tablist"] > li[aria-selected="true"] {
+ padding-bottom: 2px;
+ margin-top: -2px;
+ background-color: var(--surface);
+ position: relative;
+ z-index: 8;
+ margin-left: -3px;
+}
+
+menu[role="tablist"] > li > a {
+ display: block;
+ color: #222;
+ margin: 6px;
+ text-decoration: none;
+}
+menu[role="tablist"] > li[aria-selected="true"] > a:focus {
+ outline: none;
+}
+menu[role="tablist"] > li > a:focus {
+ outline: 1px dotted #222;
+}
+
+menu[role="tablist"].multirows > li {
+ flex-grow: 1;
+ text-align: center;
+}
+.sunken-panel {
+ box-sizing: border-box;
+ border: 2px groove transparent;
+ border-image: url("/assets/icon/sunken-panel-border.svg") 2;
+ overflow: auto;
+ background-color: #fff;
+}
+
+table {
+ border-collapse: collapse;
+ position: relative;
+ text-align: left;
+ white-space: nowrap;
+ background-color: var(--bg0);
+}
+
+table > thead > tr > * {
+ position: sticky;
+ top: 0;
+ height: 17px;
+ box-shadow: var(--border-raised-outer), var(--border-raised-inner);
+ background: var(--surface);
+ box-sizing: border-box;
+ font-weight: normal;
+ padding: 0 var(--grouped-element-spacing);
+}
+
+table.interactive > tbody > tr {
+ cursor: pointer;
+}
+
+table > tbody > tr.highlighted {
+ color: #fff;
+ background-color: var(--dialog-blue);
+}
+
+table > tbody > tr > * {
+ padding: 0 var(--grouped-element-spacing);
+ height: 14px;
+}
+
+.progress-indicator {
+ height: 32px;
+ position: relative;
+ box-shadow: var(--border-sunken-inner);
+ padding: 4px 4px;
+ border: none;
+ box-sizing: border-box;
+ -webkit-appearance: none;
+ -moz-appearance: none;
+ appearance: none;
+ border-radius: 0;
+}
+
+.progress-indicator > .progress-indicator-bar {
+ height: 100%;
+ display: block;
+ background-color: var(--dialog-blue);
+}
+
+.progress-indicator.segmented > .progress-indicator-bar {
+ width: 100%;
+ background-color: transparent; /* resets the background color which is set to blue in the non-segmented selector */
+ background-image: linear-gradient(
+ 90deg,
+ var(--dialog-blue) 0 16px,
+ transparent 0 2px
+ );
+ background-repeat: repeat;
+ background-size: 18px 100%;
+}
+
+.field-border {
+ background: var(--button-highlight);
+ box-shadow: var(--border-field);
+ padding: 2px;
+}
+
+.field-border-disabled {
+ background: var(--surface);
+ box-shadow: var(--border-field);
+ padding: 2px;
+}
+
+.status-field-border {
+ background: var(--surface);
+ box-shadow: var(--border-status-field);
+ padding: 1px;
+}
diff --git a/assets/UnifontExMono.woff2 b/static/assets/UnifontExMono.woff2
index e7db71a..e7db71a 100644
--- a/assets/UnifontExMono.woff2
+++ b/static/assets/UnifontExMono.woff2
Binary files differ
diff --git a/static/assets/badges/cookies.png b/static/assets/badges/cookies.png
new file mode 100644
index 0000000..70994a7
--- /dev/null
+++ b/static/assets/badges/cookies.png
Binary files differ
diff --git a/assets/badges/go2hell.gif b/static/assets/badges/go2hell.gif
index 7f1290a..7f1290a 100644
--- a/assets/badges/go2hell.gif
+++ b/static/assets/badges/go2hell.gif
Binary files differ
diff --git a/static/assets/badges/indieweb.png b/static/assets/badges/indieweb.png
new file mode 100644
index 0000000..a241947
--- /dev/null
+++ b/static/assets/badges/indieweb.png
Binary files differ
diff --git a/static/assets/badges/javascript.png b/static/assets/badges/javascript.png
new file mode 100644
index 0000000..7ad573f
--- /dev/null
+++ b/static/assets/badges/javascript.png
Binary files differ
diff --git a/assets/badges/lynx.gif b/static/assets/badges/lynx.gif
index 452ac5e..452ac5e 100644
--- a/assets/badges/lynx.gif
+++ b/static/assets/badges/lynx.gif
Binary files differ
diff --git a/assets/badges/midi_files_now.gif b/static/assets/badges/midi_files_now.gif
index 18a2422..18a2422 100644
--- a/assets/badges/midi_files_now.gif
+++ b/static/assets/badges/midi_files_now.gif
Binary files differ
diff --git a/assets/badges/powered-by-debian.gif b/static/assets/badges/powered-by-debian.gif
index 1f617c8..1f617c8 100644
--- a/assets/badges/powered-by-debian.gif
+++ b/static/assets/badges/powered-by-debian.gif
Binary files differ
diff --git a/static/assets/badges/soaos.png b/static/assets/badges/soaos.png
new file mode 100644
index 0000000..486af90
--- /dev/null
+++ b/static/assets/badges/soaos.png
Binary files differ
diff --git a/assets/bg.jpg b/static/assets/bg.jpg
index 4e9044c..4e9044c 100644
--- a/assets/bg.jpg
+++ b/static/assets/bg.jpg
Binary files differ
diff --git a/assets/construction.gif b/static/assets/construction.gif
index b9c4eeb..b9c4eeb 100644
--- a/assets/construction.gif
+++ b/static/assets/construction.gif
Binary files differ
diff --git a/static/assets/icon/button-down-active.svg b/static/assets/icon/button-down-active.svg
new file mode 100644
index 0000000..fa7cac1
--- /dev/null
+++ b/static/assets/icon/button-down-active.svg
@@ -0,0 +1,5 @@
+<svg width="16" height="17" viewBox="0 0 16 17" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path fill-rule="evenodd" clip-rule="evenodd" d="M0 0H15H16V17H15H0V16V1V0ZM1 16H15V1H1V16Z" fill="#232a2e"/>
+<rect x="1" y="1" width="14" height="15" fill="#3d484d"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M12 7H5V8H6V9H7V10H8V11H9V10H10V9H11V8H12V7Z" fill="#d3c6aa"/>
+</svg>
diff --git a/static/assets/icon/button-down.svg b/static/assets/icon/button-down.svg
new file mode 100644
index 0000000..7bbaa80
--- /dev/null
+++ b/static/assets/icon/button-down.svg
@@ -0,0 +1,8 @@
+<svg width="16" height="17" viewBox="0 0 16 17" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path fill-rule="evenodd" clip-rule="evenodd" d="M15 0H0V1V16H1V1H15V0Z" fill="#232a2e"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M2 1H1V15H2V2H14V1H2Z" fill="#4f585e"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M16 17H15H0V16H15V0H16V17Z" fill="#3d484d"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M15 1H14V15H1V16H14H15V1Z" fill="#232a2e"/>
+<rect x="2" y="2" width="12" height="13" fill="#3d484d"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M11 6H4V7H5V8H6V9H7V10H8V9H9V8H10V7H11V6Z" fill="#d3c6aa"/>
+</svg>
diff --git a/static/assets/icon/button-left.svg b/static/assets/icon/button-left.svg
new file mode 100644
index 0000000..80421a1
--- /dev/null
+++ b/static/assets/icon/button-left.svg
@@ -0,0 +1,8 @@
+<svg width="16" height="17" viewBox="0 0 16 17" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path fill-rule="evenodd" clip-rule="evenodd" d="M15 0H0V1V16H1V1H15V0Z" fill="#DFDFDF"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M2 1H1V15H2V2H14V1H2Z" fill="#4f585e"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M16 17H15H0V16H15V0H16V17Z" fill="#d3c6aa"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M15 1H14V15H1V16H14H15V1Z" fill="#232a2e"/>
+<rect x="2" y="2" width="12" height="13" fill="#3d484d"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M9 4H8V5H7V6H6V7H5V8H6V9H7V10H8V11H9V4Z" fill="#d3c6aa"/>
+</svg>
diff --git a/static/assets/icon/button-right.svg b/static/assets/icon/button-right.svg
new file mode 100644
index 0000000..5573fe0
--- /dev/null
+++ b/static/assets/icon/button-right.svg
@@ -0,0 +1,8 @@
+<svg width="16" height="17" viewBox="0 0 16 17" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path fill-rule="evenodd" clip-rule="evenodd" d="M15 0H0V1V16H1V1H15V0Z" fill="#DFDFDF"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M2 1H1V15H2V2H14V1H2Z" fill="#4f585e"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M16 17H15H0V16H15V0H16V17Z" fill="#d3c6aa"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M15 1H14V15H1V16H14H15V1Z" fill="#232a2e"/>
+<rect x="2" y="2" width="12" height="13" fill="#3d484d"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M7 4H6V11H7V10H8V9H9V8H10V7H9V6H8V5H7V4Z" fill="#d3c6aa"/>
+</svg>
diff --git a/static/assets/icon/button-up.svg b/static/assets/icon/button-up.svg
new file mode 100644
index 0000000..4b25846
--- /dev/null
+++ b/static/assets/icon/button-up.svg
@@ -0,0 +1,8 @@
+<svg width="16" height="17" viewBox="0 0 16 17" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path fill-rule="evenodd" clip-rule="evenodd" d="M15 0H0V1V16H1V1H15V0Z" fill="#232a2e"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M2 1H1V15H2V2H14V1H2Z" fill="#4f585e"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M16 17H15H0V16H15V0H16V17Z" fill="#3d484d"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M15 1H14V15H1V16H14H15V1Z" fill="#232a2e"/>
+<rect x="2" y="2" width="12" height="13" fill="#3d484d"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M8 6H7V7H6V8H5V9H4V10H11V9H10V8H9V7H8V6Z" fill="#d3c6aa"/>
+</svg>
diff --git a/static/assets/icon/checkmark-disabled.svg b/static/assets/icon/checkmark-disabled.svg
new file mode 100644
index 0000000..f9d34b9
--- /dev/null
+++ b/static/assets/icon/checkmark-disabled.svg
@@ -0,0 +1,3 @@
+<svg width="7" height="7" viewBox="0 0 7 7" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path fill-rule="evenodd" clip-rule="evenodd" d="M7 0H6V1H5V2H4V3H3V4H2V3H1V2H0V5H1V6H2V7H3V6H4V5H5V4H6V3H7V0Z" fill="#232a2e"/>
+</svg>
diff --git a/static/assets/icon/checkmark.svg b/static/assets/icon/checkmark.svg
new file mode 100644
index 0000000..297996a
--- /dev/null
+++ b/static/assets/icon/checkmark.svg
@@ -0,0 +1,3 @@
+<svg width="7" height="7" viewBox="0 0 7 7" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path fill-rule="evenodd" clip-rule="evenodd" d="M7 0H6V1H5V2H4V3H3V4H2V3H1V2H0V5H1V6H2V7H3V6H4V5H5V4H6V3H7V0Z" fill="#d3c6aa"/>
+</svg>
diff --git a/static/assets/icon/close.svg b/static/assets/icon/close.svg
new file mode 100644
index 0000000..9b426f5
--- /dev/null
+++ b/static/assets/icon/close.svg
@@ -0,0 +1,3 @@
+<svg width="8" height="7" viewBox="0 0 8 7" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path fill-rule="evenodd" clip-rule="evenodd" d="M0 0H1H2V1H3V2H4H5V1H6V0H7H8V1H7V2H6V3H5V4H6V5H7V6H8V7H7H6V6H5V5H4H3V6H2V7H1H0V6H1V5H2V4H3V3H2V2H1V1H0V0Z" fill="#d3c6aa"/>
+</svg>
diff --git a/static/assets/icon/groupbox-border.svg b/static/assets/icon/groupbox-border.svg
new file mode 100644
index 0000000..a205da9
--- /dev/null
+++ b/static/assets/icon/groupbox-border.svg
@@ -0,0 +1,4 @@
+<svg width="5" height="5" viewBox="0 0 5 5" fill="grey" xmlns="http://www.w3.org/2000/svg">
+<path fill-rule="evenodd" clip-rule="evenodd" d="M0 0H5V5H0V2H2V3H3V2H0" fill="#4f585e" />
+<path fill-rule="evenodd" clip-rule="evenodd" d="M0 0H4V4H0V1H1V3H3V1H0" fill="#232a2e" />
+</svg>
diff --git a/static/assets/icon/help.svg b/static/assets/icon/help.svg
new file mode 100644
index 0000000..f7a5c07
--- /dev/null
+++ b/static/assets/icon/help.svg
@@ -0,0 +1,8 @@
+<svg width="6" height="9" viewBox="0 0 6 9" fill="none" xmlns="http://www.w3.org/2000/svg">
+<rect y="1" width="2" height="2" fill="#d3c6aa"/>
+<rect x="1" width="4" height="1" fill="#d3c6aa"/>
+<rect x="4" y="1" width="2" height="2" fill="#d3c6aa"/>
+<rect x="3" y="3" width="2" height="1" fill="#d3c6aa"/>
+<rect x="2" y="4" width="2" height="2" fill="#d3c6aa"/>
+<rect x="2" y="7" width="2" height="2" fill="#d3c6aa"/>
+</svg>
diff --git a/static/assets/icon/indicator-horizontal.svg b/static/assets/icon/indicator-horizontal.svg
new file mode 100644
index 0000000..026baf5
--- /dev/null
+++ b/static/assets/icon/indicator-horizontal.svg
@@ -0,0 +1,6 @@
+<svg width="11" height="21" viewBox="0 0 11 21" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path fill-rule="evenodd" clip-rule="evenodd" d="M0 0V16H2V18H4V20H5V19H3V17H1V1H10V0Z" fill="#4f585e"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M1 1V16H2V17H3V18H4V19H6V18H7V17H8V16H9V1Z" fill="#C0C7C8"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M9 1H10V16H8V18H6V20H5V19H7V17H9Z" fill="#87888F"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M10 0H11V16H9V18H7V20H5V21H6V19H8V17H10Z" fill="#d3c6aa"/>
+</svg>
diff --git a/static/assets/icon/indicator-rectangle-horizontal.svg b/static/assets/icon/indicator-rectangle-horizontal.svg
new file mode 100644
index 0000000..9bbfb58
--- /dev/null
+++ b/static/assets/icon/indicator-rectangle-horizontal.svg
@@ -0,0 +1,6 @@
+<svg width="11" height="21" viewBox="0 0 11 21" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path fill-rule="evenodd" clip-rule="evenodd" d="M0 0V20H1V1H10V0Z" fill="#4f585e"/>
+<rect x="1" y="1" width="8" height="18" fill="#C0C7C8"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M9 1H10V20H1V19H9Z" fill="#87888F"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M10 0H11V21H0V20H10Z" fill="#d3c6aa"/>
+</svg>
diff --git a/static/assets/icon/maximize-disabled.svg b/static/assets/icon/maximize-disabled.svg
new file mode 100644
index 0000000..34c7ff9
--- /dev/null
+++ b/static/assets/icon/maximize-disabled.svg
@@ -0,0 +1,4 @@
+<svg width="10" height="10" viewBox="0 0 10 10" fill="none" xmlns="http://www.w3.org/2000/svg">
+ <path fill-rule="evenodd" clip-rule="evenodd" d="M10 1H1V3V9V10H2H9H10V9V3V1ZM9 3H2V9H9V3Z" fill="black"/>
+ <path fill-rule="evenodd" clip-rule="evenodd" d="M9 0H0V2V8V9H1H8H9V8V2V0ZM8 2H1V8H8V2Z" fill="#232a2e"/>
+</svg> \ No newline at end of file
diff --git a/static/assets/icon/maximize.svg b/static/assets/icon/maximize.svg
new file mode 100644
index 0000000..19f8b09
--- /dev/null
+++ b/static/assets/icon/maximize.svg
@@ -0,0 +1,3 @@
+<svg width="9" height="9" viewBox="0 0 9 9" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path fill-rule="evenodd" clip-rule="evenodd" d="M9 0H0V2V8V9H1H8H9V8V2V0ZM8 2H1V8H8V2Z" fill="#d3c6aa"/>
+</svg>
diff --git a/static/assets/icon/minimize.svg b/static/assets/icon/minimize.svg
new file mode 100644
index 0000000..6231e4c
--- /dev/null
+++ b/static/assets/icon/minimize.svg
@@ -0,0 +1,3 @@
+<svg width="6" height="2" viewBox="0 0 6 2" fill="none" xmlns="http://www.w3.org/2000/svg">
+<rect width="6" height="2" fill="#d3c6aa"/>
+</svg>
diff --git a/static/assets/icon/radio-border-disabled.svg b/static/assets/icon/radio-border-disabled.svg
new file mode 100644
index 0000000..c53fa15
--- /dev/null
+++ b/static/assets/icon/radio-border-disabled.svg
@@ -0,0 +1,7 @@
+<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path fill-rule="evenodd" clip-rule="evenodd" d="M8 0H4V1H2V2H1V4H0V8H1V10H2V8H1V4H2V2H4V1H8V2H10V1H8V0Z" fill="var(--fg)"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M8 1H4V2H2V3V4H1V8H2V9H3V8H2V4H3V3H4V2H8V3H10V2H8V1Z" fill="var(--fg)"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M9 3H10V4H9V3ZM10 8V4H11V8H10ZM8 10V9H9V8H10V9V10H8ZM4 10V11H8V10H4ZM4 10V9H2V10H4Z" fill="#DFDFDF"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M11 2H10V4H11V8H10V10H8V11H4V10H2V11H4V12H8V11H10V10H11V8H12V4H11V2Z" fill="#4f585e"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M4 2H8V3H9V4H10V8H9V9H8V10H4V9H3V8H2V4H3V3H4V2Z" fill="var(--fg)"/>
+</svg>
diff --git a/static/assets/icon/radio-border.svg b/static/assets/icon/radio-border.svg
new file mode 100644
index 0000000..e70978e
--- /dev/null
+++ b/static/assets/icon/radio-border.svg
@@ -0,0 +1,8 @@
+<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path fill-rule="evenodd" clip-rule="evenodd" d="M8 0H4V1H2V2H1V4H0V8H1V10H2V8H1V4H2V2H4V1H8V2H10V1H8V0Z" fill="#232a2e"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M8 1H4V2H2V3V4H1V8H2V9H3V8H2V4H3V3H4V2H8V3H10V2H8V1Z" fill="#d3c6aa"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M9 3H10V4H9V3ZM10 8V4H11V8H10ZM8 10V9H9V8H10V9V10H8ZM4 10V11H8V10H4ZM4 10V9H2V10H4Z" fill="#DFDFDF"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M11 2H10V4H11V8H10V10H8V11H4V10H2V11H4V12H8V11H10V10H11V8H12V4H11V2Z" fill="#4f585e"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M4 2H8V3H9V4H10V8H9V9H8V10H4V9H3V8H2V4H3V3H4V2Z" fill="#4f585e"/>
+</svg>
+
diff --git a/static/assets/icon/radio-dot-disabled.svg b/static/assets/icon/radio-dot-disabled.svg
new file mode 100644
index 0000000..e1a76fc
--- /dev/null
+++ b/static/assets/icon/radio-dot-disabled.svg
@@ -0,0 +1,3 @@
+<svg width="4" height="4" viewBox="0 0 4 4" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path fill-rule="evenodd" clip-rule="evenodd" d="M3 0H1V1H0V2V3H1V4H3V3H4V2V1H3V0Z" fill="#232a2e"/>
+</svg>
diff --git a/static/assets/icon/radio-dot.svg b/static/assets/icon/radio-dot.svg
new file mode 100644
index 0000000..f19e1b8
--- /dev/null
+++ b/static/assets/icon/radio-dot.svg
@@ -0,0 +1,3 @@
+<svg width="4" height="4" viewBox="0 0 4 4" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path fill-rule="evenodd" clip-rule="evenodd" d="M3 0H1V1H0V2V3H1V4H3V3H4V2V1H3V0Z" fill="#d3c6aa"/>
+</svg>
diff --git a/static/assets/icon/restore.svg b/static/assets/icon/restore.svg
new file mode 100644
index 0000000..bd8fe15
--- /dev/null
+++ b/static/assets/icon/restore.svg
@@ -0,0 +1,10 @@
+<svg width="8" height="9" viewBox="0 0 8 9" fill="none" xmlns="http://www.w3.org/2000/svg">
+<rect x="2" width="6" height="2" fill="#d3c6aa"/>
+<rect x="7" y="2" width="1" height="4" fill="#d3c6aa"/>
+<rect x="2" y="2" width="1" height="1" fill="#d3c6aa"/>
+<rect x="6" y="5" width="1" height="1" fill="#d3c6aa"/>
+<rect y="3" width="6" height="2" fill="#d3c6aa"/>
+<rect x="5" y="5" width="1" height="4" fill="#d3c6aa"/>
+<rect y="5" width="1" height="4" fill="#d3c6aa"/>
+<rect x="1" y="8" width="4" height="1" fill="#d3c6aa"/>
+</svg>
diff --git a/static/assets/icon/scrollbar-background.svg b/static/assets/icon/scrollbar-background.svg
new file mode 100644
index 0000000..04b2f59
--- /dev/null
+++ b/static/assets/icon/scrollbar-background.svg
@@ -0,0 +1,4 @@
+<svg width="2" height="2" viewBox="0 0 2 2" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path fill-rule="evenodd" clip-rule="evenodd" d="M1 0H0V1H1V2H2V1H1V0Z" fill="#3d484d"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M2 0H1V1H0V2H1V1H2V0Z" fill="#232a2e"/>
+</svg>
diff --git a/static/assets/icon/sunken-panel-border.svg b/static/assets/icon/sunken-panel-border.svg
new file mode 100644
index 0000000..c13e8ce
--- /dev/null
+++ b/static/assets/icon/sunken-panel-border.svg
@@ -0,0 +1,11 @@
+<?xml version="1.0"?>
+<svg xmlns="http://www.w3.org/2000/svg" width="5" height="5" viewBox="0 0 5 5">
+ <rect width="4" height="1" x="0" y="0" fill="#232a2e"/>
+ <rect width="1" height="4" x="0" y="0" fill="#232a2e"/>
+ <rect width="2" height="1" x="1" y="1" fill="#232a2e"/>
+ <rect width="1" height="2" x="1" y="1" fill="#232a2e"/>
+ <rect width="5" height="1" x="0" y="4" fill="#4f585e"/>
+ <rect width="1" height="5" x="4" y="0" fill="#4f585e"/>
+ <rect width="1" height="3" x="3" y="1" fill="#4f585e"/>
+ <rect width="3" height="1" x="1" y="3" fill="#4f585e"/>
+</svg>
diff --git a/favicon.png b/static/favicon.png
index fac014b..fac014b 100644
--- a/favicon.png
+++ b/static/favicon.png
Binary files differ
diff --git a/static/hell.css b/static/hell.css
new file mode 100644
index 0000000..572f3d1
--- /dev/null
+++ b/static/hell.css
@@ -0,0 +1,102 @@
+html {
+ background: url("/hell/bg.jpg");
+ background-attachment: fixed;
+ background-size: cover;
+ image-rendering: pixelated;
+}
+
+body {
+ margin: auto 0;
+ max-width: unset;
+ margin-right: auto;
+}
+
+@keyframes fire {
+ 0% {
+ text-shadow:
+ 0 0 20px #fefcc9,
+ 10px -10px 30px #feec85,
+ -20px -20px 40px #ffae34,
+ 20px -40px 50px #ec760c,
+ -20px -60px 60px #cd4606,
+ 0 -80px 70px #973716,
+ 10px -90px 80px #451b0e;
+ }
+ 100% {
+ text-shadow:
+ 0 0 20px #fefcc9,
+ 10px -10px 30px #fefcc9,
+ -20px -20px 40px #feec85,
+ 22px -42px 60px #ffae34,
+ -22px -58px 50px #ec760c,
+ 0 -82px 80px #cd4606,
+ 10px -90px 80px #973716;
+ }
+}
+
+.hellfire {
+ color: var(--orange);
+ animation: fire 1s ease-in-out infinite alternate;
+}
+
+massive-fucking-background-flame {
+ width: 100%;
+ height: 100%;
+ position: fixed;
+ top: 0;
+ left: 0;
+ z-index: -999;
+ background-image: url("/hell/smallfire.gif");
+ background-size: auto 128px;
+ opacity: 0.25;
+ --pan: 0 -128px;
+ animation: pan linear 3s infinite;
+}
+
+a {
+ color: black;
+}
+
+#inverted-cross {
+ display: inline-block;
+ rotate: 180deg;
+ transition:
+ rotate 2s,
+ color 0.5s;
+ --glow-color: red;
+ animation: glow 4s linear infinite;
+}
+
+#inverted-cross:hover {
+ rotate: 0deg;
+ --glow-color: white;
+ color: var(--yellow);
+ transition:
+ rotate 2s,
+ color 1s;
+}
+
+.evil {
+ color: black;
+}
+
+.huge {
+ font-size: 8rem;
+}
+
+.struck {
+ opacity: 0.5;
+ position: absolute;
+ left: 0;
+ z-index: -999;
+ user-select: none;
+}
+
+@keyframes pan {
+ from {
+ background-position: 0 0;
+ }
+ to {
+ background-position: var(--pan);
+ }
+}
diff --git a/index.css b/static/index.css
index 9ae6d1e..9ae6d1e 100644
--- a/index.css
+++ b/static/index.css
diff --git a/static/style.css b/static/style.css
new file mode 100644
index 0000000..b4c788a
--- /dev/null
+++ b/static/style.css
@@ -0,0 +1,184 @@
+:root {
+ --red: #e67e80;
+ --orange: #e69875;
+ --yellow: #dbbc7f;
+ --green: #a7c080;
+ --blue: #7fbbb3;
+ --aqua: #83c092;
+ --purple: #d699b6;
+ --fg: #d3c6aa;
+ --statusline1: #a7c080;
+ --statusline2: #d3c6aa;
+ --statusline3: #e67e80;
+ --gray0: #7a8478;
+ --gray1: #859289;
+ --gray2: #9da9a0;
+
+ --bg-dim: #232a2e;
+ --bg0: #2d353b;
+ --bg1: #343f44;
+ --bg2: #3d484d;
+ --bg3: #475258;
+ --bg4: #4f585e;
+ --bg5: #56635f;
+ --bg-red: #4c3743;
+ --bg-visual: #493b40;
+ --bg-yellow: #45443c;
+ --bg-green: #3c4841;
+ --bg-blue: #384b55;
+}
+
+html {
+ background: url("/assets/bg.jpg");
+ background-attachment: fixed;
+ background-size: cover;
+ image-rendering: pixelated;
+}
+
+a:not(:has(img, div))[target="_blank"]::after {
+ content: " ⎘";
+}
+
+a:not(:has(img,div))[href^="https://tv.soaos.dev/w/"]::before
+{
+ content: "📺 ";
+}
+
+ul {
+ list-style-type: "• ";
+}
+
+pre {
+ overflow: auto;
+}
+
+@keyframes blinker {
+ 50% {
+ opacity: 0;
+ }
+}
+
+.under-construction {
+ color: var(--yellow);
+ animation: blinker 1s linear infinite;
+}
+
+@keyframes glow {
+ 50% {
+ text-shadow:
+ 0 0 0px var(--glow-color),
+ 0 0 20px hsl(from var(--glow-color) h s calc(l - 10));
+ }
+}
+
+@keyframes glow-box {
+ 50% {
+ box-shadow:
+ 0 0 0px var(--glow-color),
+ 0 0 20px hsl(from var(--glow-color) h s calc(l - 10));
+ }
+}
+
+.evil {
+ color: var(--red);
+ --glow-color: red;
+ animation: glow 4s linear infinite;
+}
+
+.evil-box {
+ color: var(--red);
+ --glow-color: red;
+ animation: glow-box 4s linear infinite;
+}
+
+.holy {
+ color: var(--yellow);
+ --glow-color: white;
+ animation: glow 4s linear infinite;
+}
+
+a.evil:visited {
+ color: var(--red);
+}
+
+a.evil:hover {
+ color: red;
+}
+
+.hidden {
+ display: none;
+}
+
+.no-bullets {
+ list-style-type: none;
+ padding-left: 0;
+ :w;
+}
+
+#badge-grid {
+ display: flex;
+ gap: 4px;
+}
+
+.unselectable {
+ user-select: none;
+}
+
+.window {
+ margin: 1rem;
+}
+
+.red-on-white {
+ color: red;
+ background-color: white;
+}
+
+.centered {
+ display: flex;
+ justify-content: center;
+}
+
+body {
+ margin: auto 0;
+ max-width: 800px;
+ margin-right: auto;
+}
+
+[data-tooltip]:hover::after {
+ content: attr(data-tooltip);
+ border: 1px solid var(--bg-dim);
+ background: var(--fg);
+ color: var(--bg-dim);
+ display: inline-block;
+ white-space: pre-wrap;
+ text-align: left;
+ padding: 0.3em;
+ position: absolute;
+ z-index: 98;
+}
+
+.flip {
+ position: relative;
+}
+
+.hidden-selectable {
+ opacity: 0;
+}
+
+.flip::after {
+ display: inline-block;
+ position: absolute;
+ content: attr(data-title);
+ transform: rotateY(180deg);
+ clip-path: polygon(0 0, 50.1% 0, 50.1% 100%, 0 100%);
+ visibility: visible;
+ left: 0;
+}
+
+.flip::before {
+ display: inline-block;
+ position: absolute;
+ content: attr(data-title);
+ clip-path: polygon(0 0, 50.1% 0, 50.1% 100%, 0 100%);
+ visibility: visible;
+}
diff --git a/static/style.css.bak b/static/style.css.bak
new file mode 100644
index 0000000..22815a7
--- /dev/null
+++ b/static/style.css.bak
@@ -0,0 +1,224 @@
+@font-face {
+ font-family: unifont;
+ src: url("/assets/UnifontExMono.woff2");
+}
+
+:root {
+ --red: #e67e80;
+ --orange: #e69875;
+ --yellow: #dbbc7f;
+ --green: #a7c080;
+ --blue: #7fbbb3;
+ --aqua: #83c092;
+ --purple: #d699b6;
+ --fg: #d3c6aa;
+ --statusline1: #a7c080;
+ --statusline2: #d3c6aa;
+ --statusline3: #e67e80;
+ --gray0: #7a8478;
+ --gray1: #859289;
+ --gray2: #9da9a0;
+
+ --bg-dim: #232a2e;
+ --bg0: #2d353b;
+ --bg1: #343f44;
+ --bg2: #3d484d;
+ --bg3: #475258;
+ --bg4: #4f585e;
+ --bg5: #56635f;
+ --bg-red: #4c3743;
+ --bg-visual: #493b40;
+ --bg-yellow: #45443c;
+ --bg-green: #3c4841;
+ --bg-blue: #384b55;
+}
+
+body {
+ font-family: unifont;
+ font-size: 16px;
+ color: var(--fg);
+ perspective-origin: top left;
+}
+
+pre,
+code,
+samp {
+ font-family: unifont;
+}
+
+samp {
+ color: var(--gray2);
+}
+
+.section {
+ margin: 1rem;
+ padding: 1rem;
+}
+
+.horizontal-container {
+ display: flex;
+ justify-content: space-between;
+}
+
+a {
+ text-decoration: none;
+}
+
+a:hover {
+ text-decoration: underline;
+}
+
+h1,
+h2,
+h3,
+h4,
+h5,
+h6 {
+ font-weight: normal;
+ text-decoration: underline;
+}
+
+h1 {
+ font-size: 48px;
+}
+
+h2 {
+ font-size: 32px;
+}
+
+h3 {
+ font-size: 16px;
+}
+
+.centered {
+ text-align: center;
+}
+
+.center-content {
+ display: flex;
+ justify-content: center;
+}
+
+.half {
+ clip-path: polygon(0 0, 50.1% 0, 50.1% 100%, 0 100%);
+}
+
+.flip-x {
+ transform: scaleX(-1);
+}
+
+.unselectable {
+ user-select: none;
+}
+
+@keyframes blinker {
+ 50% {
+ opacity: 0;
+ }
+}
+
+.under-construction {
+ color: var(--yellow);
+ animation: blinker 1s linear infinite;
+}
+
+@keyframes glow {
+ 50% {
+ text-shadow:
+ 0 0 10px var(--glow-color),
+ 0 0 20px var(--glow-color),
+ 0 0 40px var(--glow-color),
+ 0 0 80px hsl(from var(--glow-color) h s calc(l - 10)),
+ 0 0 120px hsl(from var(--glow-color) h s calc(l - 10));
+ }
+}
+
+.evil {
+ color: var(--red);
+ --glow-color: red;
+ animation: glow 4s linear infinite;
+}
+
+.holy {
+ color: var(--yellow);
+ --glow-color: white;
+ animation: glow 4s linear infinite;
+}
+
+a.evil:visited {
+ color: var(--red);
+}
+
+a.evil:hover {
+ color: red;
+}
+
+.inline-block {
+ display: inline-block;
+}
+
+.no-bullets {
+ list-style-type: none;
+ padding-left: 0;
+}
+
+.red-on-white {
+ color: red;
+ background-color: white;
+}
+
+.hidden {
+ display: none;
+}
+
+.huge {
+ font-size: 128px;
+}
+
+.struck {
+ opacity: 0.5;
+ position: absolute;
+ z-index: -999;
+ user-select: none;
+}
+
+@keyframes pan {
+ from {
+ background-position: 0 0;
+ }
+ to {
+ background-position: var(--pan);
+ }
+}
+
+.bold {
+ font-weight: bold;
+}
+
+.column {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+}
+
+/* Links */
+a {
+ color: var(--blue);
+}
+
+a:visited {
+ color: var(--purple);
+}
+
+a:not(:has(img, div))[target="_blank"]::after {
+ content: " ⎘";
+}
+
+a:not(:has(img,div))[href^="https://tv.soaos.dev/w/"]::before
+{
+ content: "📺 ";
+}
+
+ul {
+ list-style-type: "• ";
+}
diff --git a/style.css b/style.css
deleted file mode 100644
index a3a4364..0000000
--- a/style.css
+++ /dev/null
@@ -1,215 +0,0 @@
-@font-face {
- font-family: unifont;
- src: url("/assets/UnifontExMono.woff2");
-}
-
-:root {
- --red: #e67e80;
- --orange: #e69875;
- --yellow: #dbbc7f;
- --green: #a7c080;
- --blue: #7fbbb3;
- --aqua: #83c092;
- --purple: #d699b6;
- --fg: #d3c6aa;
- --statusline1: #a7c080;
- --statusline2: #d3c6aa;
- --statusline3: #e67e80;
- --gray0: #7a8478;
- --gray1: #859289;
- --gray2: #9da9a0;
-
- --bg-dim: #232a2e;
- --bg0: #2d353b;
- --bg1: #343f44;
- --bg2: #3d484d;
- --bg3: #475258;
- --bg4: #4f585e;
- --bg5: #56635f;
- --bg-red: #4c3743;
- --bg-visual: #493b40;
- --bg-yellow: #45443c;
- --bg-green: #3c4841;
- --bg-blue: #384b55;
-}
-
-body {
- font-family: unifont;
- font-size: 16px;
- color: var(--fg);
- perspective-origin: top left
-}
-
-pre, code, samp {
- font-family: unifont;
-}
-
-samp {
- color: var(--gray2)
-}
-
-.section {
- margin: 1rem;
- padding: 1rem;
-}
-
-.horizontal-container {
- display: flex;
- justify-content: space-between;
-}
-
-a {
- text-decoration: none;
-}
-
-a:hover {
- text-decoration: underline;
-}
-
-h1, h2, h3, h4, h5, h6 {
- font-weight: normal;
- text-decoration: underline;
-}
-
-h1 {
- font-size: 48px;
-}
-
-h2 {
- font-size: 32px;
-}
-
-h3 {
- font-size: 16px;
-}
-
-.centered {
- text-align: center;
-}
-
-.center-content {
- display: flex;
- justify-content: center;
-}
-
-.half {
- clip-path: polygon(0 0, 50.1% 0, 50.1% 100%, 0 100%);
-}
-
-.flip-x {
- transform: scaleX(-1);
-}
-
-.unselectable {
- user-select: none;
-}
-
-@keyframes blinker {
- 50% {
- opacity: 0;
- }
-}
-
-.under-construction {
- color: var(--yellow);
- animation: blinker 1s linear infinite;
-}
-
-@keyframes glow {
- 50% {
- text-shadow: 0 0 10px var(--glow-color),
- 0 0 20px var(--glow-color),
- 0 0 40px var(--glow-color),
- 0 0 80px hsl(from var(--glow-color) h s calc(l - 10)),
- 0 0 120px hsl(from var(--glow-color) h s calc(l - 10));
- }
-}
-
-.evil {
- color: var(--red);
- --glow-color: red;
- animation: glow 4s linear infinite;
-}
-
-.holy {
- color: var(--yellow);
- --glow-color: white;
- animation: glow 4s linear infinite;
-}
-
-a.evil:visited {
- color: var(--red);
-}
-
-a.evil:hover {
- color: red;
-}
-
-.inline-block {
- display: inline-block;
-}
-
-.no-bullets {
- list-style-type: none;
- padding-left: 0;
-}
-
-.red-on-white {
- color: red;
- background-color: white;
-}
-
-.hidden {
- display: none;
-}
-
-.huge {
- font-size: 128px;
-}
-
-.struck {
- opacity: 0.5;
- position: absolute;
- z-index: -999;
- user-select: none;
-}
-
-@keyframes pan {
- from {
- background-position: 0 0;
- }
- to {
- background-position: var(--pan);
- }
-}
-
-.bold {
- font-weight: bold;
-}
-
-.column {
- display: flex;
- flex-direction: column;
- align-items: center;
-}
-
-/* Links */
-a {
- color: var(--blue);
-}
-
-a:visited {
- color: var(--purple);
-}
-
-a:not(:has(img,div))[target="_blank"]::after {
- content: " ⎘";
-}
-
-a:not(:has(img,div))[href^="https://tv.soaos.dev/w/"]::before {
- content: "📺 ";
-}
-
-ul {
- list-style-type: "• ";
-} \ No newline at end of file
diff --git a/templates/base.html b/templates/base.html
new file mode 100644
index 0000000..7af29b7
--- /dev/null
+++ b/templates/base.html
@@ -0,0 +1,95 @@
+{% import "macros.html" as macros %}
+<!doctype html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8" />
+ <title>soaos</title>
+ <link rel="icon" href="favicon.png" />
+ <link
+ rel="preload"
+ href="/assets/UnifontExMono.woff2"
+ as="font"
+ type="font/woff2"
+ />
+ <link rel="stylesheet" href="/98.css" />
+ <link rel="stylesheet" href="/style.css" />
+ <meta charset="UTF-8" />
+ </head>
+ <body>
+ <header>
+ <div class="window">
+ <div class="title-bar">
+ <div class="title-bar-text">𐂂 Badass header</div>
+ </div>
+ <div class="window-body centered">
+ <h1 class="flip" data-title="𐂂🌲🌲🌲 soaos 🌲🌲🌲𐂂">
+ <span class="hidden-selectable">𐂂🌲🌲🌲 soaos 🌲🌲🌲𐂂</span>
+ </h1>
+ </div>
+ </div>
+ </header>
+ <main>{% block content %} {% endblock content %}</main>
+ <footer class="window">
+ <div class="title-bar">
+ <div class="title-bar-text">🪪 Badges</div>
+ </div>
+ <div class="window-body">
+ <ul id="badge-grid" class="no-bullets">
+ <li>
+ <img
+ src="/assets/badges/javascript.png"
+ alt="Javascript-Free Page"
+ />
+ </li>
+ <li>
+ <a href="https://lynx.browser.org/" target="_blank"
+ ><img src="/assets/badges/lynx.gif" alt="Lynx Compatible"
+ /></a>
+ </li>
+ <li>
+ <a href="https://www.debian.org/" target="_blank"
+ ><img
+ src="/assets/badges/powered-by-debian.gif"
+ alt="Powered by Debian"
+ /></a>
+ </li>
+ <li>
+ <a href="/hell"
+ ><img
+ class="evil-box"
+ src="/assets/badges/go2hell.gif"
+ alt="Go 2 Hell Now!"
+ /></a>
+ </li>
+ <li>
+ <a
+ href="/assets/badges/soaos.png"
+ download
+ data-tooltip="🦌 - Link to me on your site!"
+ ><img src="/assets/badges/soaos.png" alt="soaos.dev"
+ /></a>
+ </li>
+ <li>
+ <img src="/assets/badges/cookies.png" alt="Cookie-Free Page" />
+ </li>
+ </ul>
+ </div>
+ <div class="status-bar">
+ <p data-tooltip="🦌 -<3" class="status-bar-field">
+ <span class="red-on-white">█🍁█</span>
+ </p>
+ <p class="status-bar-field">
+ <a href="https://evilr.ing/soaos/previous" class="evil">⛧</a>
+ <a href="https://evilr.ing" class="evil">EVILRING</a>
+ <a href="https://evilr.ing/soaos/next" class="evil">⛤</a>
+ </p>
+ <p class="status-bar-field">
+ <span class="hidden">Built at: </span>{{ now() }}
+ </p>
+ <p class="status-bar-field">
+ <a href="https://soaos.dev" class="flip" data-title="soaos"><span class="hidden-selectable">soaos<span/></a>
+ </p>
+ </div>
+ </footer>
+ </body>
+</html>
diff --git a/templates/blog.html b/templates/blog.html
new file mode 100644
index 0000000..f659571
--- /dev/null
+++ b/templates/blog.html
@@ -0,0 +1,38 @@
+{% extends "base.html" %} {% block content %}
+<div class="window">
+ <div class="title-bar">
+ <div class="title-bar-text">📖 Blog</div>
+ <div class="title-bar-controls">
+ <a href="/"><button aria-label="Close"></button></a>
+ </div>
+ </div>
+ <div class="window-body">
+ <p>
+ Welcome to my blog! This is where I'll post longer content about stuff I'm
+ working on. I'm working out some channels for posting day-to-day short
+ form shit too so keep an eye out for that.
+ </p>
+ <h2>Latest Posts</h2>
+ <div class="sunken-panel">
+ <ul class="tree-view" style="height: 8rem; overflow-y: scroll">
+ {% set current_year = now() | date(format="%Y") | int %} {% for i in
+ range(start=0, end=current_year - 2024) %}
+ <li>
+ <details open>
+ <summary><b>{{current_year - i}}</b></summary>
+ <ul>
+ {% for post in section.pages %} {% if post.year == current_year -
+ i %}
+ <li>
+ <a href="{{post.permalink}}">{{post.title}}</a> - {{post.date}}
+ </li>
+ {% endif %} {% endfor %}
+ </ul>
+ </details>
+ </li>
+ {% endfor %}
+ </ul>
+ </div>
+ </div>
+</div>
+{% endblock content %}
diff --git a/heaven/index.html b/templates/heaven.html
index e8b40b1..93a86ac 100644
--- a/heaven/index.html
+++ b/templates/heaven.html
@@ -3,12 +3,9 @@
<html lang="en">
<head>
- <title>hell</title>
- <link rel="icon" href="smallfire.gif">
- <link rel="preload" href="/assets/unifont.woff2" as="font" type="font/woff2">
- <link rel="preload" href="/assets/unifont_upper.woff2" as="font" type="font/woff2">
+ <title>heaven</title>
<link rel="stylesheet" href="/style.css">
- <link rel="stylesheet" href="heaven.css">
+ <link rel="stylesheet" href="/heaven/heaven.css">
<meta charset="UTF-8">
</head>
@@ -19,7 +16,7 @@
<summary>🕊🕊🕊
(please enable autoplay to hear music lol)</summary>
<audio autoplay loop controls>
- <source src="everytime_we_touch_nightcore.ogg">
+ <source src="/heaven/everytime_we_touch_nightcore.ogg">
</audio>
</details>
<h1 class="holy">😇 GOD BLESS YOU FRIEND... YOU ARE IN HEAVEN! 😇</h1>
@@ -29,3 +26,4 @@
</body>
</html>
+
diff --git a/templates/hell.html b/templates/hell.html
new file mode 100644
index 0000000..d799db3
--- /dev/null
+++ b/templates/hell.html
@@ -0,0 +1,22 @@
+<!doctype html>
+
+<html lang="en">
+ <head>
+ <title>hell</title>
+ <link rel="icon" href="/hell/smallfire.gif" />
+ <link
+ rel="preload"
+ href="/assets/UnifontExMono.woff2"
+ as="font"
+ type="font/woff2"
+ />
+ <link rel="stylesheet" href="/style.css" />
+ <link rel="stylesheet" href="/hell.css" />
+ <meta charset="UTF-8" />
+ </head>
+
+ <body>
+ <massive-fucking-background-flame></massive-fucking-background-flame>
+ {{ section.content | safe }}
+ </body>
+</html>
diff --git a/templates/index.html b/templates/index.html
new file mode 100644
index 0000000..0bf66d0
--- /dev/null
+++ b/templates/index.html
@@ -0,0 +1,2 @@
+{% extends "base.html" %} {% block content %} {{ section.content | safe }} {%
+endblock content %}
diff --git a/templates/macros.html b/templates/macros.html
new file mode 100644
index 0000000..eae30fe
--- /dev/null
+++ b/templates/macros.html
@@ -0,0 +1,3 @@
+{% macro post_list() %} {% for post in section.pages %}
+<a href="{{ post.permalink }}">{{ post.title }}</a>
+{% endfor %} {% endmacro input %} \ No newline at end of file
diff --git a/templates/page.html b/templates/page.html
new file mode 100644
index 0000000..4cd271b
--- /dev/null
+++ b/templates/page.html
@@ -0,0 +1,2 @@
+{% extends "base.html" %} {% block content %} {{ page.content | safe }} {%
+endblock content %}
diff --git a/templates/post.html b/templates/post.html
new file mode 100644
index 0000000..794c715
--- /dev/null
+++ b/templates/post.html
@@ -0,0 +1,19 @@
+{% extends "base.html" %} {% block content %}
+
+<div class="window">
+ <div class="title-bar">
+ <b>📰 Post: {{ page.title }}</b>
+ <div class="title-bar-controls">
+ <button
+ aria-label="Help"
+ data-tooltip="Posted {{ page.date }}&#xa;{{ page.word_count }} words ({{ page.reading_time }} minutes)"
+ ></button>
+ <a href="../"><button aria-label="Close"></button></a>
+ </div>
+ </div>
+ <div class="window-body">
+ <h1>{{ page.title }}</h1>
+ <article>{{ page.content | safe}}</article>
+ </div>
+</div>
+{% endblock content %}
diff --git a/templates/section.html b/templates/section.html
new file mode 100644
index 0000000..0bf66d0
--- /dev/null
+++ b/templates/section.html
@@ -0,0 +1,2 @@
+{% extends "base.html" %} {% block content %} {{ section.content | safe }} {%
+endblock content %}
diff --git a/templates/shortcodes/soaosed.html b/templates/shortcodes/soaosed.html
new file mode 100644
index 0000000..c5ae639
--- /dev/null
+++ b/templates/shortcodes/soaosed.html
@@ -0,0 +1,3 @@
+<span class="flip" data-title="{{ body }}"
+ ><span class="hidden-selectable">{{ body }}</span></span
+> \ No newline at end of file
diff --git a/templates/shortcodes/subtree.html b/templates/shortcodes/subtree.html
new file mode 100644
index 0000000..2246718
--- /dev/null
+++ b/templates/shortcodes/subtree.html
@@ -0,0 +1,8 @@
+<li>
+ <details {% if open %}open{% endif %}>
+ <summary><b>{{ name | safe }}</b></summary>
+ <ul>
+ {{ body | markdown | safe }}
+ </ul>
+ </details>
+</li> \ No newline at end of file
diff --git a/templates/shortcodes/title_bar.html b/templates/shortcodes/title_bar.html
new file mode 100644
index 0000000..facc6fb
--- /dev/null
+++ b/templates/shortcodes/title_bar.html
@@ -0,0 +1,8 @@
+<div class="title-bar">
+ <div class="title-bar-text">{{ body | safe }}</div>
+ <div class="title-bar-controls">
+ {% if close %}
+ <a href="{{ close }}"><button aria-label="Close"></button></a>
+ {% endif %}
+ </div>
+</div>
diff --git a/templates/shortcodes/tree_view.html b/templates/shortcodes/tree_view.html
new file mode 100644
index 0000000..e8de782
--- /dev/null
+++ b/templates/shortcodes/tree_view.html
@@ -0,0 +1,5 @@
+<div class="sunken-panel">
+ <ul class="tree-view" {% if height %} style="height: {{height}}; overflow-y: scroll" {% endif %} >
+ {{ body | markdown | safe }}
+ </ul>
+</div>
diff --git a/templates/shortcodes/treelink.html b/templates/shortcodes/treelink.html
new file mode 100644
index 0000000..ff80fdc
--- /dev/null
+++ b/templates/shortcodes/treelink.html
@@ -0,0 +1,8 @@
+<li>
+ <a href="{{ url }}" {% if blank %} target="_blank" {% endif %} {% if rel %} rel="{{ rel }}" {% endif %}>{{ text }}</a>
+ {% if wip %}
+ <span {% if wip_tooltip %}data-tooltip="{{ wip_tooltip }}" {% endif %}>
+ <span class="under-construction unselectable">⚠</span>
+ </span>
+ {% endif %}
+</li> \ No newline at end of file
diff --git a/templates/shortcodes/window.html b/templates/shortcodes/window.html
new file mode 100644
index 0000000..8abddbe
--- /dev/null
+++ b/templates/shortcodes/window.html
@@ -0,0 +1,3 @@
+<div class="window">
+{{ body | safe }}
+</div>
diff --git a/templates/shortcodes/window_body.html b/templates/shortcodes/window_body.html
new file mode 100644
index 0000000..f7c84a7
--- /dev/null
+++ b/templates/shortcodes/window_body.html
@@ -0,0 +1 @@
+<div class="window-body">{{ body | markdown | safe }}</div>