Move to src dir

This commit is contained in:
Nox Sluijtman 2024-02-10 21:03:20 +01:00
parent 79fd67e916
commit bb3007d53f
83 changed files with 1316 additions and 2 deletions

View file

@ -0,0 +1,6 @@
---
title: "{{ replace .Name "-" " " | title }}"
date: {{ .Date }}
draft: true
---

View file

@ -0,0 +1,8 @@
---
title: "{{ replace .Name "-" " " | title }}"
date: {{ .Date }}
author: "$HUMANOID"
tags: ["", ""]
description: "This is the default article description"
draft: true
---

45
src/config.toml Normal file
View file

@ -0,0 +1,45 @@
baseURL = '/'
languageCode = 'en-us'
title = 'The Voidcruiser'
theme = 'vugo'
paginate = 100
[params]
contentTypeName = 'rambles'
[params.logo]
logoText = "The Voidcruiser"
logoHomeLink = "/"
[menu]
[[menu.main]]
identifier = "about"
name = "about"
url = "/about/"
[[menu.main]]
identifier = "insanity"
name = "aural insanity"
url = "/insanity/"
[[menu.main]]
identifier = "services"
name = "services"
url = "/services/"
[[menu.main]]
identifier = "rambles"
name = "rambles"
url = "/rambles/"
[[menu.services]]
identifier = "alpine repo"
name = "alpine repo"
url = "https://alpine.voidcruiser.nl"
[[menu.services]]
identifier = "searXNG instance"
name = "searXNG instance"
url = "/searx/"
[markup]
[markup.tableOfContents]
endLevel = 4
startLevel = 1
[markup.highlight]
noClasses = true
style = 'gruvbox'

21
src/content/_index.md Normal file
View file

@ -0,0 +1,21 @@
---
title: "Home"
date: 2022-08-10T14:19:40+02:00
description: "Digital enclave of the Voidcruiser"
---
# This thing might contain a ramble every now and then
Welcome to my little corner of the internet!
If you're a sane person, you might be wondering why I made this site.
That is to say, you are the type of person who hasn't seen several dozen sites like this one yet.
To you, dear sane person, the quickest way to explain why I made this site would to to call it an expression of ideology.
Or at least part of it.
Other reasons I made this site include wanting a place to shout into the public consciousness known as the internet without making use of well known/existing platforms.
I don't like mainstream platforms as they restrict me to what big tech wants me to express.
As you might be able to guess from the design of this page, I am not a big fan of big tech and centralisation of power.
I might kick various services into the air at some point if I feel like it (have been thinking about PeerTube and Mastodon or Pleroma not that I really know what I'd do with them).
{{< noscript content="Hello fellow Non-JavaScript-runner! This site page doesn't use any JavaScript unless mentioned." >}}

56
src/content/about.md Normal file
View file

@ -0,0 +1,56 @@
---
title: About
description: "The obligatory about page"
---
# The HUMANOID running this thing {#HUMANOID}
## Identity - 's a funny thing isn't it?
{{< about >}}
# The site itself
- Colorscheme: [Gruvbox](https://github.com/morhetz/gruvbox)
Depending on `prefers-color-scheme:` in your browser settings being `light` or not, you get to see the light variant or not.
- This site is viewable and usable on every browser I've thrown at it so far.\
I even spent a 10 minutes making this site look reasonable on mobile devices.\
Here's the browser list:
- Qutebrowser
- Bromite
- LibreWolf
- FireFox
- UnGoogled Chromium
- Lynx
- Midori
- w3m
- Dillo
- Netsurf
- Nyxt
# Various devices in posession of [$HUMANOID](#HUMANOID)
| hostname | os | device/model/main board | role |
|---|---|---|---|
| hazyMonolith | Debian 11 | HP Z210 Workstation | Used to be an entertainment box but got superceded by voidBerry. These days, mainly standing under my desk being a thing to put my feed up on.|
| voidSlab | Alpine Edge | ThinkPad T440p |Daily driver for lighter and less serious things.|
| RejuvinatedBrick | Alpine Edge |Dell Latitude E5500 |Being a beautiful brick to nagivate Gopher and Gemini.|
| voidCreeper | NixOS 22.05 | HP Omen 15 |Central heating if my actual central heating fails and running the few games I still play these days.|
| HappyThonk | NixOS 21.11 | ThinkCentre Edge something-something |Experimental box if I need something slightly stronger then a Raspberry Pi.|
| PicturePlanck | Debian 11 | Raspberry Pi 4B |Home server.|
| voidBerry | Debian 11 | Raspberry Pi 4B |Light entertainment box.|
# Software used by [$HUMANOID](#HUMANOID)
| category | programs |
|---|---|
| Window manager: | DWM, XMonad
| Graphical browser: | FireFox, LibreWolf, UnGoogled Chromium, Brave, Qutebrowser
| Text browser: | Lynx, w3m
| Gemini client: | Lagrange, Amfora
| Terminal: | ST, Kitty
| Text editors: | Vim, NeoVim VSCodium with NVim plugin
| Image viewer: | sxiv, nsxiv
| Video player: | MPV
| Music player: | MPD + NCMPCPP

8
src/content/ideas.md Normal file
View file

@ -0,0 +1,8 @@
---
title: "Ideas"
date: 2023-03-26T22:21:28+02:00
description: "List of ideas hacked together for a friend"
---
- Car bine
- Gun named Tommy (Chicago mafia accent)
- Pee/Pea shooter

79
src/content/insanity.md Normal file
View file

@ -0,0 +1,79 @@
---
title: "Aural Insanity"
date: 2022-08-10T15:09:31+02:00
description: "Insanity of the aural variety."
---
# Bid Farewel to your Sanity
## A thing consisting of 4 audio tracks.
The tracks used in this... thing are from the following games:
|Track Source | Channel |
|---------------------------------|---------|
|Don\'t Starve OST - Ragtime | Center |
|Blood - The Chanting track | Center |
|Painkiller - The Carnival level | Left |
|Blood - The Mall/Elevator music | Right |
This is a file dedicated to aural insanity produced every now and then
{{< audio src=/audio/insanity.wav audio=wav >}}
# Anvil
This is the result of deciding to mess around with LMMS, a distorted Minecraft anvil sound ripped from YouTube and having a friend nearby with knowledge on how to create music (in other words, a musician).
I made an anvil melody of sorts and Vlams added a drum sample loop and a few synthesizers.
{{< audio src=/audio/aaaaanvil.flac audio=flac >}}
# Orca
Some [Orca experiments.](/rambles/orca/)
## plinkinator
{{< start-details summary="Orca source" >}}
```orca
..................
..nVC.............
..................
5U9..Vn..4U9..Vn..
.*:04C....*:03C...
..................
3U5..Vn...........
..:05C............
..................
3U8..Vn...........
.*:06C............
..................
```
{{< end-details >}}
### First result
{{< audio src=/audio/orca/plinkinator.flac audio=flac >}}
### Later variant
{{< audio src="/audio/orca/varied_plinkinator.flac" audio=flac >}}
## Shepard tone
{{< start-details summary="Orca source" >}}
```orca
.C8C7........
.1.67TABCDEFG
.J.3XG.......
.18T1234567..
...2.........
D1.J.........
*:02G.2......
```
{{< end-details >}}
### Climb
{{< audio src="/audio/orca/climb.flac" audio=flac >}}
### Fall
Same as 'climb' but with the note and octave loops reversed.
{{< audio src="/audio/orca/fall.flac" audio=flac >}}

View file

@ -0,0 +1,57 @@
---
title: "Adblock"
date: "2022-08-22T23:46:09+02:00"
author: "$HUMANOID"
tags: ["internet", "advertisements"]
description: "The morality and utility of adblockers"
---
### UBlock Origin
Before I even get started, I'm going to tell you to use [UBlock Origin](https://ublockorigin.com/).
It is by far the most configurable and extensive adblocker that I know of.
I use it in every browser that supports it.
# Moral Conundrums
You often hear people proclaim that blocking ads is bad for the internet the way that it is.
After which they will say that the alternative is to pay for every now-free aspect of the internet.
Usually through something like subscriptions.
My response is to this is "Have you seen how popular crowd funding has gotten over the last few years?"
People already frequently pay for the content they want to consume.
Sure, it might be less than one might get through ad revenue, but that pays off in not having to shill to daddy {Google,Facebook,Apple,Amazon}.
This in turn means not indirectly violating your user's privacy.
But let's analyse that a bit further.
The claim here is that the lack of advertisements would be bad, because it would mean large portions of the internet would become subscription based in some way.
For one, I'd argue that the presence of ads on the internet has done more harm than good, going as far as saying that, no, the internet would be better off without ads plastered everywhere.
This is never going to happen as there is, understandably a cooperate interest in the internet.
And two, not-insignificant portions of the internet _already are_ subscription based.
{{< hyperbowl content="By blocking ads you are stealing from creators!" >}}
<!--Sometimes you will even come across people who go as far as claiming that blocking ads is theft.-->
Sometimes you will even come across people who go as far to genuinely proclaim the preceding.
To those people, the only thing I can do is quote the classic "You wouldn't download a car, would you?"
Shortly followed by saying "Yes I fucking would, given the opportunity"
No one owes you shit based on the amount of people have ignored the ads on your website.
Then there is also the argument to be made that ads steal one's attention.
The goal here being to manipulate you into buying a product.
Much like with piracy, the you're not missing out on anything.
The only way you would've seen any money from it, would be when you're audience directly sees your content and happens to get an ad shoved in their face.
Though I suppose in the case of ads, your audience has no choice but to support you when they don't use an adblocker.
In other words, you are forcing your audience to sacrifice their a part attention to support you, whether they want to or not.
In my opinion, this makes it morally wrong to make money off of ads.
I'd argue it is a moral imperative to use an adblocker.
Then there is also the angle of corporations focussing on ads more and more.
This in turn means that avoiding an absurd portion of cooperate influence has become hilariously easy;
Just install an adblocker that isn't sponsored by an ad company.
The thing is that most people are too lazy to install an adblocker and thus ads are still a viable business model.
Subscription services such as the one that started as a DVD and VHS rental business and the other one with a monopoly over a significant portion of pop culture, have been working towards ad-supported tiers while drastically increasing the price of their paid tiers.
It would be pretty funny if these ads could be avoided using UBlock Origin.
Personally, I prefer using MPV wherever possible, so I probably won't find out how well that might work.

View file

@ -0,0 +1,79 @@
---
title: Browsers
date: 2022-04-04T02:54:33+02:00
author: $HUMANOID
description: "A little ramble mainly about browsers with Vi-like bindings"
tags: ["vim", "linux"]
---
# Web Browsers With Vim Bindings
If there's one thing I like it's Vim bindings everywhere.
Since all Vim browser extentions suck on some level, the remaining choice I have is to use browser with built in vim bindings.
To that end, there are technically a few options.
Those being:
- [Qutebrowser](https://qutebrowser.org)
- [Vieb](https://vieb.dev)
- [Vimb](https://fanglingsu.github.io/vimb)
- [Nyxt](https://nyxt.atlas.engineer)
These projects are great but all have their problems.
Let's go through them bottom-to-top.
## Nyxt
Nyxt is written in Common Lisp, making it very easy to hack on.
Supports either QtWebEngine or WebKitGTK.
It also appeals more to the Emacs world then the Vim world.
Much like Emacs, Nyxt _does_ have the option of Vim bindings, but they feel somewhat half baked.
Then there is the problem of what little interface there is not being particularly clear.
To be fair, I haven't really given it a proper shot because it's configured on Common fucking Lisp.
Not being much of an Emacs user, I have little to no interest in learning Lisp to configure a browser.
## Vimb
The suckless option.
Uses WebKitGTK.
Is written in C and configured in a vimscript-esque syntax.
The thing that turns me off from this browser is it being _too_ suckless.
It almost feels like surf with a slightly more accessible way of configuring it through something that _isn't_ a header file.
Practical reasons that prevent me from using it are the lack of proper ad blocking and tabs.
By default, it also uses the number keys for URL hinting instead of the home row.
It also has similar performance to suckless' surf
In other words, it takes quite a long time to load pages, especially if they contain a large amount of JavaScript (though you shouldn't be visiting those pages unless strictly necessary) and rather quickly hogs quite a lot of resources.
## Vieb
An Electron based browser.
Say whatever you want about Vimb's performance, but Vieb makes Google Chrome seem like a well balanced product.
I've seen it eat 25% of a CPU thread with a single tab containing nothing but the Vieb documentation.
It has a rather pretty interface and could (if the github page is to be believed) be used to interact with other Electron applications like the Discord client as if it were ran natively (don't quote me on that, I'm too lazy to check and probably wrong).
## Qutebrowser
Which brings us here.
Qutebrowser is written and configured in Python; and uses the QtWebEngine.
It's extremely flexible and easy to configure.
It has reasonable ad blocking that allows for the addition of blocklists in a plain text file.
A problem I have with it are that I can't choose which JavaScript elements to block and which to allow, like UMatrix.
But then I did see plans to add support for Firefox extensions which would solve this problem.
Another minor gripe I have is that scrolling through long pages can look a little sluggish.
With these changes and (if it weren't a gigantic nightmare) Gecko support, it would be the perfect browser for me.
# Other Web Browsers
When I need to use a site that requires some JS, but still functions without enabling everything, my choice of browser is either Librewolf or a heavily configured Firefox.
From there I use the following list of extensions to make the web usable:
- [vim-vixen](https://github.com/ueokande/vim-vixen)
- [uBlockOrigin](https://github.com/gorhill/uBlock) (Installed by default on Librewolf)
- [redirector](https://github.com/einaregilsson/Redirector)
- [ToS;DR](https://github.com/tosdr/browser-extensions)

View file

@ -0,0 +1,133 @@
---
title: "Emergent Game Mechanics"
date: "2023-09-25T02:36:11+02:00"
author: "$HUMANOID"
tags: ["gamedesign", "quake", "doom"]
description: "A little piece about emergent mechanics"
---
# Preamble
I usually talk about whatever hobby project I've been hacking together when the
inspiration to write strikes. In a sense this is a similar ramble, just on a
wildly different topic, namely video game design.
# The Meat and Potatoes
For some time now I've been attempting to wrap my head around why I find games
like Doom Eternal to be boring, while games like Ultrakill and Quake are some of
my favourites of all time. And I think I've found the answer.
Doom Eternal has location based damage and various enemy weak spots. For
instance, shooting the Mancubus' arm cannons causes them to be disabled, making
the Mancubus significantly more manageable. Likewise, shooting a grenade at a
Cacodemon causes it swallow the grenade and be stunned for an easy Glory Kill.
The Arachnotron has a turret that can be shot and disabled; the Makyr Drones can
be shot in the head as quick ammo piñatas; the shields on the dudes carrying
them can be easily disabled using the Plasma Rifle, causing a shock wave... so
on and so forth.
Initially, I found these to be really neat ideas. However, it rather quickly
became boring when I found out shooting the weak spots is really the only viable
way to get rid of enemies, especially on higher difficulties. Sure, Doom Eternal
allows you to be very creative in _how_ you shoot those weak spots, but the
moment you stop bothering with them, the difficulty curve starts to resemble
something closer to a vertical line than an actual curve. The end result is that
the combat feels pre-baked and static if you want to be remotely efficient.
Ultrakill does this right. It has -- as far as I'm aware -- two ways the
trajectory of projectiles can be influenced. One being by punching a projectile
as it is about to hit you. The other is with a shock wave, often caused by an
explosion. Shock waves can be caused using the Knuckleduster (heavy melee
attack), the Core Eject (shotgun altfire that shoots a grenade), the rocket
launcher, parrying your own shotgun blast as it exists the barrel and several
that I can't think of right now. This is the only rule regarding projectiles
that is set in stone. Because of this simplicity and how many enemies have
projectile attacks, you can get very creative in how you kill them, while
keeping all approaches equally viable at any given moment. When an enemy has
just shot a projectile at you, you can chose to dodge it, answer it with a
shotgun blast; punch it back to the culprit using a light melee attack; switch
to heavy melee and shoot it back using a shock wave; overcharge the pump action
shotgun, dodge out of its explosion damage using the I-frames received when
dodging, while using said same explosion to send the projectile back to the
unsuspecting enemy; use those I-frames from the dodge to go through the
projectile and punch the enemy in the face retrieving some health in the
process, etc, etc.
All of these are viable stratagies depending on how much health you still have
and how many other enemies there are. Getting out of the way of overcharge
explosion damage by dodging is no small feat, but might save you from more
damage if you're surrounded by a lot of high level enemies. Punching a single
enemy isn't something you'll want to do when still surrounded as it means doing
less damage in that moment and only damaging a single enemy. I could go on and
on, describing how and when one might chose any given strategy, but I'll try to
stop here.
The result is an incredibly dynamic combat system that allows for endless
creativity in dealing with any given scenario. If Ultrakill's design philosophy
were closer to that of Doom Eternal, there would be a dedicated "redirect
projectile" button/weapon. It wouldn't serve much of a purpose outside of
deflecting the occasional projectile. On top of that, each enemy attack would
have one and only one viable way to answer it.
For me an encounter in Doom Eternal would go something like the following:
> Mancubus, take out its cannons using machine gun altfire; Arachnotron, ditto;
> Cacodemon, switch to shotgun, shoot grenade using altfire, Glory Kill; "oh
> hey, shield dudes", switch to Plasma Rifle and pepper them a bit...
There is (usually) one "proper" strategy to deal with each enemy. As a result,
instead of improvising throughout the entire fight, I'm focussing on what hoops
to jump through and in what order. And I guess I don't like being treated as a
circus lion. In Ultrakill, I just do what comes natural in any given situation
without feeling like I'm being forced down a rather
face-paced-yet-impressively-boring path.
Doom Eternal's movement is even more strict. There is a double jump, double dash
and later on a grapple hook. Sure, there are some interesting things you can do
here, but short of exploiting the physics engine by manipulating the framerate
by opening the weapon wheel, you're not going to see any crazy movement
strategies.
Ultrakill has a triple dash, a slide, dash jump, ground pound and the player
character can be knocked about by explosions and shock waves. On top of that,
there is also the very intricate interplay between these elements due to how the
physics engine works.
The result here is a set of extremely complex mechanics that are never set in
stone; instead emerging from a few laws of the game that don't have total death
grip control over their domain.
There is no way I could write an article about emergent mechanics without
mentioning Quake. So lets do that now.
None of the high level mechanics in Quake are intended. Everything from
straferunning, to wallrunning, to rocket jumping, to bunnyhopping and power
bunnyhopping (also called strafejumping in the context of Quake) is the result
of a few basic rules and wack physics.
1. "The player can turn"
2. "The player can move in 8 cardinal directions"
3. "The player can jump"
4. "The player can sprint, increasing their speed"
5. "There are entities that are affected by physics"
6. "The player is one such entity"
And that's it as far as I'm aware.
All the of the complex movement mechanics emerge from the interplay of these
rules. One of the first that was found was straferunning. What this means is
sending both a move-forward and strafe input. Due to the way velocity is
calculated, this results in a slight speed increase. Shortly after this, it was
discovered that doing this while running into a wall causes a greater speed
increase. As did wiggling the mouse left and right. When jumping you don't lose
any momentum. Put all of these together -- with the exception of wallrunning --
add little finesse on top and the result is strafejumping.
To me there is something about using these emergent mechanics that is infinitely
more satisfying than the pre baked equivalents in modern games. Figuring out how
to (ab)use the game's rules to your advantage is half the fun if you ask me and
games like Doom Eternal take most of that away from you by giving you one or
very few viable strategies to approach a given situation and then telling you
most of them from the get go. Getting it to do something that the developers
never originally intended.

View file

@ -0,0 +1,126 @@
---
title: 'Hosting your own Git "server"'
date: "2022-09-30T14:27:31+02:00"
author: "$HUMANOID"
tags: ["git", "ssh"]
description: "The basis of a Git server is a machine accessible in a way that Git understands with a bunch of bare repositories"
toc: true
---
# Introduction
Pretty much the moment I started to work with Git a few years ago, I've wanted to host my own server.
I didn't really bother looking into how that could work for a very long time.
I assumed it would be rather complex considering Git itself is an extremely complex (set of) tool(s) to work with.
I was hilariously wrong.
It's _so_ simple to me that I'm under the impression that the lack of posts regarding the topic on blogs like this, is because it's so simple.
Still, I'm going to spend the rest of this post explaining how to set up a server and why even calling a basic configuration a server, is a bit of an exaggeration.
# Setting up the server
The first step is to set up a Unix box of any kind.
Linux, any of the BSDs and probably even Darwin will work (hell, even Haiku might suffice).
The rest of this post assumes a Debian system (or rather, that you have the core GNUtils available).
Basics requirements are quite few:
- It needs to be accessible using SSH.
- It needs Git installed.
## Adding an account
Common practice dictates that you have a dedicated account for managing your Git repositories.
Common practice again dictates that this account be called `git`.
Its home directory doesn't really matter, but I like tossing it into `/srv/git`.
Login shell also doesn't matter _yet_, I tend to change it to `bash` for when I need to do things as the `git` user.
You can create an account with the following:
```sh
sudo useradd git -md /srv/git -s /bin/bash
```
- `-d` specifies where the home directory should be
- `-m` creates the home directory
- `-s` specifies what binary to use as login shell
## Setting up SSH
### Client side
On the machine from where you want to push your git repositories, generate a new SSH key to access your Git server:
```sh
ssh-keygen -t ed25519 -f ~/.ssh/<git-key>
```
I also recommend creating an entry in `~/.ssh/config` for your Git server:
```ssh_config
Host <host>-git
Hostname <hostname>
User git
Identityfile ~/.ssh/<git-key>
```
### Server side
Next step is to make the `git` account accessible over SSH.
`ssh-copy-id` won't work because the `git` user doesn't have a password.
So instead, log in as the `git` user and create a `.ssh` directory with an `authorized_keys` file with the public half of the `<git-key>` keypair:
```sh
sudo su - git
umask 077 # strict read write permissions
mkdir ~/.ssh
cat <git-key>.pub >> ~/.ssh/authorized_keys
```
## Adding a repository
You should now be able to log into your host as the `git` user with using `<git-key>`.
Setting up the route from client to server is half the work.
Next step.
Make this actually serve Git repositories.
All this requires is a bare Git repository somewhere within `/srv/git`.
To do this, log in as the `git` user and run:
```sh
git init --bare <repo-name>.git
```
That's it.
You now have a Git "server" with a repo named `<repo-name>`.
To add something to this repo, on your client machine create a new repository and add `<host>-git:/srv/git/<repo-name>.git` as remote and push your project to it.
Common practice dictates that this remote be called `origin`:
```sh
mkdir my-fancy-project
cd my-fancy-project
git init
git remote add origin <host>-git:/srv/git/<repo-name>.git
echo "# Self-hosted repo!" > README.md
git add -A
git commit -m "Initial commit"
git push -u origin main # assuming you use main as your default branch name
```
To clone this repository on another machine, add the `<git-key>` SSH keypair and related configuration section in `~/.ssh/config` to said machine and run:
```sh
git clone <host>-git:/srv/git/<repo-name>.git
```
# Automation
Creating bare repositories by hand gets annoying quite quickly.
I have an SSH forced command for another key that runs a script with the following:
```sh
#!/bin/sh
BASELOC="/srv/git"
echo "Enter new repo name:"
read REPONAME
[ -z $REPONAME ] && echo "No repo initialised" && exit 1
git init --bare $BASELOC/$REPONAME.git
```
`authorized_keys` entry for `git` user:
```authorized_keys
command="/srv/git/.bin/new-repo" ssh-ed25519 <new-repo-git-key>.pub
```
Another thing you could do is change the login shell binary to a script that does something similar.
I haven't bothered to do so for reasons that presently escape me.

View file

@ -0,0 +1,53 @@
---
title: "Hugo"
date: "2022-08-18T14:53:00+02:00"
author: "$HUMANOID"
tags: ["hugo"]
description: "A little ramble about Hugo"
---
# Reflection
I've tried to take a look at [Hugo](https://gohugo.io) for a few times before properly diving into it.
The first time was back when I first decided to look into making a website not-completely by hand.
I had hacked a few previous iterations of this site together and was happy with the design.
The next step was to write some blobs of text for people to read besides the random antics that can be found in the side bar.
A few problems arose here.
For one, writing directly in HTML is a pain.
So I decided to write in markdown and convert that to HTML using [Pandoc](https://pandoc.org).
From there, I still had to index every page completely by hand.
The prospect of this annoyed me quite a bit, so I decided to take a cursory glance at Hugo.
The quick start guide showed how to take a pre-existing theme, a bunch of markdown files and turn that into website.
Never having been one to use other people's work as-is, I quickly gave up when I saw the structure of the themes and couldn't be bothered to give it a proper shot.
This also gave me the impression of it being a Wordpress-esque environment that works great as long as you stick within the standards.
A bit later I found out that Pandoc supports templates.
So I tried my hand at hacking a site generator together based on a Makefile and Pandoc.
This worked... somewhat.
But I deemed it more trouble than it was worth and never got very far with it.
A few weeks ago, I came across someone mentioning Hugo again and how it was really great.
I decided to give it another shot.
This time really diving into the structure of the themes and it clicked.
A thing that probably helped this time around was that it very quickly became clear to me that the features I had been half arsedly implementing in my Makefile were standard features for Hugo.
Another thing that undoubtedly helped being that the philosophy and syntax of Pandoc's more advanced features are quite similar to Hugo's.
## Disservice to Hugo
One big thing I bump into with Hugo is the fact that most Hugo websites I come across take one template and _maybe_ change the colorscheme.
I feel this is a great disservice to the potential that Hugo has to offer in terms of flexibility.
Yes, you _can_ have it act similar to Wordpress, but you don't _have to_.
I was scared off because I assumed would have to use some existing template or get my CSS to work within a Hugo context.
The process of which, I assumed, would be mostly having beat Hugo into submission before it would display the things I wanted in the way I wanted.
The beauty of Hugo and ultimately the reason this is now a Hugo site, is it's flexibility.
You wouldn't be able to tell this is a Hugo site if you don't dig through the metadata fields in the `<head>` tag.
And I guess that is also kind of the problem with it.
Most people visibly using Hugo take some standard template proudly proclaiming that the page we're presently looking at is generated using it.
And the people who want to use Hugo but don't care for the pre-existing themes, don't care to proudly shout to the world that their site is made with it.
This is why I got the impressions of Hugo that I did.
In that sense, I'm part of the problem in not showing that Hugo can do pretty much anything you can think of in the realm of static, somewhat blog oriented websites.

View file

@ -0,0 +1,232 @@
---
title: "I2p on NixOS"
date: 2023-07-27T00:21:37+02:00
draft: true
author: "$HUMANOID"
tags: ["nix", "linux", "i2p"]
description: "My i2p configuration on NixOS"
draft: false
toc: true
---
# Introduction
Recently I've decided to move most of my hardware over to NixOS. It's been an
amazing experience being able to configure everything OS related from one
central point.
A few years ago, I discovered [i2p](https://geti2p.net/en/). I really liked the
idea of it; a different darknet than tor with different kinds of people on it --
more blog oriented. At the time I ran a node on a Raspberry Pi running Debian. I
connected to it using an ssh tunnel over
[Yggdrasil](https://yggdrasil-network.github.io/) and used an
[Arkenfox](https://github.com/arkenfox/user.js) hardened Firefox profile to
browse around.
That Raspberry Pi no longer serves as a (har) server. Instead I now have a NixOS
machine churning away. As with a lot of NixOS topics, there isn't a lot
of documentation on configuring i2p. Though truth be told, when comparing it to
the config files I had on Debian, a lot of it was rather self explanatory and
easy to implement in a Nix module.
What follows is my current setup on my desktop. Initially I set this up as a
test environment, but it works well to the point where I don't really feel the
need to move it to another machine.
# The i2p module
I'll be using the `i2pd` implementation of the i2p daemon as it requires less
resources than the java implementation and has less features out of the box that
need configuring.
Since it is safe to assume that the i2p stack is frequently under attack as it
is a darknet protocol, I decided to run it inside of a Nix container. If you
don't want to do this, only declare a `services.i2pd` instance.
With that out of the way, let's declare a basic container environment named
`i2pd-container`.
```nix
{ ... }: {
containers.i2pd-container = {
autoStart = true;
config = { ... }: {
system.stateVersion = "23.05"; # If you don't add a state version, nix will complain at every rebuild
};
}
}
```
From here there are a couple of things you can do with your i2p configuration.
My initial goal was to be able to search the i2p net. To that end, the following
will suffice.
```nix
...
# Exposing the nessecary ports in order to interact with i2p from outside the container
networking.firewall.allowedTCPPorts = [
7070 # default web interface port
4447 # default socks proxy port
4444 # default http proxy port
];
services.i2pd = {
enable = true;
address = "127.0.0.1"; # you may want to set this to 0.0.0.0 if you are planning to use an ssh tunnel
proto = {
http.enable = true;
socksProxy.enable = true;
httpProxy.enable = true;
};
};
...
```
The SOCKS proxy isn't strictly required, but needed when wanting to use anything
else than an HTTP connection to interact with i2p.
## Torrents
After a little while, I wanted to see what kinds of _linux distros_ the i2p
network has to offer, I also enabled `services.i2pd.proto.sam` and added the
corresponding default port to the container's firewall in order to get
[XD](https://xd-torrent.github.io/) (which can be found in the Nix repo) to
behave properly:
```nix
...
networking.firewall.allowedTCPPorts = [ 7656 ];
services.i2pd.proto.sam.enable = true;
...
```
{{< start-details summary="Click here to see everything together" >}}
```nix
{ ... }: {
containers.i2pd-container = {
autoStart = true;
config = { ... }: {
system.stateVersion = "23.05"; # If you don't add a state version, nix will complain at every rebuild
# Exposing the nessecary ports in order to interact with i2p from outside the container
networking.firewall.allowedTCPPorts = [
7656 # default sam port
7070 # default web interface port
4447 # default socks proxy port
4444 # default http proxy port
];
services.i2pd = {
enable = true;
address = "127.0.0.1"; # you may want to set this to 0.0.0.0 if you are planning to use an ssh tunnel
proto = {
http.enable = true;
socksProxy.enable = true;
httpProxy.enable = true;
sam.enable = true;
};
};
};
}
}
```
{{< end-details >}}
# Tying it to `/etc/nixos/configuration.nix`
I have a perhaps somewhat over engineered structure when it comes to adding my
modules to `/etc/nixos/configuration.nix`.
I have a `/etc/nixos/services` directory with my own modules, one of which being
`services.nix`. `services.nix` imports all the other modules found in the
directory. From there, `/etc/nixos/configuration.nix` has
`./services/services.nix` in it's import section:
`/etc/nixos/configuration.nix`:
```nix
{ pkgs, config, ... }: {
imports = [
./hardware-configuration.nix
./services/services.nix
...
];
...
}
```
`/etc/nixos/services/services.nix`:
```nix
{ ... }: {
imports = [
./i2pd-container.nix
...
];
}
```
# Browser profile
The next step was to set up a browser profile. Since Mullvad recently released a
browser, I decided to use it as a basis for my i2p browser.
To create a new profile in Firefox or any of it's forks, type/paste
`about:profiles` in the URL bar and click the "Create a New Profile" button at
the top. You will then be prompted for a name. I named mine "i2p".
Open this profile in a new session and navigate to `about:preferencess#general`
and scroll all the way down to "Network Settings". In there, check "Manual proxy
configuration" and "Also use this proxy for HTTPS". From there, fill in the
address you used in `services.i2pd.address` or the address of the machine you're
tying it to if you're using an ssh tunnel. For ports, use `4444` for the HTTP
proxy and `4447` for the SOCKS proxy.
Then move over to `about:preferencess#privacy` and scroll down to "Security". I
recommend putting it to "Safest". If you _really need_ to run any JavaScript,
you can always add the NoScript icon to your window bar.
Then scroll a bit further down to "HTTPS-Only Mode" and disable it, as the i2p
network doesn't use browser level encryption and thus it would only get in the
way.
As a final step, go to `about:config` and set `keyword.enabled` to false. This
isn't strictly necessary, but will make your like a whole lot easier when typing
`.i2p` domains and not having your browser attempt -- and fail -- to query the
default search engine.
## `.desktop` entry via `home-manager`
If you're using the `home-manager`, you can quite easily create a `.desktop`
file to make starting your i2p profile more convenient. This is assuming that
have either a full on desktop environment or use some other way of parsing
`.desktop` entries such as `rofi`'s `drun` mode.
I have the following in my `$HOME/.config/home-manager/home.nix`:
```nix
xdg.desktopEntries = {
i2p-browser = {
name = "i2p Browser";
genericName = "Web Browser";
exec = "${pkgs.mullvad-browser}/bin/mullvad-browser -p i2p";
};
};
```
Firefox and it's forks allow you to start a session with a profile using the
`-p` parameter. Similarly the `-P` (that is, an upper case 'P' rather than lower
case) parameter brings up a profiles menu that allows you to easily change the
default profile when running the binary the next time.
# Using everything
After starting the container, wait at least 15 minutes for i2pd to build a few
tunnels before attempting to connect to anything. Once you have, open your
i2p browser profile and you should be able to browse the i2p net. Don't expect
things to be fast however, i2p is a much slower network in my experience than
Tor.
Some pages to get you started:
- [reg.i2p](http://reg.i2p) - link index
- [stats.i2p](http://stats.i2p/) - link index
- [notbob.i2p](http://notbob.i2p/) - link index
- [My site](http://lmwr2pugnmv4pkkxlneeepi4oysfly33zfuj7x25s6hfydysnpfq.b32.i2p/)

View file

@ -0,0 +1,183 @@
---
title: "Mpd and Icecast on Nix"
date: "2022-11-06T22:11:10+01:00"
author: "$HUMANOID"
tags: ["nix", "linux"]
description: "Ramble about setting up MPD + Icecast on NixOS"
draft: True
---
# Introduction
For quite some time now, I've been wanting to move my home server from Debian to
NixOS. There were a few things that kept me from starting with the migration.
The main one being that the Debian installation still worked reasonably well. I
only had occasional hickups in regards to the two USB disks hooked up to my
Raspberry Pi 4. Every now and then they'd disappear. My guess is that it was due
to power draw as it would consistently happen when putting heavy IO loads on
either disk. One of these disks is home to my music collection and used by my
internet radio setup (in other words, the main thing I listen to when at home).
And this nicely brings my to my next issue.
{{< img class="stickers" src="/images/graphs/radio.png" mouse="Flowchart demonstrating software stack">}}
This radio consists of an MPD instance streaming to an Icecast2 instance,
being reverse proxied through an NGINX instance (future plans include managing
the music collection through Git Annex...). I have tried setting up MPD on NixOS
in the past, but couldn't get it to work for some odd reason. I don't remember
why I never managed it. I just know that I gave up after a few hours. Today I
decided to spend as much time as necessary to get at least the MPD + Icecast2
portion to work. The rest of this post will be detailing how I went about
setting configuring MPD + Icecast2 on NixOS as there were a few non-obvious
things I bumped into.
# Setting up MPD
To prevent my `/etc/nixos/configuration.nix` from ballooning into absurdity,
you'll want to create something like a `/etc/nixos/services/radio.nix` file
containing everything related to the radio stack. This file is then imported in
`/etc/nixos/configration.nix` in the `imports` section as follows:
```nix
{ config, pkgs, ... }: {
imports =
[ # Include the results of the hardware scan.
./hardware-configuration.nix
./drives.nix
./services/radio.nix
...
];
...
}
```
The first thing you'll want to do is enable the MPD service:
```nix
{ config, pkgs, ... }: {
services.mpd = {
enable = true;
...
```
From here, there are a few rather basic options that you'll want to set:
```nix
...
musicDirectory = "/path/to/music-collection/";
network.listenAddress = "any"; # not quite sure if this is needed
...
```
At the `extraConfig` section is where things get interesting. Here I assumed you
could point Nix to an existing config file with `(builtins.readFile
/path/to/mpd.conf)` but it seems I am wrong. It doesn't throw any errors when
switching to a new build, but it doesn't read the given config file either. I'm
probably missing something here. Please shout at me if you happen to know what
it is.
Regardless, you're going to want to add at least one audio output method. Since
we're going to make MPD talk to Icecast2, we're going to add `audio_output`
section of type "shout":
```nix
...
musicDirectory = "/path/to/music-collection/";
extraConfig = ''
audio_output {
type "shout"
encoder "vorbis" # FOSS codec for the win
name "My Shouty Stream"
host "<name found in the networking.hostName variable>"
port "8000" # or use whatever meme you like
mount "/mpd.ogg"
password "hunter2"
quality "5.0"
format "44100:16:2"
genre "Rythmic Noise!"
protocol "icecast2"
}
'';
};
...
```
Please don't take this snippet as gospel and read through the MPD configuration.
Whatever is found in `extraConfig` will be directly given to MPD as if through
`/etc/mpd.conf` on e.g. Debian. For instance, if you only locally want to listen
to your music through say PulseAudio, than you'll need a corresponding output
section.
{{< start-details summary="Example PulseAudio configuration I have on my laptop" >}}
```conf
audio_output {
type "pulse"
name "Pulse output"
audio_output_format "44100:16:2"
samplerate_converter "Medium Sinc Interpolator"
mixer_type "software"
replaygain "album"
volume_normalization "no"
}
```
{{< end-details >}}
## Mounting a music disk
In order to automount a disk on NixOS, add an expression something like the
following (indirectly) in your `/etc/nixos/configration.nix`:
```nix
fileSystems."/srv/music" = {
device = "/dev/sdb1"; # assuming that sdb1 is the partition with your music on it
# device = "/dev/disk/by-label/Sauserer"; # or ideally, if you have a labelled disk
fsType = "ext4"; # assuming it has a ext4 filesystem on it
};
```
I have my drives listed in `/etc/nixos/drives.nix` and import that in my
`configration.nix`
# Setting up Icecast2
The next step is to set up Icecast2. Same as with MPD, the first thing you'll
want to do is enable it...
```nix
...
services.icecast = {
enable = true;
...
```
... then add at least the necessary lines of configuration...
```nix
...
hostname = "hostname:8000";
listen.port = 8000;
admin.password = "it gives me a headache that I can't get Nix to read this from a file, I am probably too much of an idiot figure out how this works";
...
```
```nix
extraConf = ''
<location>Floating around in an example configuration snippet on the internet</location>
<admin>icemaster@localhost</admin>
<authentication>
<source-password>hunter2</source-password>
<relay-password>even-more-super-secret-password</relay-password>
</authentication>
<limits>
<clients>100</clients>
<sources>2</sources>
<queue-size>524288</queue-size>
<client-timeout>30</client-timeout>
<header-timeout>15</header-timeout>
<source-timeout>10</source-timeout>
<burst-on-connect>1</burst-on-connect>
<burst-size>65535</burst-size>
</limits>
'';
};
}
```

View file

@ -0,0 +1,161 @@
---
title: "Nix Home Manager"
date: "2022-09-19T21:49:46+02:00"
author: "$HUMANOID"
tags: ["nix", "linux"]
description: "Basics of using the Nix Home Manager in a way that makes sense to me and probably just me"
draft: false
toc: true
---
# The Home Manager
The Nix Home Manager is a way to work with Nix in a declarative environment to manage your dotfiles and user environment packages.
This can be used on any system running the Nix packagemanager.
Personally, I've started to dabble around with it a bit after installing the Nix packagemanager in my Alpine installation.
Since I've already dabbled with Nix on a few systems through NixOS, I already knew the basics of functionally managing packages (or derivations) with the Nix packagemanager.
Hence I really quickly got sick of installing packages the imperative way.
After looking around for a bit, I found some post mentioning that managing packages in a functional environment outside of NixOS is a bit of a hack.
The post referred to some other sources, one of which used a custom meta package and another referred to the [Home Manager](https://nix-community.github.io/home-manager/).
I had messed around with the Nix Home Manager a tiny bit before, but never _really_ gave it a chance to shine as I didn't really understand it.
This time around, I decided to look up some tutorials and see how it worked.
When going through the instructions of either tutorial I used, I quickly came to the realisation why I had never _really_ looked at it.
Both tutorials took a lot of unnecessary steps according to the author's will and assumed that everyone following their tutorials had the same preferences as them.
I obviously don't.
So here's my take on installing the Home Manager outside of NixOS.
# Actually using Home Manager
Assuming you're continuing from [my previous article on Nix](/rambles/nix-on-other-distros-packagemanagers), the first step is to go to the Home Manager github page and go to the [page regarding the standalone installation](https://nix-community.github.io/home-manager/index.html#sec-install-standalone).
From there, things will be quite self explanatory if you're used to the (bare) basics of NixOS.
Though instead of using `/etc/nixos/configuration.nix` you'll be using `$HOME/.config/nixpkgs/home.nix` by default.
A basic Home Manager installation will leave you with a `home.nix` file with the following content:
```nix
{ config, pkgs, ... }:
{
# Home Manager needs a bit of information about you and the
# paths it should manage.
home.username = "$USER";
home.homeDirectory = "/home/$USER";
# This value determines the Home Manager release that your
# configuration is compatible with. This helps avoid breakage
# when a new Home Manager release introduces backwards
# incompatible changes.
#
# You can update Home Manager without changing this value. See
# the Home Manager release notes for a list of state version
# changes in each release.
home.stateVersion = "22.05";
# Let Home Manager install and manage itself.
programs.home-manager.enable = true;
}
```
After making any changes to your `home.nix` file, you can apply them with:
```sh
home-manager switch
```
If you first want to see whether you build is going to be successful or not, run:
```sh
home-manager build
```
## Installing packages
To add some packages, you'll need to add them to the `home.packages` array.
On my Alpine installation I have the following:
```nix
...
home.packages = with pkgs; [
brave
vscodium
];
...
```
Notice the `with pkgs;` section.
This prevents you from having to add the `pkgs` prefix to every package you want to add.
I don't think this is the idiomatic way of adding packages to your configuration, but it allows me to be a bit lazier and it hasn't caused any breakages yet.
## Managing dotfiles and configuration
Another amazing thing the Home Manager can do is manage your dotfiles.
And this in turn can be managed with Git.
More on this later.
### Git configuration
For instance, I have it manage my Git config using the git module.
To do this, I have the something like the following in my `home.nix`:
```nix
...
programs.git = {
enable = true;
ignores = [ "*.swp" ]; # I don't need to see that I still have a file open in vim
signing = {
key = "<gpg-fingerprint>";
signByDefault = false; # it would probably be better for security to have this be true, but doing so gets annoying really fast
};
userEmail = "<user-email>";
userName = "<user-name>";
extraConfig = {
init = {
defaultBranch = "main";
};
};
};
...
```
Here's the [list of options](https://nix-community.github.io/home-manager/options.html#opt-programs.git.enable) supported by the git module.
I _strongly_ recommend digging through the documentation, looking for things that interest you in your current situation and setup.
### Integrating existing (dot)files
The Home Manager can also manage arbitrary (dot)files for you.
I have it link my `.zshrc` into place with the following line:
```nix
...
home.file.".zshrc".source = ./zshrc;
...
```
This looks for a file called `zshrc` in the same directory as `home.nix`.
From there, it symlinks it to `~/.zshrc`
It can also manage recursive file structures.
I have it keep track of my `sxiv` configuration with the following few lines:
```nix
...
home.file.".config/sxiv" = {
source = ./sxiv;
recursive = true;
};
...
```
The interesting portion of this snippet is the `recursive` boolean.
Because of this, Home Manager will recreate the directory structure found in `./sxiv` in `~/.config/sxiv` and symlinks the files found inside into place.
# Git integration
Since all the Home Manager requires is (at least) a single text file to manage your dotfiles and Nix environment packages, it's really easy to keep track of your configuration using Git.
To that end, I have a bare repository on one of my home servers over at `/srv/git/nix/<nix-configuration>.git`
As a remote, I have pointed my repo in `$HOME/.config/nixpkgs/` to `<server-name>:/srv/git/nix/<nix-configuration>.git`.
This works over ssh.
My `$HOME/.ssh/config` contains the following lines to make the preceding work:
```ssh
Host <server-name>
Hostname <ip-address>
User git
Identityfile ~/.ssh/<private-key>
Port 4242
IdentitiesOnly yes # this makes using an ssh agent a bit easier when using multiple keys on the same host
```

View file

@ -0,0 +1,159 @@
---
title: "Nix on Other Distros' Packagemangers"
date: "2022-09-12T11:37:11+02:00"
author: "$HUMANOID"
tags: ["nix", "linux"]
description: "A guide on installing the Nix packagemanger on Alpine and Debian"
toc: true
---
# Introduction
The Nix package manager is an amazing tool that allows you to manage your packages through a purely functional environment.
I'm not going to get into why it's amazing or how it really works in this article.
This is purely a guide to installing it in Alpine and Debian Linux.
> "Why not use the official instructions?"
The official instructions require you to `curl` a script directly into `sh`.
From there it requires sudo privileges to install the package manager itself.
This sets of a load of alarm bells in terms of security and what it's going to do from there.
As the Nix package manager is packaged for both Debian and Alpine, my two favourite distros next to NixOS, there is no real reason _not_ to use their respective package managers.
When you try to install Nix on Alpine using the aforementioned installation script you also get the warning that it only supports systemd and that you're on your own when it comes to getting it to work on any other init system (Alpine uses OpenRC).
This problem is fixed in the Alpine package.
Then there is also the benefit of of being able to remove it with either `apt autoremove --purge nix-setup-systemd` or `apk del nix` instead of having to run said same script a second time.
Yes, I'm aware that you don't _have_ to curl the script directly into `sh` and that you can download it to your local system to see what it's actually doing.
But that doesn't take away from the fact that the official instructions tell you to perform an inherently insecure set of actions by trusting what is effectively a random script on the internet.
> "Doesn't this mixing of packagemanagers cause a huge amount of anomalies?"
No, that's part of the beauty of Nix.
Pretty much everything it does lives in `/nix` and is softlinked into place, meaning it never interferes with your existing package manager.
_*DISCLAIMER*_: Debian Stable at time of writing has Nix version `2.3.1`, which appears to have some issues that would be fixed by moving to a later version.
Using the official instructions would sidestep this.
# Installation Process
## Package Installation
The first step is to install the package itself.
On Debian this is done with:
```sh
sudo apt install nix
```
On Alpine, you first have to add the `testing` repo.
This is part of the `edge` branch.
As far as I know, you _could in theory_ get away with just adding the `testing` repo and not moving the rest of your system over to `edge`, but I highly doubt that will do any good for the stability of your system.
Moving your system over to the `edge` branch is done by opening `/etc/apk/repositories`, uncommenting the mirrors referring to `edge` and commenting out the lines referring to (at time of writing) version `3.16`.
From there, run...
```sh
doas apk -U upgrade
```
...to move your system over to `edge`.
Now installing Nix can be done as usual with:
```sh
doas apk add nix
```
This will install the required binaries and the Nix daemon.
## Starting the Daemon
On Debian the Nix daemon is enabled by default.
To make sure it's running and enabled, run:
```sh
sudo systemctl status nix-daemon.service
```
If for whatever reason it's not started and/or enabled, run:
```sh
sudo systemctl enable --now nix-daemon.service
```
On Alpine, the daemon isn't added to any run level by default.
This is done using:
```sh
doas rc-update add nix-daemon
```
Starting it is then done using:
```sh
doas rc-service nix-daemon start
```
## Groups
Next step is to add your account to the `nix-user` or `nix` group depending whether you are on Alpine or Debian respectively.
On Debian this can be done with:
```sh
sudo usermod -aG nix-user $USER
```
And on Alpine this can be done with:
```sh
doas addgroup $USER nix
```
## Environment variables
The next step is to set add the Nix binary dir to your `PATH`.
To that end, I have the following in my `.profile`
```sh
# nix
export PATH=$PATH:$HOME/.nix-profile/bin
```
Now source your `.profile` again, either using...
```sh
source ~/.profile
```
...or by logging out and back in again.
## Adding a channel
Now you have a functioning Nix package manager configuration and all that remains to be done is adding and syncing the repo.
The Alpine package defaults to adding the `unstable` channel.
The Debian package doesn't add any channels at all by default.
This can be verified with:
```sh
nix-channel --list
```
To add the unstable channel to your channels, run the following command:
```sh
nix-channel --add https://nixos.org/channels/nixpkgs-unstable
```
You can probably also add stable channels, but I haven't tried that, as one of the reasons I want to use the Nix package manager on other distros than NixOS is for it's newer packages.
(I am aware that the stable channel of NixOS is a lot newer than the Debian stable branch, which is precisely why I don't want to to be my base system on machines where I have Debian installed.)
## Syncing channels
To sync your (just added) channel(s), run:
```sh
nix-channel --update
```
From this point on you can query your channel(s) using either [the website](https://search.nixos.org/packages) (which is a lot quicker) or:
```sh
nix-env -qa <query>
```
Packages can be imperatively installed using...
```sh
nix-env -iA nixpkgs.<package-name>
```
...and removed using...
```sh
nix-env --uninstall <package-name>
```
Consult [the manual](https://nixos.org/manual/nix/stable/) for further usage.

View file

@ -0,0 +1,37 @@
---
title: "NixVim"
date: "2024-01-15T02:53:23+01:00"
author: "$HUMANOID"
tags: ["nix", "linux", "vim"]
description: "Some thoughts on Vim, NeoVim and NixVim"
draft: true
---
# Vim and Editors in General
So I've been using Vim for nearly as long as I've been using Linux. I still
vaguely remember awkwardly working `vimtutor` for the first time. Before I
touched Vim, I didn't really have any strong preference for an editor on Linux.
I'd just come from Windows where I'd used Notepad++ to edit small text files and
Visual Studio and JetBrains Rider for my college work at the time -- mainly C#
in conjunction with Unity. Sure, these editors got the job done, but they
weren't... Elegant. Visual Studio and Rider are monstrous pieces of software
with several kitchen sinks. And I'm sure Notepad++ has some really cool features
that I've never touched, but then it didn't really feel inviting to me and thus
I never dug deeper. When moving to Linux, I mainly used Gedit to edit the odd
ini file. This got rather tedious when needing root privileges to edit, say an
`sshd_config` file. Sure, you can open Gedit using `sudo`, but that makes the
terminal window from which you start it useless as long as Gedit is running. On
top of that, it being a GTK application means that it spews a lot of output to
`STDOUT`. At this point -- not wanting to dig into Vim yet -- I found out Nano
is a thing. I never liked it. It's always felt something like a clunky
on-graphical Windows Notepad of Linux.
So after a little while I decided to bite the bullet and start up `vimtutor`.
As mentioned before, it was awkward at first. But I could also see the power
that the anatomy of Vim allows the user to wield and decided to commit at least
the basics to muscle memory.
# Plugins and NeoVim
From here it would only be a matter of time.

115
src/content/rambles/orca.md Normal file
View file

@ -0,0 +1,115 @@
---
title: "Orca"
date: "2023-09-09T15:59:21+02:00"
author: "$HUMANOID"
tags: ["orca", "audio"]
description: "Rambles on Experiments with Orca"
---
# Brainy Music
There's something I really like about the idea of mathematically and
programmatically making music. As a result, I've harboured an interest in
trackers for quite some time, but never got into attempting to actually use one.
In a similar vein, I've looked at [Hundred Rabbits'
Orca](https://100r.co/site/orca.html) every now and then, but once again
couldn't be bothered to figure it out.
That was until recently, due to unrelated circumstances, I got access to Ableton
Live. This piqued my interest in Orca again and I decided to grit my teeth and
learn the syntax. The result is that I have now produced some blobs of
characters that -- when paired with the right software -- produce some really
interesting noises.
I won't go into detail on how everything works as the documentation is easy
enough to read. The syntax is the biggest hurdle.
# Patterns
My favourite operator so far is `U` -- the eUclidian rhythm operator. It takes a
left and right argument -- lets refer to them as `X` and `Y` respectively -- and
then attempts to produce a bang -- that is to say pulse on `X` out of `Y`
frames. So `3U8` attempts to produce 3 equally spaced bangs in 8 frames, which
will fail. As a result, it spaces the bangs as close to equal as possible. This
can lead to some really interesting patterns and rhythms.
Here's an example of when I wrapped my head around it for the first time:
{{< audio src=/audio/orca/plinkinator.flac audio=flac >}}
And the Orca code:
```orca
..nVC...........
................
5U9..Vn..4U9..Vn
.*:04C....*:03C.
................
3U5..Vn.........
..:05C..........
................
3U8..Vn.........
.*:06C..........
```
Here's a variant with a different synth and where I've been messing around with
the note being played while recording:
{{< audio src="/audio/orca/varied_plinkinator.flac" audio=flac >}}
# Numbers and stuff
Because I suck at basic calculus and have been half arsedly learning BQN, I
wrote a function to get a list of numbers to produce potentially interesting
patterns for the `U` operator:
```bqn
OrcaList ← {1↕(⊢⋈¨𝕩⊸÷)1+↕𝕩}
```
This function requires a number and returns a list of all possible divisions
from 1 up to the given number.
```bqn
Orcalist 9
┌─
╵ ⟨ 1 9 ⟩
⟨ 2 4.5 ⟩
⟨ 3 3 ⟩
⟨ 4 2.25 ⟩
⟨ 5 1.8 ⟩
⟨ 6 1.5 ⟩
⟨ 7 1.2857142857142858 ⟩
⟨ 8 1.125 ⟩
⟨ 9 1 ⟩
```
Rough Haskell equivalent for readability's sake:
```haskell
orcaList x = map ((,) <$> id <*> (x/)) [1..x]
```
# Recording from LMMS
Since LMMS puts whatever MIDI notes it gets into a grid (by default at least),
most of the intricacies of timing are lost. As a result, when using LMMS, I use
`pw-record` to record the live audio output with timing still intact.
This audio journey has also lead me to appreciate Pipewire a lot more. In that
regard, I have the following audio stack on NixOS:
```nix
{ config, ... }: {
sound.enable = true;
hardware.pulseaudio.enable = false;
services.pipewire = {
enable = true;
alsa.enable = true;
alsa.support32Bit = true;
pulse.enable = true;
jack.enable = true;
};
}
```
And I use `qwpgraph` as patchbay.

View file

@ -0,0 +1,143 @@
---
title: "PWR Switch"
date: "2022-10-14T10:50:56+02:00"
author: "$HUMANOID"
tags: ["linux", "ssh", "iot"]
description: "Hacked together IOT setup with some semblance of security"
---
# Introduction
As we all know, the "S" in IOT stands for security.
We've all seen countless stories of people's networks getting hijacked due to some 3 year old vulnerability in the software stack of their *insert smart device here*.
These things barely get any patches at all.
On top of that, the most they do is spy on you and send [your footage to the police without asking for your consent](https://edition.cnn.com/2022/07/14/tech/amazon-ring-police-footage/index.html "Load without JS to make the page behave.").
Still, on the other hand, it can be really convenient to be able to control some of the electronics in your home without having to walk towards them.
I personally have a lamp that I frequently forget to turn off when leaving my room.
The solution was an [Olimex PWR-switch](https://www.olimex.com/Products/Duino/Shields/PWR-SWITCH/) in combination with a Raspberry Pi 4 that I have running 24/7 regardless (mostly to watch YouTube videos and listen to music over my speaker set; I might write a post about this beautifully hacky contraption at some point in the future).
# Setup
Setting it up was relatively easy.
First I connected a European extension socket to a random 230 volt cable I still had laying around.
Then I hooked that up to my PWR-switch and plugged my lamp into that contraption.
From there I got little bag of jumper wires from my local electronics store and hooked up the PWR-switch to my Raspberry Pi.
I hooked up the PWR-switch's positive to `GPIO 15` and it's ground to the preceding `ground` pin.
[Here's](https://pinout.xyz) a reference page.
_Note_, `GPIO 15` is in reference to pin number 10 and the preceding ground pin is pin number 6.
From there I looked up how to send power to `GPIO 15` in order to have it toggle to the "on" state.
The first step to this end is to initialise the pin.
This can be done with:
```sh
echo 15 > /sys/class/gpio/export
echo out > /sys/class/gpio/gpio15/direction
```
Then, sending power to it can be done using something like...
```sh
echo 1 > /sys/class/gpio/gpio15/value
```
...and the opposite can be done using:
```sh
echo 0 > /sys/class/gpio/gpio15/value
```
Deinitialising the pin can be done with:
```sh
echo 15 > /sys/class/gpio/unexport
```
To make this all a little easier I have written a wrapper in POSIX Shell:
{{< start-details summary="Click to see script" >}}
```sh
#!/bin/sh
INIT_PIN(){
# the PWR-SWITCH is connected to GPIO 15 and the preceding ground pin
# reference page https://pinout.xyz/pinout/
[ -d /sys/class/gpio/gpio15 ] && echo "Pin already initialised" && exit 0
echo 15 > /sys/class/gpio/export
echo out > /sys/class/gpio/gpio15/direction
}
DEINIT_PIN(){
[ -d /sys/class/gpio/gpio15 ] || echo "Pin not initialised" && exit 0
echo 15 > /sys/class/gpio/unexport
}
PIN_STATUS(){
if [ -d /sys/class/gpio/gpio15 ] ; then
echo "pin status: Pin initialised"
else
echo "pin status: Pin not initialised"
fi
}
SWITCH_ON(){
# initialise pin before doing anything
[ -d /sys/class/gpio/gpio15 ] || INIT_PIN
echo 1 > /sys/class/gpio/gpio15/value
}
SWITCH_OFF(){
# initialise pin before doing anything
[ -d /sys/class/gpio/gpio15 ] || INIT_PIN
echo 0 > /sys/class/gpio/gpio15/value
}
TOGGLE_SWITCH(){
# initialise pin before doing anything
[ -d /sys/class/gpio/gpio15 ] || INIT_PIN
case $(cat /sys/class/gpio/gpio15/value) in
1) SWITCH_OFF;;
0) SWITCH_ON;;
esac
}
USAGE(){
cat >&2 <<EOF
Usage: pwr-switch <on|off|init|deinit>
on: send on signal
off: send off signal
init: initialise pin
deinit: deinitialise pin
status: show pin initialisation state
toggle: toggle on/off signal
EOF
}
case $1 in
on) SWITCH_ON;;
off) SWITCH_OFF;;
init) INIT_PIN;;
deinit) DEINIT_PIN;;
status) PIN_STATUS;;
toggle) TOGGLE_SWITCH;;
*) USAGE;;
esac
```
{{< end-details >}}
You could probably also do this in Python, but my knowledge of Python is nearly none.
# Remote interaction
You can probably make a guess as to how I set up the automation and interaction from other devices.
That's right, I used an SSH forced command:
```
no-pty,command="/usr/local/sbin/pwr-switch toggle" ssh-ed25519 <contents of <lamp-key>.pub>
```
The unique thing here being that it needs to be executed as the root user in order to mess with the state of the pins.
(Or at least in the case of my setup.)
So having it be a forced command is a security benefit here as it prevents whatever device you're using to toggle your lamp from having to log in as the root user.
All a device with the `<lamp-key>` can do, is toggle the state of `GPIO 15`.
You could contrivedly call this SIOT...
Since Android 12, there is the "Device controls" tile in the swipe-down-from-the-top-of-the-screen-menu.
Termux can interact with the API that this system provides and allows you to add buttons corresponding to Termux' shortcuts.
These shortcuts are executables found in `~/.shortcuts` within the Termux environment.
Here I've created a little script that logs into my Raspberry Pi using the `<lamp-key>`, allowing me to toggle my lamp at the tap of a button as long as I'm on my home network.

265
src/content/rambles/ssh.md Normal file
View file

@ -0,0 +1,265 @@
---
title: "SSH Configuration"
date: "2022-09-27T11:40:31+02:00"
author: "$HUMANOID"
tags: ["ssh"]
description: "An article on configuring SSH from the ground up to something that can grow out into something like my monster of a config file"
toc: true
---
# Introduction
If you're anything like me, you'll have at least a hand full of servers you'll log into (at least) every now and then using SSH.
These servers don't all have the same key.
And on top of that, I frequently have multiple keys per host.
Managing this by hand is a pain in the arse.
Thankfully there are a few tools that make it a breeze.
I will be going through what I have done to both my local configuration and what I tend to do with my server configuration.
# Generating keys
In order to do anything with SSH in a secure way, you'll need to make use of public and private keys.
These keypairs are generated using `ssh-keygen`.
I typically generate mine using:
```sh
ssh-keygen -t ed25519 -f ~/.ssh/<new-key>
```
Without any parameters, it will generate an rsa3072 key.
This form of cryptography isn't recommended as it's become a bit flimsy with computers becoming stronger.
Instead I recommend at least adding the `-t ed25519` flag to generate a ed25519 key.
When prompted for a passphrase, **_always_** give it one.
The only situation where _not_ using a passphrase is acceptable is when you are planning on using the key for a [forced command](#forced-commands).
# Server Configuration
This is all done under the assumption that the you use the OpenSSH implementation on your server.
If you use something like Dropbear, I can't help you as haven't properly dug through it's configuration file (yet).
The things I see _way_ to often on the internet are...
* People not disabling password authentication.
* People not changing the default port<!--or only allowing a range of IPs to log in-->.
* People not disabling root login and never using it.
So lets go through these steps one by one.
## Password Authentication
Password authentication is _the_ most basic thing every server should have disabled.
Otherwise, it is possible to brute force a connection into your server.
> "But my server is not exposed to the internet."
I guess you _could_ get away with not disabling password authentication, but it's still not a good idea in case, say, your network gets compromised.
On top of that, it's less convenient to have to type in a password every time you want to log into your server (more in that in the SSH Agent section).
In order to disable password authentication, open your SSH daemon configuration file: `/etc/ssh/sshd_config` and look for the following lines...
```sshd_config
...
# To disable tunneled clear text passwords, change to no here!
#PasswordAuthentication yes
#PermitEmptyPasswords no
...
```
...uncomment `PasswordAuthentication` and replace "yes" for "no".
Make sure you still have a way into your server before restarting the daemon.
If you're not planning on logging in as the root user, uncomment and set the following setting to "no".
```sshd_config
...
#PermitRootLogin prohibit-password
...
```
Be aware of the fact that you can still utilise the root account using `sudo su -` (assuming you're using `sudo` on your server, else use whatever other privilege escalation tool you have at hand).
Restarting the daemon on modern systems is usually done using:
```sh
systemctl restart sshd
```
If you're not using systemd, I'm sure you know what command to use instead.
## Adding keys to `authorized_keys`
When going through `/etc/ssh/sshd_config` you've probably come across a few lines resembling:
```sshd_config
...
# Expect .ssh/authorized_keys2 to be disregarded by default in future.
#AuthorizedKeysFile .ssh/authorized_keys .ssh/authorized_keys2
...
```
This means that the SSH daemon will check in `.ssh/authorized_keys` in the home directory of the user as whom you're trying to log in for authorized keys.
So the next step is to append your public key to this file in the home directory of the user as whom you want to be able to log in.
This can be done in a few ways.
The proper way is by using:
```sh
ssh-copy-id -i ~/.ssh/<key-file> <user>@<host>
```
I'm usually too lazy to remember there is a proper way and just open the file in `vi` paste and it in there by hand during the same initial login when I'm disabling password authentication.
Either way works fine.
## Changing the port
The advantage to this is that your logs will be a lot cleaner if your host is exposed to the internet.
There are large amounts of bots hammering port `22` on every IP address they can find with common usernames and passwords like "root" and "admin".
A solution next to this is to use `fail2ban` along side changing the port.
> "Won't this mean I have to add the port to my login command every time I go to this server?"
No, more in this in [the client configuration](#client-configuration) section
In `/etc/ssh/sshd_config` look for...
```sshd_config
...
#Port 22
#AddressFamily any
#ListenAddress 0.0.0.0
#ListenAddress ::
...
```
...and change the `Port` to your liking, I tend to change this to something like 6969 or some other meme number.
Another thing I tend to do is not open a port in my firewall, thus preventing any normal outside connections all together.
Instead opting to only connect over Yggdrasil and/or Tor.
# Client configuration
If people tend not to think much about their server configuration, their client configuration is probably not even touched at all.
## The `~/.ssh/config` file
The very first thing I do after setting up a server, is add an entry to my `~/.ssh/config` in order to manage its key, the user, the port and possibly subdomain should the need arise.
A basic configuration section looks like the following:
```ssh_config
Host <host> # this is something you can easily identify
Host <hostname> # this does need to be an IP address or DNS record pointing to an IP address
IdentityFile ~/.ssh/<key-file>
User <user-name>
Port <meme-number>
```
This allows you to log into host `<host>` with on port `<meme-number>` with key `~/.ssh/<key-file>` as user `<user-name>` without by typing:
```sh
ssh <user-name>@<hostname> -p <meme-number> -i ~/.ssh/<key-file>
```
Instead the following command will work:
```sh
ssh <host>
```
More information on the SSH config file can be found in the `ssh_config` manpage.
Some other frequently used settings for me are:
* `AddKeysToAgent`
* `IdentitiesOnly`
* `ProxyCommand`
### `AddKeysToAgent`
Automatically adds the key to your SSH agent if you have one running.
This is useful if you frequently log in and out of a certain host and don't want to take the time to add it's key to your agent manually.
### `IdentitiesOnly`
Ignore whatever keys your agent has and only use the contents of `IdentityFile`.
Useful for when you want to be able to log into the same host using multiple keys while using an SSH agent session.
### `ProxyCommand`
Always connect to your host using a proxy, using a given command.
Useful for when you can only access a host through a certain proxy.
I use this for my Tor hosts:
```ssh_config
Host tor-<host>
Hostname <lengthy-56-character-string>.onion
# this is dependent on the netcat implementation of the OpenBSD project, often packaged as "netcat-openbsd"
ProxyCommand nc -X 5 -x localhost:9050 %h %p # this assumes you are running a tor proxy on your local system and attempts to make a connection through that
Identityfile ~/.ssh/<key-file>
User <user>
```
# The SSH Agent
If you're using SSH keys with passphrases, it will very quickly get annoying to type in the passphrase every time you use a certain key.
To alleviate this tedium, the SSH agent exists.
If you're using a full desktop environment, chances are that you already have an SSH agent running in the background.
You can check this by seeing if `$SSH_AGENT_PID` is set to anything.
```sh
echo $SSH_AGENT_PID
```
If this isn't set to anything, you can start an agent session by running:
```sh
eval $(ssh-agent)
```
Now you can add keys to your agent with:
```sh
ssh-add </path/to/key-file>
```
You can also have it automatically drop keys after a specified amount of time with the `-t` flag.
I tend to do this with my root keys as a security precaution.
```sh
ssh-add -t 1h ~/.ssh/<root-key>
```
Starting an SSH agent every time you open a new shell session gets quite annoying quite quickly.
There are a few things you can automate this.
The simplest is to add `eval $(ssh-agent)` to your `~/.profile`.
Another option, the one I prefer, is to use [keychain](https://www.funtoo.org/Funtoo:Keychain) from the Funtoo project.
It checks whether there's an agent running every time you start a new login session.
If there is, it sets the SSH agent environment variables to the existing ones from some other session.
If there isn't a running SSH session, it will start one.
I have the following in my `~/.profile`:
```sh
...
eval $(keychain --agents 'gpg,ssh' --eval)
...
```
As you can see, it can also keep track of your GPG agent.
# Forced Commands
Another amazing feature of SSH is forced commands.
The name is relatively self explanatory.
You give a public key the privilege to execute a single command and nothing else.
This is really useful if there is something that you frequently do on a server which is simple enough that it can be automated but still requires interaction from device.
I make use of a forced command to open NCMPCPP on my MPD server.
Logging in every time and typing `ncmpcpp` every time I wanted to add or remove some songs from the current playlist got annoying really fast.
On my client machines I have an entry in my `~/.ssh/config` with roughly the following:
```ssh_config
Host <host>-radio
Hostname <hostname>
User <mpd-user>
Port <meme-number>
Identityfile ~/.ssh/<host>-radio
IdentitiesOnly yes # here's where forcing it to ignore the ssh agent comes in useful
```
On my MPD server, I have the following in `~/.ssh/authorized_keys`:
```authorized_keys
command="ncmpcpp" ssh-ed25519 <contents of <host>-radio.pub>
```
When I `ssh` to `<host>-radio`, all I get is an NCMPCPP session.
On one of my Raspberry Pis, I make use of a forced command to toggle my desk lamp using a little wrapper I wrote around the Pi's GPIO interface.
It has the following line in `~/.ssh/authorized_keys`:
```authorized_keys
no-pty,command="/path/to/script/lamp toggle" ssh-ed25519 <contents of lamp-key.pub>
```
The `no-pty` option prevents any sessions opened with this key from starting an interactive shell session.
Documentation of the `authorized_keys` file can be found in the `sshd` manpage under the `AUTHORIZED_KEYS FILE FORMAT` section.

104
src/content/rambles/vim.md Normal file
View file

@ -0,0 +1,104 @@
---
title: Vim
date: 2022-03-21T01:17:06+02:00
author: $HUMANOID
tags: ["linux", "vim"]
---
# Vim Cheat sheet
This is a little Vim cheat sheet with thing people generally don't seem to be particularly familiar with when it comes to Vim.
This is about Vim, not NeoVim, but I most things with still apply to NeoVim.
## Self-explanatory things
These are a few bindings that I don't feel require an extensive explanation.
| binding | action |
| --- | --- |
| Z Z | write and quit; equivalent to `:wq` |
| Z Q | quit; equivalent to `:q!` |
| :x | write and quit; equivalent to `:wq` |
| g u | swap characters to lower case |
| g u u | swap characters to lower case on current line |
| g U | swap characters to upper case |
| g U U | swap characters to upper case on current line |
| g {j,k} | if the current line you're on spans more then the width of your screen, go down to the portion that's past the linebreak |
| ~ | toggle case |
| z g | add word to current dictionary |
| [ s | highlight previous misspelled word |
| ] s | highlight next misspelled word |
| z = | open spelling suggestions based on current dictionary and highlighted word |
| . | repeat last modify operation |
| ; | repeat last move operation |
| ! | start command with `:<selection>!` |
## Splits and Terminals
You can split your vim window with somewhat Emacs-like chords by default; I don't bother changing them because I use with quite a few different computers, not all of which have my Vim dotfiles installed.
| binding | action |
| --- | --- |
| C-w s | Horizontal split |
| C-w v | Vertical split |
| C-w {h,j,k,l} | Move focus to split {left,down,up,right} |
| C-w S-{h,j,k,l} | Move split to {left,down,up,right} |
Another usefull thing I don't see a lot of people do is open a terminal session in vim.
This can be really quite useful if you quickly need to change or check something.
You do this with the `:terminal` command.
In Vim, this will horizontally split your current window and open a terminal session in the top half.
(Yes, you can also open Vim sessions in these terminal sessions if you feel like it).
You can navigate to and from it just like any other splits.
In NeoVim terminals are handled significantly worse and is the main reason I went back to base Vim.
## Tips
Something I came across some time ago is that you can interpet the current line with whatever you want using
```
:.!<interpeter>
```
So for instance running...
```
:.!bash
```
...while highlighting the line...
``` bash
echo Line{1..5} | sed 's/ /\n/g'
```
...will result in:
```
Line1
Line2
Line3
Line4
Line5
```
# Configuration
Some settings I like putting in my git configuration files.
```vimrc
set nocompatible
set termguicolors
set cursorline
set hlsearch
set ignorecase
set smartcase
set number
set relativenumber
set wildmenu
set mouse=
set scrolloff=3
set listchars=tab:│\ ,lead:·,trail:·,eol:¬ "'lead' is a NeoVim specific option
set list
" I haven't tried if this works in base vim.
match ExtraWhitespace /\s\+$/
highlight ExtraWhitespace guifg=red
```

View file

@ -0,0 +1,144 @@
---
title: "On window managers and XMonad"
date: "2022-11-03T23:17:35+01:00"
author: "$HUMANOID"
tags: ["linux", "window managers", "xmonad"]
description: "A ramble about, among other things, a highly configurable window manager"
---
# My journey into Tiling Window Managers
When I started my Linux journey, I stuck with GNOME 3 something for around the
first year. Sure, I tried KDE and Cinnamon and XFCE, but GNOME is the one I
always kept coming back to. I think it's because it's pretty much the only one
that felt completely different from what the rest of the world was doing. It
helped me with thinking about Linux as being different from windows. After this
first year though, I came across a few videos about tiling window managers and
wanted to try one. The first one I installed was [i3](https://i3wm.org/).
I hated it.
I had a hard time configuring it as I didn't really know what I was doing.
Other than that, I found it plain awkward to use. The way it tiles windows --
and how you _still_ have to babysit pretty much every last one of them --
drives me away from i3 to this day. I'm sure it has the potential to be a great
window manager. It's just _really_ not for me. Which is a shame, I would really
like to properly live in Wayland with [Sway](https://swaywm.org/ "Sway is like
i3 in Wayland") for at least a little while.
After a few hours of trying to get things to work in i3, I went back to GNOME.
Some time later, I came across [AwesomeWM](https://awesomewm.org/). It was
being recommended as a fairly easy window manager to start with -- it having
window decorations and it's own menu system. The first thing I did was try-and
fail to rip out said menu system and window decorations. I was putting too much
on myself trying to learn the basics of both using a tiling window manager and
Lua.
Some time after that, I came across
[BSPWM](https://github.com/baskerville/bspwm). This was the first time where I
felt like I _really_ managed to get a tiling window manger to do what I wanted
it to do. For some odd reason, there are people out there who consider BSPWM a
more "advanced" window manger. I really don't get why. To this day, I am of the
opinion that SXHKD's configuration syntax is some of the best out there. I
think it took me around 10 minutes to wrap my head around the basics. Somewhere
around an afternoon later, I had a config that served me well for the next few
months to come.
A while after BSPWM, I decided to give suckless'
[DWM](https://dwm.suckless.org/) a shot. Despite my lack of knowledge of C,
this very quickly became my favorite window manager at the time. There is just
something about the insanity of using diff files to configure your piece of
software when perfectly functional configuration libraries and languages exist
that got it's hooks in me. I also caught the minimalism bug around this time,
so DWM's nearly non-existent memory footprint was also great. Despite this
really being the first really "advanced" window manager, I had an easier time
configuring it than AwesomeWM or i3. It was also the first time where I could
appreciate the master-stack layout properly and not having to think about
keeping track of windows in two dimensions anymore. It made me realise that I
want to have to think as little as possible about window positioning. It was
the reason I couldn't deal with i3's paradigm and shifted away from BSPWM the
moment I found DWM.
I ran DWM as my main window manger for over a year before having issues with
some fullscreen applications and the JetBrains suite, which I had to use for
college activities.
{{< img link="https://xkcd.com/1806/" src="/images/xkcd/borrow_your_laptop.png" >}}
I decided to give AwesomeWM another shot after having figured out what I want
from a tiling window manger. This time I managed to get something that worked
pretty much exactly how I wanted it to. In other words, a fairly basic
configuration with most of the default features ripped out and instead my
partially-organically-grown-probably-batshit-insane-keybindings (I use `Super` +
`Space` to open my run launcher. In fact, on my keyboard I have holding the big
space bar (yes it has two space bars, its layout is about as insane as my
window management keybindings) bound to `Super` + `Space`).
To this day I still use DWM quite frequently on machines where I don't really
want to think about what graphical interface to chuck on it (hence I half
arsedly maintain an Alpine package of my fork).
For quite a long time I used DWM and AwesomeWM depending on whether I was
planning on frequently using fullscreen applications and how strong the machine
in question was; AwesomeWM being noticeably slower than DWM on _really_ old
machines (like RejuvinatedBrick). Until at some point, I came across
[XMonad](https://xmonad.org/).
I tried it for an evening.
I hated the fact that it's configured in _sodding Haskell_ and went back to DWM.
A few months later, I came across an implementation of chorded keys through
the `XMonad.Util.EZConfig` module and decided to give it another shot.
This time I was hooked.
The biggest problem I had with it was _still_ the fact that it was configured
in Haskell, but the absurd level of customisability made it worth dealing with
the functional pain. It was also the first time I decided to not bother with a
status bar as getting a basic configuration going had given me enough grief for
one month.
As for gripes I had (and partially still have) with XMonad, they were quite few,
surprisingly. Coming from AwesomeWM and DWM, I would've liked XMonad to use tags
instead of workspaces; fullscreen is a bit of a pain to get working, but has
less fuckups than DWM; the fact that it's configured in Haskell; it not having a
set of workspaces per monitor (at least by default, I kind of stopped caring
after a little while); the fact that it's configured in Haskell; the
`XMonad.Layout.ShowWName` module being kind of unstable and last but not least,
the fact that it's configured in _fucking Haskell_.
It almost seems like I got sick of being sick of it being configured in Haskell
and I decided to dive into learning the language with the goal of being able to
fully understand my monstrous 384 line config file (586 lines including the
documentation in commented sections).
{{< img src="/images/config_length.png" >}}
I don't fully understand it yet at the time of writing, but I do understand it
a hell of a lot better than when I produced most of those 300 lines.
The greatest thing about XMonad is simultaneously the thing that kept me away
from it: it's written and configured in bloody Haskell. Thus there is barely a
separation between configuration and source code. The only difference there
_really_ is, is the filename. Once you understand a bit of Haskell (no small
task if you're used to imperative languages) adding and integrating your own
features is really easy. And then they are _properly_ integrated. Sort of like
how DWM works, only with proper documentation and support. Part of me wants to
see if I can figure out a way to package my XMonad build as a single binary to
be able to chuck it onto systems without putting much thought into it.
Weird thing with Haskell I'm noticing so far is that I'm slowly but surely
managing to dig up old concepts that I tried to implement in imperative
languages, but couldn't due to their nature. Things like pattern matching and
maps are quite intuitive to me. It's just that Haskell's syntax takes a _lot_ of
getting used to. In the mean time, I guess that my experience with POSIX Shell
bridged the gap between my knowledge of Go and C#; and Haskell.
I highly recommend giving XMonad a shot if you're willing to bash your head
against the Haskell wall for a while before understanding how your window
manager works.
Looking back I find it quite funny to see how I went from not being able to get
i3 to do what I wanted it too and giving up; to barely being able to write or
understand Haskell but putting up with it.

View file

@ -0,0 +1,91 @@
---
title: "Xmonad Configuration Bits"
date: "2023-09-13T16:47:48+02:00"
author: "$HUMANOID"
tags: ["xmonad", "haskell"]
description: "A few interesting things I've done with my XMonad config"
---
So the other day I was hacking some more on my XMonad config and came across a
few quirks that I don't think I've seen other people implement in their configs.
Though truth be told, I haven't really looked at a lot of other people's config
files.
> "Where is your config file anyway?"
Haven't published it yet as it's still a huge mess of half baked Haskell
snippets.
## Complex terminal sessions
I use [Kitty](https://sw.kovidgoyal.net/kitty/) as my terminal. It's amazing and
probably warrants its own article one day. One of the really cool features is
that it allows you to override options on launch with the `-o` or `--override`
parameter. This allows you to change any of the values in your `kitty.conf`,
this includes the font and colour pallet.
I've been using this to start terminals with
[BQN](https://mlochbaum.github.io/BQN/) compatible font.
```sh
kitty -o font_size=17 -o font_family='BQN386 Unicode' -e bqn
```
This works great and I assigned it to a keybinding for a while. However, some
time later I decided to check out [R](https://www.r-project.org/) and wanted to
have a similar keybinding with slightly different options. Here it dawned on me
that it would be silly to copy-paste the BQN keybinding and change a few things,
so I abstracted that away into a function that takes a list of options and feeds
that to Kitty.
```haskell
runInKittyWithOpts :: [String] -> String -> X ()
runInKittyWithOpts opts executable = spawn $ "kitty "
++ (unwords . map ("-o " ++)) opts
++ " -e " ++ executable
```
If I now want to start a kitty session with a BQN font and R repl, I can add
something like the following to a keybinding:
```haskell
runInKittyWithOpts rOptions "R"
where rOptions = [ "font_size=17"
, "font_family='BQN386 Unicode'"
]
```
Since I write quite a bit, I also have a kitty session with a giant margin left
and right of the "terminal" section of the window as that empty space helps me
concentrate. All this is, is the previous function without the `-e` parameter:
```haskell
kittyWithOpts :: [String] -> X ()
kittyWithOpts opts = spawn $ "kitty "
++ (unwords . map ("-o " ++)) opts
```
Though I didn't let that stop me from golfing it down to a point free version:
```haskell
kittyWithOpts :: [String] -> X ()
kittyWithOpts = spawn . ("kitty " ++) . (unwords . map ("-o " ++))
```
My prefered settings for a "text hole" terminal.
```haskell
kittyWithOpts [ "font_family='BQN386 Unicode'"
, "font_size=14"
, "single_window_margin_width='5 200'"
]
```
If I were to implement this properly, I would make a kitty module with all
options in DataType. But alas, I'm not patient/bored enough for that... yet.
## Mouse bindings
I thought it would be neat to be able to scroll up and down while holding my
modifier key to change my screen brightness and audio levels. The basic concept
here was quite simple; us a lambda function that chucks whatever data it gets
onto the ground because the type signature for mousebindings forces you to deal
with a focussed window.
```haskell
, ((modMask, button4), \_ -> safeSpawn "pulsemixer" ["--change-volume","+1"])
```

View file

@ -0,0 +1,308 @@
---
title: "XMonad Promtps"
date: "2023-03-08T14:20:11+01:00"
author: "$HUMANOID"
tags: ["haskell", "xmonad", "linux"]
description: "There aren't a lot of instructions or explanations on creating XMonad prompts, or at least not within a minute of checking my searx instance. This is my attempt at filling that gap."
toc: true
---
# Introduction
XMonad has it's own prompt system. Some time ago, I wanted to see if it could
replace dmenu entirely. I managed it for the more common usages I had for it. My
application launcher, `ssh` prompt and pass interface were easy to replace using
standard XMonad Contrib modules (`XMonad.Prompt.Shell`, `XMonad.Prompt.Ssh` and
`XMonad.Prompt.Pass` respectively). However, things became more difficult when
it came to my universal/external Qutebrowser bookmarks menu and
`yt-dlp`-and-`pipe-viewer` wrapper.
This tutorial-of-sorts will assume _some_ Haskell knowledge or not being afraid
of diving straight into how Haskell works. I'm not going into great detail on
how everything works here.
# Bookmarks menu
The first one I decided to tackle was the bookmarks menu, as it is by far the
simplest of the two.
Let's take a look at the original:
```sh
#!/bin/sh
bookmarks="$HOME/.config/qutebrowser/bookmarks/urls"
choice="$(awk '{print$1}' $bookmarks | sort | dmenu -p "Bookmark:" -l 30)"
[ -z $choice ] || qutebrowser "$choice"
```
Things get interesting at the initialisation of the `choice` variable:
1. It takes the contents of Qutebrowser's bookmarks file
2. It sorts the results of that
3. Sends that to `dmenu`, prompting the user to make a choice
After this, it checks whether `choice` is empty or not and in case it isn't,
opens Qutebrowser with its contents.
Here is an example of how Qutebrowser saves its bookmarks:
```
https://www.alpinelinux.org/ index | Alpine Linux
https://www.openbsd.org/ftp.html OpenBSD: Mirrors
https://commonmark.org/ CommonMark
https://xxiivv.com/ Echorridoors
https://100r.co/site/home.html 100R — home
https://solar.lowtechmagazine.com/about.html About this website | LOW←TECH MAGAZINE
```
## Implementation
Its functionality does boils down to the following:
1. Parse a given file according to a set of rules, returning it's contents in
the form of a list
2. Allow the user to make a choice from that list
3. Launch an application with that choice as parameter
Seems easy enough to implement.
### Parsing the Bookmarks file
Let's start off by creating a function that can parse our bookmarks file. Here we
need something to read a file -- in this case a bookmarks file -- and return its
contents in the form of a list of strings.
```haskell
fileContentList :: FilePath -> IO [String]
```
This function takes a filepath -- the `Filepath` datatype is an alias for
`String` -- and returns `IO [String]`.
Now for the body of the function:
```haskell
fileContentList :: FilePath -> IO [String]
fileContentList f = do
homeDir <- getEnv "HOME"
file <- readFile (homeDir ++ "/" ++ f)
return . uniqSort . lines $ file
```
Let's go over what is happening here line by line.
`fileContentList` is a function that takes an argument `f`; then it starts a
`do` block. `do` blocks are used to put multiple functions in sequence in the
scope of a single function without having them interact with eachother.
Within the `do` block, it first retrieves the current home directory based on
the `$HOME` environment variable and binds it to `homeDir` using the `getEnv`
function from the `System.Environment` module. `getEnv` returns a string with
the contents of the variable given as its argument.
Next, it retrieves the file contents from `$HOME/path/to/file` using the
`readFile`. This path is created by appending `f` to the `homeDir`.
Now for the final line.
First it takes the `file` and splits it up into a list of strings based on
newlines using the `lines` function.
```haskell
lines $ file
```
Then it pipes the result from that into `uniqSort` from the `XMonad.Prompt`
module in order to -- as the name implies -- sort it and get rid of any
duplicate items.
```haskell
uniqSort . lines $ file
```
And the output of that is piped into `return`:
```haskell
return . uniqSort . lines $ file
```
This function will allow us to parse any given text file. To parse the
Qutebrowser bookmarks file, call it using `.config/qutebrowser/bookmarks/url`
> _Note_: I say "pipe" because the '`.`' function behaves quite similar to
> pipes in POSIX Shell. However, the correct way of referring to what it does
> is composition; it takes two functions and passes the output of the first
> function to the second, thereby creating -- or composing a new function. As
> apposed to how pipes in POSIX Shell work, function composition chains are
> executed from right to left.
### Creating a Prompt
Let's see if there is anything in the
[`XMonad.Prompt`](https://hackage.haskell.org/package/xmonad-contrib-0.17.1/docs/XMonad-Prompt.html)
module that looks like it could help us in creating a prompt.
> ```haskell
> mkXPrompt :: XPrompt p => p -> XPConfig -> ComplFunction -> (String -> X ()) -> X ()
> ```
> ---
> Creates a prompt given:
> - a prompt type, instance of the `XPrompt` class.
> - a prompt configuration (`def` can be used as a starting point)
> - a completion function (`mkComplFunFromList` can be used to create a completions function given a list of possible completions)
> - an action to be run: the action must take a string and return `X ()`
This looks like it could serve as the basis for our prompt. The description and
type signature tell us that it is going to require an instance of the `XPrompt`
typeclass. So let's create a `Bookmark` datatype and implement the `showXPrompt`
function from `XPrompt` in order to give it a default message when executed and
thereby having it derive from `XPrompt`.
```haskell
data Bookmark = Bookmark
instance XPrompt Bookmark where
showXPrompt Bookmark = "Bookmark: "
```
As its second argument, `mkXPrompt` requires an instance of `XPConfig`. The
`XPConfig` typeclass is where you -- as the name implies -- specify the
configuration of XMonad's prompts. Knowing this we can start to write function that
uses `mkXPrompt`:
```haskell
bookmarkPrompt c = do
mkXPrompt Bookmark c
```
`c` is our `XPConfig` argument.
This takes care of the `XPrompt p => p -> XPConfig` portion of the function.
Now for the completion function, that will handle the list given to our prompt.
Let's mostly follow the suggestion in the description of `mkXPrompt` and lets
take a look at:
> ```haskell
> mkComplFunFromList' :: XPConfig -> [String] -> String -> IO [String]
> ```
> ---
> This function takes a list of possible completions and returns a completions
> function to be used with mkXPrompt. If the string is null it will return all
> completions.
This is how Qutebrowser and `dmenu` act by default with a given list of possible
options.
```haskell
bookmarksFile = ".config/qutebrowser/bookmarks/urls" :: String
```
> I didn't know where to put this, but I created a string to hold the path to my
> bookmarks
So it takes an instance of `XPConfig` -- that will again be our `c` argument,
and a list of strings. Here is where we feed it the contents of our file using
our `fileContentList` function. We will do this by binding the output to, say
`bl` for "bookmark list" with `<-`. Since `fileContentList` is a member of the
`IO` monad and we're working in, we have to call it using the `io` function,
which is an alias for the `liftIO` function.
```haskell
bookmarkPrompt :: XPConfig -> (String -> X ()) -> X ()
bookmarkPrompt c f = do
bl <- io fileContentList bookmarksFile
mkXPrompt Bookmark c (mkComplFunFromList' c bl) f
```
You'll see that I've also added argument `f`, this is the function we're going
to use to actually do something with our prompt output. Considering we're
working with bookmarks, opening them in a browser would make sense.
```haskell
openBookmark :: String -> X ()
openBookmark bookmark = do
browser <- io getBrowser
spawn $ browser ++ " '" ++ getUrl bookmark ++ "'"
where getUrl = head . words
```
`openBookmark` is a function that takes a string and returns something in the
context of the `X` monad (hence the name "XMonad", it's a monad that interacts
with Xorg). Let's go through it line by line.
```haskell
browser <- io getBrowser
```
First we get user's browser using the `getBrowser` function from the
`XMonad.Prompt.Shell` module and bind that to `browser`.
This function checks the `$BROWSER` environment variable and if it isn't set, it
defaults to "firefox".
```haskell
spawn $ browser ++ " '" ++ getUrl bookmark ++ "'"
```
Since `getBrowser` returns a string, we can append things to it and feed that to
`spawn`. In this case, we get the URL portion of the bookmark entry surrounded by
single quotes in case a given bookmark contains any symbols that mess up our
shell. After all, what `spawn` ultimately does is feed a given string to
`/bin/sh` as a command to execute.
```haskell
where getUrl = head . words
```
For get `getUrl`, we take the given string, split it into a list of strings
based on space characters, pipe that into head, thus retrieving the first item.
## Keybinding
We now have a set of functions that create a prompt populated with our
Qutebrowser bookmarks file (any other list of URLs will also work) which will
open our browser when choosing one.
Now all we have to do is bind it to a key. Personally I use the
`XMonad.Util.EZConfig` so I have the following in my keybindings:
```haskell
, ("M-M1-C-b", bookmarkPrompt (myXPConfig {autoComplete = Just 200000}) openBookmark Bookmark)
```
If you use the default way of defining keybindings you can use something like
the following:
```haskell
, ((modm .|. controlMask, xK_b), bookmarkPrompt def openBookmark)
```
`def` is a reference to the default implementation of `XPConfig`.
# Everything Together
Everything put together, your config should have something like the following
added.
```haskell
data Bookmark = Bookmark
instance XPrompt Bookmark where
showXPrompt Bookmark = "Bookmark: "
bookmarksFile = ".config/qutebrowser/bookmarks/urls" :: String
fileContentList :: FilePath -> IO [String]
fileContentList f = do
homeDir <- getEnv "HOME"
file <- readFile (homeDir ++ "/" ++ f)
return . uniqSort . lines $ file
bookmarkPrompt :: XPConfig -> (String -> X ()) -> X ()
bookmarkPrompt c f = do
bl <- io fileContentList bookmarksFile
mkXPrompt Bookmark c (mkComplFunFromList' c bl) f
openBookmark :: String -> X ()
openBookmark bookmark = do
browser <- io getBrowser
spawn $ browser ++ " '" ++ getUrl bookmark ++ "'"
where getUrl = head . words
-- ... keybindings
, ((modm .|. controlMask, xK_b), bookmarkPrompt def openBookmark)
-- more keybindings ...
```

26
src/content/services.md Normal file
View file

@ -0,0 +1,26 @@
---
title: Services
---
These are a few serices I provide to the people who may be interested in them.
# [SearXNG instance](/searx)
Since DuckDuckGo decided to censor content a few weeks ago at time of writing, the thought of spinning up another SearX instance started to form in the back of my mind.
Then *at* time of writing, they decided to start to block more content in relation to copyright infringing material.
This still doesn't nessecarily mean too much, but who knows how much they're going to continue blocking now they've started in the first place.
So here's that SearX instance, {{< bold-gay content="enjoy!" >}}
I have got to say that the theming of SearXNG has improved quite a lot either over the years or in my mind.
At the very least I think the default theme looks quite nice these days.
{{< noscript content="SearXNG does make use of JavaScript for certain functions, but it's purely to make using it a smoother experiece and it's perfectly functional without JavaScript." >}}
# [Alpine repository](https://alpine.voidcruiser.nl)[(onion)](http://imerwns46jfdawado7xxb42i2kx7wjy6eyzugxehehxluh4hjqpebmyd.onion)
I use [suckless' dwm](https://dwm.suckless.org) on every other machine I use.
Since I can't be bothered to always keep cloning the repo, I thought I'd make package based on it.
Why Alpine of all things? It's my second most used distro after Debian.
Since it's as lightweight as it is, I'd say it's a natural fit for suckless utilities.
(also perhaps because the process of making package was really quite easy).

View file

@ -0,0 +1,16 @@
{{ define "main" }}
{{ .Content -}}
<div class="posts">
{{ $isntDefault := not (or (eq (trim $.Site.Params.contentTypeName " ") "rambles") (eq (trim $.Site.Params.contentTypeName " ") "")) }} {{ $contentTypeName := cond $isntDefault (string $.Site.Params.contentTypeName) "rambles" }} {{ $PageContext := . }} {{ if .IsHome }} {{ $PageContext = .Site }} {{ end }} {{ $paginator := .Paginate (where $PageContext.RegularPages "Type" $contentTypeName) }}
<table class="post-entry-meta"> {{ range first 10 .Paginator.Pages }}
<tr>
<td><a href="{{ .RelPermalink }}">{{ .Title | markdownify}}</a></td>
<td>{{ .Params.Date.Format "2006-01-02" }}</td>
<td>{{ .Params.Author }}</td>
<td>{{ range .Params.tags }} #<a href="{{ (urlize (printf "tags/%s/" .)) | absLangURL }}">{{ . }}</a>{{ end }}</td>
</tr> {{ end }}
</table>
<p><a class="button" href="/rambles">Read more</a></p>
</div>
{{ partial "stickers.html" }}
{{ end }}

View file

@ -0,0 +1,8 @@
<footer>
<div class="mirror">
[<a rel="me" href="https://tech.lgbt/@nuclearegg">Mastodon</a>]
[<a rel="rss" href="/index.xml">rss</a>]
[<a href="http://void5xgev2n26prpzqvf2g3gqv5kx7kz6md4skqngrfm23rmwkbralad.onion">onion_1</a>|<a href="http://fxawfamgojpoxxgzmmjiro4wx4eycempujmryk6miz4a64ljl6sz7dqd.onion">onion_2</a>|<a href="http://lmwr2pugnmv4pkkxlneeepi4oysfly33zfuj7x25s6hfydysnpfq.b32.i2p">i2p</a>|<a href="http://[201:e2aa:7c70:666b:9a28:6406:be7f:bf0d]">yggdrasil</a>]
<a rel="lisence" href="http://creativecommons.org/licenses/by-sa/4.0/"><img alt="CC BY-SA 4.0" src="/stickers/cc4.png"></a>
</div>
</footer>

View file

@ -0,0 +1,8 @@
<div class="stickers">
<a target="_blank" href="https://yesterweb.org/no-to-web3/" ><img alt="Web 3 is a scam" src="/stickers/notoweb3.gif" /></a>
<a target="_blank" href="https://mullvad.net/en/browser" ><img alt="Fuck Chromium" src="/stickers/same-shit-different-asshole_banner.gif" /></a>
<a target="_blank" href="https://www.vim.org" ><img alt="I ♡ vim" src="/stickers/edited-with-vim_banner.gif" /></a>
<a target="_blank" href="https://yewtu.be/watch?v=TdkdR92a7dU"><img alt="Nazi punks, fuck off!" src="/stickers/nonazis.png"></a>
<a target="_blank" href="https://wiby.me"><img alt="Wiby is great" src="/stickers/wiby.gif"></a>
<a target="_blank" href="https://anybrowser.org/campaign/"><img alt="Viewable in any browser" src="/stickers/4nobody.gif"></a>
</div>

View file

@ -0,0 +1,14 @@
<div class="about">
<a title="Vector graphic" href="/logoOptimised.svg"><img src="/logoOptimised.png" /></a>
<ul>
<li><b>Name:</b> Marty</li>
<li title="13-6-2001 Gregorian calendar"><b>Date of birth:</b> 18-con-3167</li>
<li><b>Gender:</b> Not really</li>
<li><b>Email:</b> <a href="mailto:marty.wanderer@disroot.org">marty.wanderer@disroot.org</a></li>
<li><b><a href="/keys/gpg.asc">GPG key</a></b></li>
<li><b>Country:</b> Check the TLD</li>
<li><b><a href="gemini://voidcruiser.nl">Gemini</a></b></li>
<li><b><a href="https://gitlab.com/EternalWanderer">Gitlab</a></b> - because I evidently can't be arsed to host my own git interface yet...</li>
<li><b><a rel="me" href="https://tech.lgbt/@nuclearegg">Mastodon</a></b></li>
</ul>
</div>

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

View file

@ -0,0 +1,9 @@
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2y/Ij9YlP9/CxAQoYrJU
lZ5c6Srk99a+tYu02yr3ffyHGJJ+K5HMFT5v8JEgXYtNs5cq2thl9kcInizQg4N5
l+NwHt/GlOKtsx8BAzWjN39GMwEylOdmM2AecPCAYRVAcxGgykxNW5oH29ncuEOL
ATGr6wcxawSue2V6PVx781UEb9aEAhRYsg+0OZN08Qm8dg9Oxh9fHOfpBBajpv8E
/6/oPEPtJGN1Nauz8MA5lzyDFHxMRYvVX+YCjAD8HvNU8VEH3PVHJJ6R4mhOKBcv
wehaCJY+RP9HCbu5OENS0ForzReO1zSaqtq1r4pcDQbFXT/enB0R5RjxSLKMH24a
aQIDAQAB
-----END PUBLIC KEY-----

13
src/static/keys/gpg.asc Normal file
View file

@ -0,0 +1,13 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
mDMEYlbtQRYJKwYBBAHaRw8BAQdAfnOdIezMvkyaBths3yFxT5dk9nm37jpUh9jb
IQr5trG0Ik1hcnR5IDxtYXJ0eS53YW5kZXJlckBkaXNyb290Lm9yZz6IkAQTFggA
OAIbAwULCQgHAgYVCgkICwIEFgIDAQIeAQIXgBYhBNK25epl5gCIsN0oT1dsSfma
HwhLBQJiVu+3AAoJEFdsSfmaHwhLhBMBALLFkddAJiMRU77YLk74wGNX+ghG/VDu
vpoYqk/7fuVGAP9sLnx8+UFNGCGbfdLS05w297o7BOIX3zsOnhrvE+M7B7g4BGJW
7UESCisGAQQBl1UBBQEBB0DuwfvvsSLyLEiPW0DPJu6RsgUq2YfhRnu2abPJYncM
QQMBCAeIeAQYFggAIBYhBNK25epl5gCIsN0oT1dsSfmaHwhLBQJiVu1BAhsMAAoJ
EFdsSfmaHwhLTwIBAJqBpCv+FVwvisiOplPlw6AMbwBcRtuUpNIJh1boNiYrAP9U
YgS1gHu06Tblwp+TqHQ7oppSg6jc7xxhiSXIy2ZaDQ==
=1rR/
-----END PGP PUBLIC KEY BLOCK-----

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

View file

@ -0,0 +1,7 @@
<html>
<head><title>301 Moved Permanently</title></head>
<body bgcolor="white">
<center><h1>301 Moved Permanently</h1></center>
<hr><center>nginx</center>
</body>
</html>

BIN
src/static/stickers/cc4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 446 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

661
src/themes/vugo/LICENSE Normal file
View file

@ -0,0 +1,661 @@
GNU AFFERO GENERAL PUBLIC LICENSE
Version 3, 19 November 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU Affero General Public License is a free, copyleft license for
software and other kinds of works, specifically designed to ensure
cooperation with the community in the case of network server software.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
our General Public Licenses are intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
Developers that use our General Public Licenses protect your rights
with two steps: (1) assert copyright on the software, and (2) offer
you this License which gives you legal permission to copy, distribute
and/or modify the software.
A secondary benefit of defending all users' freedom is that
improvements made in alternate versions of the program, if they
receive widespread use, become available for other developers to
incorporate. Many developers of free software are heartened and
encouraged by the resulting cooperation. However, in the case of
software used on network servers, this result may fail to come about.
The GNU General Public License permits making a modified version and
letting the public access it on a server without ever releasing its
source code to the public.
The GNU Affero General Public License is designed specifically to
ensure that, in such cases, the modified source code becomes available
to the community. It requires the operator of a network server to
provide the source code of the modified version running there to the
users of that server. Therefore, public use of a modified version, on
a publicly accessible server, gives the public access to the source
code of the modified version.
An older license, called the Affero General Public License and
published by Affero, was designed to accomplish similar goals. This is
a different license, not a version of the Affero GPL, but Affero has
released a new version of the Affero GPL which permits relicensing under
this license.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU Affero General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Remote Network Interaction; Use with the GNU General Public License.
Notwithstanding any other provision of this License, if you modify the
Program, your modified version must prominently offer all users
interacting with it remotely through a computer network (if your version
supports such interaction) an opportunity to receive the Corresponding
Source of your version by providing access to the Corresponding Source
from a network server at no charge, through some standard or customary
means of facilitating copying of software. This Corresponding Source
shall include the Corresponding Source for any work covered by version 3
of the GNU General Public License that is incorporated pursuant to the
following paragraph.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the work with which it is combined will remain governed by version
3 of the GNU General Public License.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU Affero General Public License from time to time. Such new versions
will be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU Affero General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU Affero General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU Affero General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
vugo
Copyright (C) 2022 Voidcruiser
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If your software can interact with users remotely through a computer
network, you should also make sure that it provides a way for users to
get its source. For example, if your program is a web application, its
interface could display a "Source" link that leads users to an archive
of the code. There are many ways you could offer source, and different
solutions will be better for different programs; see section 13 for the
specific requirements.
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU AGPL, see
<https://www.gnu.org/licenses/>.

View file

@ -0,0 +1,3 @@
# Voidcruiser Hugo theme
Hacked together based on the CSS I used before hugo and a whole bunch of examples on the internet

View file

@ -0,0 +1,6 @@
---
title: "{{ replace .Name "-" " " | title }}"
date: {{ .Date }}
draft: true
---

View file

@ -0,0 +1,7 @@
---
title: "{{ replace .TranslationBaseName "-" " " | title }}"
date: "{{ .Date }}"
author: "$HUMANOID"
tags: ["", ""]
description: ""
---

View file

@ -0,0 +1,8 @@
title = "Website Name"
baseURL = '/'
languageCode = 'en-us'
[params]
# "relatedtext" is the text that appears above the tag list at the bottom of pages.
#relatedtext = "Related:"
favicon = "/favicon.ico"

View file

@ -0,0 +1,22 @@
<!DOCTYPE html>
<html lang="{{ $.Site.Language }}">
<head>
{{ block "title" . }}
<title>{{ if .IsHome }}{{ $.Site.Title }}{{ else }}{{ .Title }} | {{ $.Site.Title }}{{ end }}</title>
{{ end }}
{{ partial "head.html" . }}
</head>
<body>
{{ partial "logo.html" . }}
{{ partial "menu.html" . }}
<h1 class="page-head">{{ .Title | markdownify }}</h1>
<main>
{{ block "main" . }}
{{ end }}
</main>
{{ block "footer" . }}
{{ partial "footer.html" . }}
{{ end }}
</body>
</html>

View file

@ -0,0 +1,16 @@
{{ define "main" }}
{{ .Content -}}
<div class="posts">
{{ $isntDefault := not (or (eq (trim $.Site.Params.contentTypeName " ") "rambles") (eq (trim $.Site.Params.contentTypeName " ") "")) }} {{ $contentTypeName := cond $isntDefault (string $.Site.Params.contentTypeName) "rambles" }} {{ $PageContext := . }} {{ if .IsHome }} {{ $PageContext = .Site }} {{ end }} {{ $paginator := .Paginate (where $PageContext.RegularPages "Type" $contentTypeName) }}
<table class="post-entry-meta"> {{ range first 10 .Paginator.Pages }}
<tr>
<td><a href="{{ .RelPermalink }}">{{ .Title | markdownify}}</a></td>
<td>{{ .Params.Date.Format "2006-01-02" }}</td>
<td>{{ .Params.Author }}</td>
<td>{{ range .Params.tags }} #<a href="{{ (urlize (printf "tags/%s/" .)) | absLangURL }}">{{ . }}</a>{{ end }}</td>
</tr> {{ end }}
</table>
<p><a class="button" href="/rambles">Read more</a></p>
</div>
</div>
{{ end }}

View file

@ -0,0 +1,25 @@
{{ define "main" }}
{{ if .Content }}
<article>
{{ .Content -}}
</article>
{{ end }}
<div class="posts">
<table class="post-entry-meta">
{{ range .Paginator.Pages }}
<tr>
<td><a href="{{ .RelPermalink }}">{{ .Title | markdownify}}</a></td>
<td>{{ .Params.Date.Format "2006-01-02" }}</td>
<td>{{ .Params.Author }}</td>
<td > {{ range .Params.tags }} #<a href="{{ (urlize (printf "tags/%s/" .)) | absLangURL }}">{{ . }}</a>&nbsp; {{ end }}</td>
<!--
{{ if and (.Param "readingTime") (eq (.Param "readingTime") true) }}
<td class="post-reading-time">- {{ .ReadingTime }} min read ({{ .WordCount }} words)</td>
{{ end }}
-->
</tr>
{{ end }}
</table>
</div>
{{ partial "pagination.html" . }}
{{ end }}

View file

@ -0,0 +1,39 @@
{{- $pctx := . -}}
{{- if .IsHome -}}{{ $pctx = .Site }}{{- end -}}
{{- $pages := slice -}}
{{- if or $.IsHome $.IsSection -}}
{{- $pages = $pctx.RegularPages -}}
{{- else -}}
{{- $pages = $pctx.Pages -}}
{{- end -}}
{{- $limit := .Site.Config.Services.RSS.Limit -}}
{{- if ge $limit 1 -}}
{{- $pages = $pages | first $limit -}}
{{- end -}}
{{- printf "<?xml version=\"1.0\" encoding=\"utf-8\" standalone=\"yes\"?>" | safeHTML }}
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
<channel>
<title>{{ .Site.Title }}</title>
<link>{{ .Permalink }}</link>
<description>Recent content {{ if ne .Title .Site.Title }}{{ with .Title }}in {{.}} {{ end }}{{ end }}on {{ .Site.Title }}</description>
<generator>Hugo -- gohugo.io</generator>{{ with .Site.LanguageCode }}
<language>{{.}}</language>{{end}}{{ with .Site.Author.email }}
<managingEditor>{{.}}{{ with $.Site.Author.name }} ({{.}}){{end}}</managingEditor>{{end}}{{ with .Site.Author.email }}
<webMaster>{{.}}{{ with $.Site.Author.name }} ({{.}}){{end}}</webMaster>{{end}}{{ with .Site.Copyright }}
<copyright>{{.}}</copyright>{{end}}{{ if not .Date.IsZero }}
<lastBuildDate>{{ .Date.Format "Mon, 02 Jan 2006 15:04:05 -0700" | safeHTML }}</lastBuildDate>{{ end }}
{{- with .OutputFormats.Get "RSS" -}}
{{ printf "<atom:link href=%q rel=\"self\" type=%q />" .Permalink .MediaType | safeHTML }}
{{- end -}}
{{ range $pages }}
<item>
<title>{{ .Title }}</title>
<link>{{ .Permalink }}</link>
<pubDate>{{ .Date.Format "Mon, 02 Jan 2006 15:04:05 -0700" | safeHTML }}</pubDate>
{{ with .Site.Author.email }}<author>{{.}}{{ with $.Site.Author.name }} ({{.}}){{end}}</author>{{end}}
<guid>{{ .Permalink }}</guid>
<description>{{ .Content | html }}</description>
</item>
{{ end }}
</channel>
</rss>

View file

@ -0,0 +1,43 @@
{{ define "main" }}
{{ if eq .Type $.Site.Params.contentTypeName }}
<div class="post-meta">
{{ if .Params.Date }} <span class="post-date"> {{ .Date.Format "2006-01-02" }} </span> {{ end }}
{{ with .Params.Author }} <span> - </span> <span class="post-author">{{ . }}</span> {{ end }}
<span class="post-reading-time">- {{ .ReadingTime }} min read ({{ .WordCount }} words)</span>
</span>
{{ if .Params.tags }}
<span class="post-tags">
{{ range .Params.tags }}
#<a href="{{ (urlize (printf "tags/%s/" .)) | absLangURL }}">{{ . }}</a>&nbsp;
{{ end }}
</span>
{{ end }}
</div>
{{ end }}
{{ if (.Params.Toc | default .Site.Params.Toc) }}
<div class="table-of-contents">
<h2>
{{ (.Params.TocTitle | default .Site.Params.TocTitle) | default "Table of Contents" }}
</h2>
{{ .TableOfContents }}
</div>
{{ end }}
{{ if eq .Type $.Site.Params.contentTypeName }}
<div class="post-content">
{{- with .Content -}}
{{ . | replaceRE "(<h[1-9] id=\"([^\"]+)\".+)(</h[1-9]+>)" `${1} <a href="#${2}" class="anchor" ariaLabel="Anchor">#</a> ${3}` | safeHTML }}
{{- end -}}
</div>
{{ else }}
{{ .Content }}
{{ end }}
{{ if eq .Type $.Site.Params.contentTypeName }}
{{ partial "posts_pagination.html" . }}
{{ end }}
</div>
{{ end }}

View file

@ -0,0 +1,7 @@
<footer>
<div class="mirror">
[<a rel="rss" href="/index.xml">rss</a>]
[*Insert mirrors here*]
<a rel="lisence" href="http://creativecommons.org/licenses/by-sa/4.0/"><img alt="CC BY-SA 4.0" src="/stickers/cc4.png"></a>
</div>
</footer>

View file

@ -0,0 +1,13 @@
<head>
<link rel="canonical" href="{{ .Site.BaseURL }}">
<link rel='alternate' type='application/rss+xml' title="{{ .Site.Title }} RSS" href='/index.xml'>
<link rel='stylesheet' type='text/css' href='/style.css'>
{{ with .Site.Params.favicon }}<link rel="icon" href="{{ . }}">
{{ end -}}
<meta name="description" content="{{ with .Params.description }}{{ . }}{{ else }}{{ .Summary }}{{ end }}">
{{ if isset .Params "tags" }}<meta name="keywords" content="{{ with .Params.tags }}{{ delimit . ", " }}{{ end }}">
{{ end -}}
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="robots" content="index, follow">
<meta charset="utf-8">
</head>

View file

@ -0,0 +1,8 @@
<div class="logo">
<img src="/logoOptimised.png" />
<h1>
<a href="{{ if $.Site.Params.Logo.LogoHomeLink }}{{ $.Site.Params.Logo.LogoHomeLink }}{{else}}{{ $.Site.BaseURL }}{{ end }}">
{{ with $.Site.Params.Logo.logoText }}{{ . }}{{ else }}Void{{ end }}
</a>
</h1>
</div>

View file

@ -0,0 +1,6 @@
<menu>
<a href="/">home<br/></a> {{ range $.Site.Menus.main }} {{ if not .HasChildren }}
<a href="{{ .URL }}">{{ .Name }}<br/></a> {{ end }} {{ end }}
<hr />
{{ range $.Site.Menus.services }} {{ if not .HasChildren }} <a href="{{ .URL }}">{{ .Name }}<br/></a> {{ end }} {{ end }}
</menu>

View file

@ -0,0 +1,10 @@
{{ if or .Next .Prev -}}
<div id="nextprev">
{{- with .Prev }}
<a href="{{ .RelPermalink}}"><div id="prevart">Previous:<br>{{.Title}}</div></a>
{{ end -}}
{{- with .Next -}}
<a href="{{ .RelPermalink}}"><div id="nextart">Next:<br>{{.Title}}</div></a>
{{ end -}}
</div>
{{ end -}}

View file

@ -0,0 +1,8 @@
<div class="pagination__buttons">
{{ if .Paginator.HasPrev }}
<a class="button prev" href="{{ .Paginator.Prev.URL }}"> {{ $.Site.Params.newerPosts | default "Newer posts" }} </a>
{{ end }}
{{ if .Paginator.HasNext }}
<a class="button next" href="{{ .Paginator.Next.URL }}"> {{ $.Site.Params.olderPosts | default "Older posts" }} </a>
{{ end }}
</div>

View file

@ -0,0 +1,8 @@
<div class="pagination__buttons">
{{ if .NextInSection }}
<a class="button prev" href="{{ .NextInSection.RelPermalink }}">{{ .NextInSection.Title }}</a>
{{ end }}
{{ if .PrevInSection }}
<a class="button next" href="{{ .PrevInSection.RelPermalink }}">{{ .PrevInSection.Title }}</a>
{{ end }}
</div>

View file

@ -0,0 +1,6 @@
<audio controls="true">
<source src="{{.Get "src" }}" type="audio/{{.Get "audio" }}">
Sorry mate, your browser doesn't support playing audio files.
</source>
</audio>

View file

@ -0,0 +1 @@
<b class="gay">{{ .Get "content" }}</b>

View file

@ -0,0 +1 @@
<b class="gay"><i>{{ .Get "content" }}</i></b>

View file

@ -0,0 +1 @@
</details>

View file

@ -0,0 +1 @@
<span class="gay">{{ .Get "content" }}</span>

View file

@ -0,0 +1 @@
<blockquote class="hyperbowl">"{{ .Get "content" }}"</blockquote>

View file

@ -0,0 +1,20 @@
<!--
class: class of the figure
link: url the image directs to
alt: alternative text
caption: caption
mouse: what the image says when moused over ("title" in HTML)
-->
<figure {{ with .Get "class" }}class="{{.}}"{{ end -}}>
{{- with .Get "link"}}<a href="{{.}}">{{ end -}}
<img src="{{ .Get "src" }}"
{{- with .Get "mouse" }} title="{{.}}"{{ end -}}
{{- with .Get "alt" }} alt="{{.}}"{{ end -}}
>
{{- if .Get "link"}}</a>{{ end -}}
{{- with .Get "caption" -}}
<figcaption>
{{- . -}}
</figcaption>
{{- end -}}
</figure>

View file

@ -0,0 +1 @@
<i class="gay">{{ .Get "content" }}</i>

View file

@ -0,0 +1,4 @@
<noscript>
<hr />
<p class="gay">{{ .Get "content" }}</p>
</noscript>

View file

@ -0,0 +1,4 @@
<details>
<summary>
{{ .Get "summary" }}
</summary>

View file

@ -0,0 +1,3 @@
{{ if isset .Site.Taxonomies "tags" }}{{ if not (eq (len .Site.Taxonomies.tags) 0) }} <ul id="tagcloud">
{{ range $name, $items := .Site.Taxonomies.tags }}{{ $url := printf "%s/%s" "tags" ($name | urlize | lower)}}<li><a href="{{ $url | absURL }}" id="tag_{{ $name }}">{{ $name | title }}</a></li>
{{ end }}</ul>{{ end }}{{ end }}

Binary file not shown.

View file

@ -0,0 +1,48 @@
:root {
/* gruvbox dark */
--ansi00: #282828;
--ansi00-hard: #1d2021;
--ansi00-soft: #32302f;
--ansi01: #cc241d;
--ansi02: #98971a;
--ansi03: #d79921;
--ansi04: #458588;
--ansi05: #b16286;
--ansi06: #689d6a;
--ansi07: #a89984;
--ansi08: #928374;
--ansi09: #fb4934;
--ansi10: #b8bb26;
--ansi11: #fabd2f;
--ansi12: #83a598;
--ansi13: #d3869b;
--ansi14: #8ec07c;
--ansi15: #ebdbb2;
--selection-fg: #ededed;
--selection-bg: #d65d0e;
}
@media (prefers-color-scheme:light){
:root{
/* gruvbox light */
--ansi00: #fbf1c7;
--ansi00-hard: #f9f5d7;
--ansi00-soft: #f2e5bc;
--ansi01: #cc241d;
--ansi02: #98971a;
--ansi03: #d79921;
--ansi04: #458588;
--ansi05: #b16286;
--ansi06: #689d6a;
--ansi07: #7c6f64;
--ansi08: #928374;
--ansi09: #9d0006;
--ansi10: #79740e;
--ansi11: #b57614;
--ansi12: #076678;
--ansi13: #8f3f71;
--ansi14: #427b58;
--ansi15: #3c3836;
--selection-fg: #3c3836;
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View file

@ -0,0 +1,95 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
viewBox="0 0 128pt 128pt"
version="1.1"
id="svg363"
sodipodi:docname="logoOptimised.svg"
width="500"
height="500"
inkscape:version="1.2.1 (9c6d41e410, 2022-07-14)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<title
id="title18">Chaos Wheel</title>
<defs
id="defs28" />
<g
id="g22"
transform="matrix(2.2026076,0,0,2.2026173,117.84692,117.84376)">
<path
d="M 60,0 A 60,60 0 0 0 0,60 60,60 0 0 0 60,120 60,60 0 0 0 120,60 60,60 0 0 0 60,0 Z m 0,9.2377 A 50.762,50.762 0 0 1 110.762,59.9997 50.762,50.762 0 0 1 60,110.7617 50.762,50.762 0 0 1 9.238,59.9997 50.762,50.762 0 0 1 60,9.2377 Z"
id="path2" />
<path
transform="scale(0.26458)"
d="m 207.87,-113.38 -0.002,203.98 20.121,22.4 17.674,-19.682 0.002,-206.7 H 207.87 Z M 28.49,4.49 5.088,27.82 l 116.5,116.86 26.496,1.4199 -1.2285,-22.879 -118.37,-118.73 z m 397.22,0.60742 -116.48,116.13 -1.332,24.863 24.52,-1.3164 116.63,-116.27 -23.33,-23.402 z m -539.1,202.77 v 37.797 h 206.5 l 20.275,-18.213 v -2.8984 l -18.58,-16.686 h -208.19 z m 680.3,0.002 -205.75,0.006 -20.164,18.111 21.92,19.686 204,-0.006 -0.002,-37.797 z m -259.01,98.023 1.457,27.135 115.67,116.02 23.402,-23.332 -118.28,-118.63 -22.254,-1.1934 z m -159.81,0.01 -25.5,1.3691 -118.12,117.76 23.332,23.4 119.01,-118.65 1.2793,-23.885 z m 78.871,34.244 -19.113,21.283 0.004,205.49 h 37.795 l -0.004,-208.28 -16.611,-18.494 h -2.0703 z"
id="path4" />
<path
transform="scale(0.26458)"
d="m -202.22,226.77 76.81,-20.827 56.441,-56.106 -20.368,76.933 20.368,76.933 -56.441,-56.106 z"
id="path6" />
<path
transform="matrix(-0.26458,6.6311e-6,-6.6311e-6,-0.26458,120,120)"
d="m -202.22,226.77 76.81,-20.827 56.441,-56.106 -20.368,76.933 20.368,76.933 -56.441,-56.106 z"
id="path8" />
<path
transform="matrix(-2.9145e-6,0.26458,-0.26458,-2.9145e-6,120,0.0023416)"
d="m -202.22,226.77 76.81,-20.827 56.441,-56.106 -20.368,76.933 20.368,76.933 -56.441,-56.106 z"
id="path10" />
<path
transform="matrix(-3.7181e-6,-0.26458,0.26458,-3.7181e-6,-0.0032404,120)"
d="m -202.22,226.77 76.81,-20.827 56.441,-56.106 -20.368,76.933 20.368,76.933 -56.441,-56.106 z"
id="path12" />
<path
transform="matrix(0.16332,0.16382,-0.16382,0.16332,60.112,-14.187)"
d="m -202.22,226.77 76.81,-20.827 56.441,-56.106 -20.368,76.933 20.368,76.933 -56.441,-56.106 z"
id="path14" />
<path
transform="matrix(-0.16333,-0.16382,0.16382,-0.16333,59.885,134.19)"
d="m -202.22,226.77 76.81,-20.827 56.441,-56.106 -20.368,76.933 20.368,76.933 -56.441,-56.106 z"
id="path16" />
<path
transform="matrix(-0.16382,0.16332,-0.16332,-0.16382,134.19,60.115)"
d="m -202.22,226.77 76.81,-20.827 56.441,-56.106 -20.368,76.933 20.368,76.933 -56.441,-56.106 z"
id="path18" />
<path
transform="matrix(0.16382,-0.16333,0.16333,0.16382,-14.19,59.886)"
d="m -202.22,226.77 76.81,-20.827 56.441,-56.106 -20.368,76.933 20.368,76.933 -56.441,-56.106 z"
id="path20" />
</g>
<metadata
id="metadata16">
<rdf:RDF>
<cc:Work
rdf:about="">
<cc:license
rdf:resource="http://artlibre.org/licence/lal" />
<dc:title>Chaos Wheel</dc:title>
<dc:creator>
<cc:Agent>
<dc:title>The Voidcruiser</dc:title>
</cc:Agent>
</dc:creator>
</cc:Work>
<cc:License
rdf:about="http://artlibre.org/licence/lal">
<cc:permits
rdf:resource="http://creativecommons.org/ns#Reproduction" />
<cc:permits
rdf:resource="http://creativecommons.org/ns#Distribution" />
<cc:permits
rdf:resource="http://creativecommons.org/ns#DerivativeWorks" />
<cc:requires
rdf:resource="http://creativecommons.org/ns#ShareAlike" />
<cc:requires
rdf:resource="http://creativecommons.org/ns#Notice" />
<cc:requires
rdf:resource="http://creativecommons.org/ns#Attribution" />
</cc:License>
</rdf:RDF>
</metadata>
</svg>

After

Width:  |  Height:  |  Size: 4.4 KiB

View file

@ -0,0 +1,148 @@
@charset "UTF-8";
@import "gruvbox.css";
@font-face {
font-family: "BQN386";
src: url('/fonts/BQN386.ttf');
}
:root{
scrollbar-color: var(--ansi08) transparent;
}
:target{border-bottom: 1pt dashed; background-color: var(--ansi00-soft);}
::selection{
background-color: var(--selection-bg);
color: var(--selection-fg);
}
a:hover::selection{ background-color: var(--ansi03); }
html{
color: var(--ansi15);
background-color: var(--ansi00-hard);
}
body{ padding: 0; margin: 0; }
a:hover:active{ background-color: var(--ansi03); }
a{ color: var(--ansi11); }
a:hover{ color: var(--ansi00-hard); background-color: var(--ansi11); text-decoration: none;}
a:hover > code{ color: var(--ansi00-hard); background-color: var(--ansi11); text-decoration: none;}
audio{ width: 100%; display: block; margin-top: 1em; }
a.anchor{visibility:hidden; text-decoration: none;}
h1:hover>a,h2:hover>a,h3:hover>a,h4:hover>a,h5:hover>a,h6:hover>a{visibility:visible;}
main{ margin-left: 12em; margin-right: 1em; padding: 1em; border:1pt solid; margin-bottom: 1em;}
main h1{border-bottom: 1pt solid;}
main a h1 {border-bottom: transparent 0pt solid;}
li::marker{color: var(--ansi05);}
menu{
height: 100%;
padding: 0.5em;
float:left;
background: var(--ansi00-soft);
border-right:solid 1pt;
border-bottom: solid 1pt;
width: 10em;
text-transform: capitalize;
margin-top:0;
}
menu li{list-style: none; list-style-position:outside;}
menu a{ display: block;}
nav{
border-bottom: 1pt solid;
margin-bottom: .5em;
}
summary{cursor:pointer;}
summary:hover{ color: var(--ansi00-hard); background-color: var(--ansi11); }
footer{text-align:center; border-top:1pt solid; margin-top: 1em;}
footer a{text-decoration:none;}
footer .mirror{text-align:center; color: var(--ansi11);}
code{ background: var(--ansi00); }
pre{ overflow-x: auto; background: var(--ansi00); padding: 1em; border-radius: 1em;}
blockquote{ background-color: var(--ansi00); padding:0.5em; border-radius:1em;}
hr{color: var(--ansi00);}
.hyperbowl{ font-style: italic; font-size: 1.2em;}
.logo{
padding: 5pt 0;
margin:0;
width:100%;
border-bottom: 2pt solid;
background-color: var(--ansi00-soft);
background: linear-gradient(to right,var(--ansi05),var(--ansi00));
}
.logo a{ color: var(--ansi00); text-decoration: none; }
.logo a:hover{ background-color: var(--ansi03); }
.logo img{
float:left;
max-height:5em;
}
.about ul{list-style:none;}
.about img{height:10em; float:left;}
@media (prefers-color-scheme:dark){
.about img{filter:invert(.92)}
}
header{text-transform: capitalize; text-align:center;border-bottom:2pt solid; font-weight:bold;}
.page-head{text-transform: capitalize; text-align:center;border-bottom:2pt solid; font-weight:bold;}
@media (max-width: 800px){
main{max-width:100%; margin: 1em;}
menu{padding:0; width: 100%; border-right:solid 0pt;}
menu a{ padding-left: 1em;}
.post-content{
max-width: 80ch;
}
}
@media (min-width: 80ch){
.post-content{ column-count: 1;
max-width: 80ch;
margin: auto;
}
}
@media (min-width: 160ch){
.post-content{ column-count: 2;
max-width: 160ch;
margin: auto;
}
}
@media (min-width: 240ch){
.post-content{ column-count: 3;
max-width: 240ch;
margin: auto;
}
}
.post-content{
margin: auto;
}
.post-content img{
max-width: 100%;
text-align: center;
}
.stickers{ text-align:center; }
.gay{
background:linear-gradient(-45deg, var(--ansi01), var(--ansi02), var(--ansi03), var(--ansi04), var(--ansi05), var(--ansi06));
background-clip: text;
background-size:300%;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
animation:gay 8s ease-in-out infinite;
-moz-animation: gay 8s ease-in-out infinite;
-webkit-animation: gay 8s ease-in-out infinite;
}
table{ border-spacing:0; border: 1pt solid; padding:1em; margin:1em 0; }
td{ border-bottom: 1pt dashed; }
.posts{ overflow-x: auto; }
.posts p{ text-align: center;}
.post-meta{ color: var(--ansi08); border-bottom: 1pt solid var(--ansi15); padding-bottom:.25em; margin-bottom: 1em;}
.post-meta a{ text-decoration: none; color: inherit;}
.post-meta a:hover{ color: var(--ansi15); background-color: var(--ansi00-hard);}
.post-entry-meta{ width: 100%; background-color: var(--ansi00); padding:.5em;}
.post-entry-meta table{color: var(--ansi07); margin-bottom: .5em;}
.button{padding:.3em;border-radius:.3em;}
.pagination__buttons{display:flex; justify-content:center;}
.pagination__buttons .next::after{content:" →";}
.pagination__buttons .prev::before{content:"← ";}
.language-bqn{font-family: "BQN386", monospace;}
.language-orca{font-family: "BQN386", monospace;}
@keyframes gay {
0%{background-position: 0px 50%;}
50%{background-position: 100% 50%;}
10%{background-position: 0px 50%;}
}