Compare commits

...

21 commits

Author SHA1 Message Date
8fc5a0fbda FreshLists skeleton 2025-03-15 18:22:52 -04:00
d79c4b6f46 Tweak LessMacrosMoreTypes blurb 2025-03-15 18:22:45 -04:00
a85bfdcd2a Fix series list styling 2025-03-15 18:19:25 -04:00
a5f9c659ff No special characters in slugs 2025-03-15 18:07:37 -04:00
a2a811aa4a Header link support 2025-03-15 17:56:00 -04:00
51e50cd0d6 Make about special and style 2025-03-15 17:33:40 -04:00
5ced2f6298 Hireme initial contents 2025-03-15 16:17:24 -04:00
3d2d1f897e Special post handling 2025-03-15 16:15:15 -04:00
c0780047bf Basic hire-me post 2025-03-15 14:38:30 -04:00
0e87e752d2 Add header links to blog meta 2025-03-15 14:28:22 -04:00
79ee0d4c63 typo correct blog upload destination 2025-02-15 21:16:27 -05:00
e01342c980 fix footer on mobile 2025-02-15 21:05:08 -05:00
4c6839c698 add upload command 2025-02-15 05:47:47 -05:00
4aefda36fd oops 2025-02-15 05:35:01 -05:00
88bb800ec4 Clean up footer links 2025-02-15 05:31:50 -05:00
75b174f0a7 fix oops 2025-02-15 04:57:39 -05:00
d6b17a4be0 Credit mastodon 2025-02-15 04:56:42 -05:00
3069e480a4 Setup footer 2025-02-15 04:50:58 -05:00
63e581ac03 Select from static format strings at runtime 2025-02-14 23:42:18 -05:00
82485924be Spelling corrections and typo fixes 2025-02-14 16:45:53 -05:00
4de44c7e8c Add comments 2025-02-12 21:26:00 -05:00
29 changed files with 801 additions and 33 deletions

45
blog
View file

@ -3,6 +3,7 @@ use v6.e.PREVIEW;
use DB;
use DB::BlogMeta;
use DB::HeaderLink;
use DB::Series;
use DB::MarkdownPost;
use DB::IdrisPost;
@ -417,3 +418,47 @@ multi MAIN(
$series.post-ids.push: $post-id.Int;
$db.write: $db-dir;
}
#| Add a header link
multi MAIN(
"header-link",
"add",
#| The path of the database directory
IO::Path(Str) :$db-dir = $default-db-dir,
) {
my $db = read-db $db-dir;
print "Link Text: ";
my $text = get;
print "Link Location: ";
my $link = get;
print "Icon: ";
my $icon = get;
my $header-link =
HeaderLink.new:
text => $text, link => $link, icon => $icon;
$db.meta.header-links.push: $header-link;
$db.write: $db-dir;
}
#| Do a clean build and upload the blog to the remote
multi MAIN(
"upload",
#| The path of the database directory
IO::Path(Str) :$db-dir = $default-db-dir,
#| The path of the output directory
IO::Path(Str) :$site-dir = $default-site-dir,
) {
my $db = read-db $db-dir;
my $site = "{$site-dir.absolute}/";
# Clean out the site dir
my $proc = run <rm -rf>, $site;
die "clean failed" unless $proc;
# Render the site
$db.render: $site-dir;
# Upload it
$proc = run <rsync -avzhL>, $site, <static.stranger.systems:/var/www/www.stranger.systems>;
die "rsync failed" unless $proc;
}

View file

@ -1,6 +1,13 @@
{
"about-id": 2,
"base-url": "https://www.stranger.systems",
"header-links": [
{
"icon": "receipt",
"link": "/posts/by-slug/hire-me.html",
"text": "Hire Me"
}
],
"placeholder-id": 0,
"tagline": "Making software better by making it weird",
"title": "Stranger Systems"

View file

@ -7,6 +7,7 @@
"slugs": [
],
"source": "/dev/null",
"special": false,
"tags": [
]
}

View file

@ -7,6 +7,7 @@
"slugs": [
],
"source": "/home/nathan/Projects/Blog/projects/Markdown/MyNewBlog.md",
"special": false,
"tags": [
"meta",
"raku"

View file

@ -7,6 +7,7 @@
"slugs": [
],
"source": "/home/nathan/Projects/Blog/projects/Markdown/About.md",
"special": true,
"tags": [
]
}

View file

@ -7,6 +7,7 @@
"slugs": [
],
"source": "/home/nathan/Projects/Blog/projects/Markdown/CryptoSuite.md",
"special": false,
"tags": [
"cryptography"
]

View file

@ -7,6 +7,7 @@
"slugs": [
],
"source": "/home/nathan/Projects/Blog/projects/Markdown/2025/01-Jan/AdventOfBugs.md",
"special": false,
"tags": [
"idris",
"advent-of-code"

View file

@ -1,5 +1,7 @@
{
"edited-at": [
"2025-02-14T12:00:50.492225-05:00",
"2025-02-14T23:42:15.625034-05:00"
],
"hidden": false,
"idris": true,
@ -9,6 +11,7 @@
],
"source": "/home/nathan/Projects/Blog/projects/Idris/src/LessMacrosMoreTypes/Printf.md",
"source-code": "https://git.stranger.systems/thatonelutenist/website/src/branch/trunk/projects/Idris/src/LessMacrosMoreTypes/Printf.md",
"special": false,
"tags": [
"idris"
]

13
db/posts/6.json Normal file
View file

@ -0,0 +1,13 @@
{
"edited-at": [
],
"hidden": true,
"markdown": true,
"posted-at": "2025-03-15T14:36:38.402495-04:00",
"slugs": [
],
"source": "/home/nathan/Projects/Blog/projects/Markdown/HireMe.md",
"special": true,
"tags": [
]
}

16
db/posts/7.json Normal file
View file

@ -0,0 +1,16 @@
{
"edited-at": [
],
"hidden": false,
"idris": true,
"ipkg": "/home/nathan/Projects/Blog/projects/Idris/Idris.ipkg",
"posted-at": "2025-03-15T18:07:00.649227-04:00",
"slugs": [
],
"source": "/home/nathan/Projects/Blog/projects/Idris/src/DependentNuggets/FreshLists.md",
"source-code": "https://git.stranger.systems/thatonelutenist/website/src/branch/trunk/projects/Idris/src/DependentNuggets/FreshLists.md",
"special": false,
"tags": [
"idris"
]
}

View file

@ -1,7 +1,7 @@
{
"desc": "Macros are annoying, but an unfortunate fact of life in many programming languages. Especially in languages with nominally \"strong\" type systems, like Rust, macros are quite frequently needed to work around the type system to avoid needless repetition when consuming an API, generate formulaic boilerplate that only exists to please the type system, or work around the lack of variadic functions for things like printf. Lets explore the ways we can use dependently typed constructs to eliminate the need for such macros.",
"desc": "Macros are annoying, but an unfortunate fact of life in many programming languages.</p><p>Especially in languages with nominally \"strong\" type systems, like Rust, macros are quite frequently needed to work around the type system to avoid needless repetition when consuming an API, generate formulaic boilerplate that only exists to please the type system, or work around the lack of variadic functions for things like printf. Lets explore the ways we can use dependently typed constructs to eliminate the need for such macros.",
"post-ids": [
5
],
"title": "Less Macros, More Types"
}
}

7
db/series/1.json Normal file
View file

@ -0,0 +1,7 @@
{
"desc": "Introductions to the building blocks of dependently typed programs.",
"post-ids": [
7
],
"title": "Dependent Nuggets"
}

View file

@ -5,6 +5,7 @@ use HTML::Functional;
use Render::Util;
use Render::Head;
use Render::Post;
use Render::Foot;
use DB::BlogMeta;
use DB::Post;
@ -13,9 +14,20 @@ unit class Config;
method generate-post(Int:D $id, Post:D $post, $db) {
my $meta = $db.meta;
my $content = $post.render-html;
my $cactus-script = qq
document.addEventListener('DOMContentLoaded', () => \{
initComments(\{
node: document.getElementById("comment-section"),
defaultHomeserverUrl: "https://matrix.cactus.chat",
serverName: "cactus.chat",
siteName: "stranger.systems",
commentSectionId: "post-$id"
\})\})」;
my $head =
head [
# Generate the universal header components
generate-head($meta, $post.title, $post.description);
# Open graph tags for embedding
meta :property<og:title>, :content($post.title);
meta :property<og:url>, :content(post-link-abs $db.meta, $id, $post);
meta :property<og:site_name>, :content($db.meta.title);
@ -26,20 +38,38 @@ method generate-post(Int:D $id, Post:D $post, $db) {
$post.tags.map(-> $tag {
meta :property<article:tag>, :content($tag)
});
# Cactus comments support
link :rel<stylesheet>,
:href</resources/cactus.css>,
:type<text/css>;
script :src<https://gateway.pinata.cloud/ipfs/QmSiWN27KZZ1XE32jKwifBnS3nWTUcFGNArKzur2nmDgoL/v0.13.0/cactus.js>;
# Only actually load the script if the post isn't hidden
optl !$post.hidden, -> {script $cactus-script};
];
my $body =
body [
site-header $meta;
article :class<post>, [
post-header $id, $post, $db;
div :class<post-body>, [
$content;
]
]
# Only generate the post header if the post isn't special
optl !$post.special, -> {post-header $id, $post, $db};
# If the post is special, wrap it in a special div
do if $post.special {
div :class<special-post>, [
div :class<post-body>, [
$content;
];
];
} else {
div :class<post-body>, [
$content;
];
}
];
# Only actually have the comment section if the post isn't hidden
optl !$post.hidden, -> {div :id<comment-section>, :class<comments>};
generate-footer;
];
# TODO: Setup Comments
# TODO: Setup footer
# my $footer;
my $html = html :lang<en>, [
$head,
@ -64,6 +94,7 @@ method generate-index($db) {
div :class<post-blurbs>, [
h1 "Recent Posts"
], @most-recent;
generate-footer;
];
my $html =
@ -89,6 +120,7 @@ method generate-archive($db) {
div :class<post-blurbs>, [
h1 "All Posts"
], @most-recent;
generate-footer;
];
my $html =
@ -147,6 +179,7 @@ method generate-tags-page($db, @tags) {
div :class<tags>, [
h1 "Tags";
], @tags.map(-> $tag {self.generate-tag-blurb($db, $tag, 4)});
generate-footer;
];
my $html =
@ -163,6 +196,7 @@ method generate-tag-page($db, $tag) {
my $body = body [
site-header $db.meta;
self.generate-tag-blurb($db, $tag, 4);
generate-footer;
];
my $html =

View file

@ -159,10 +159,11 @@ class PostDB {
mkdir $res-dir unless $res-dir.e;
# symlink the resources directory to make "interactive" styling eaiser
# TODO: Directories support
%?RESOURCES<colors.css>.IO.symlink: $res-dir.add('colors.css') unless %?RESOURCES<colors.css>.IO.e;
%?RESOURCES<main.css>.IO.symlink: $res-dir.add('main.css') unless %?RESOURCES<main.css>.IO.e;
%?RESOURCES<code.css>.IO.symlink: $res-dir.add('code.css') unless %?RESOURCES<code.css>.IO.e;
%?RESOURCES<admonitions.css>.IO.symlink: $res-dir.add('admonitions.css') unless %?RESOURCES<admonitions.css>.IO.e;
%?RESOURCES<colors.css>.IO.symlink: $res-dir.add('colors.css') unless $res-dir.add('colors.css').e;
%?RESOURCES<main.css>.IO.symlink: $res-dir.add('main.css') unless $res-dir.add('main.css').e;
%?RESOURCES<code.css>.IO.symlink: $res-dir.add('code.css') unless $res-dir.add('code.css').e;
%?RESOURCES<admonitions.css>.IO.symlink: $res-dir.add('admonitions.css') unless $res-dir.add('admonitions.css').e;
%?RESOURCES<cactus.css>.IO.symlink: $res-dir.add('cactus.css') unless $res-dir.add('cactus.css').e;
}
#| Get a list of posts sorted by date

View file

@ -1,6 +1,7 @@
use v6.e.PREVIEW;
use JSON::Class:auth<zef:vrurg>;
use DB::HeaderLink;
# Top level metadata for the blog
unit class BlogMeta is json(:pretty);
@ -17,6 +18,9 @@ has Int:D $.placeholder-id is rw = 0;
#| The id of the about post
has Int:D $.about-id is rw = 0;
#| Optional list of extra header links
has HeaderLink:D @.header-links is rw = [];
#| The base url of this post
has Str:D $.base-url is required;

12
lib/DB/HeaderLink.rakumod Normal file
View file

@ -0,0 +1,12 @@
use v6.e.PREVIEW;
use JSON::Class:auth<zef:vrurg>;
# Additional header links
unit class HeaderLink is json(:pretty);
has Str:D $.link is required is rw;
has Str:D $.text is required is rw;
has Str $.icon is rw;

View file

@ -43,6 +43,8 @@ has Str:D @.tags is rw is json = [];
has Bool:D $.hidden is json is rw = False;
#| An optional link to the source code for the post
has Str $.source-code is rw is json;
#| Special posts follow different rendering rules
has Bool:D $.special is json is rw = False;
#| Get the title for this post, intended to be extracted from whatever
#| document produced it
@ -60,9 +62,9 @@ method updated(--> DateTime:D) {
#| Get the list of slugs for this post, including ones auto generated from
#| the title, as well as any additional slugs
method all-slugs(--> Array[Str:D]) {
my $title-words = self.title.lc.trim.words;
my $long-title-slug = $title-words.join('-');
my $six-word-slug = self.title.lc.words.head(6).join('-');
my @title-words = self.title.lc.trim.words.map(*.subst(/<-alnum>/, :g));
my $long-title-slug = @title-words.join('-');
my $six-word-slug = @title-words.head(6).join('-');
my Str:D @slugs = @!slugs.clone;
@slugs.push($long-title-slug);
@slugs.push($six-word-slug);

69
lib/Render/Foot.rakumod Normal file
View file

@ -0,0 +1,69 @@
use v6.e.PREVIEW;
unit module Render::Foot;
use HTML::Functional;
use Render::Util;
use DB::BlogMeta;
sub footer-link($name, $title, $path, $icon) {
div :class<footer-link>, [
$icon;
a :href($path), :title($title), [
$name;
]
]
}
sub generate-footer() is export {
footer [
div :class<contact>, [
div :class<footer-title>, span "Contact Me";
div :class<footer-links>, [
footer-link (^ "Discord"),
"Discord",
"https://discordapp.com/users/thatonelutenist",
simple-icon "discord";
footer-link (^ "Matrix"),
"Matrix",
"https://matrix.to/#/@thatonelutenist:stranger.systems",
simple-icon "matrix";
footer-link (^ "Public Email Inbox"),
"Email",
"mailto:~thatonelutenist/public-inbox@lists.sr.ht",
icon-solid "envelope";
];
];
div :class<code>, [
div :class<footer-title>, span "Find My Code";
div :class<footer-links>, [
footer-link "git.stranger.systems",
"Stranger Systems Forgejo",
"https://git.stranger.systems/explore/repos",
simple-icon "forgejo";
footer-link (^ "sr.ht"),
"sr.ht",
"https://sr.ht/~thatonelutenist/",
simple-icon "sourcehut";
footer-link (^ "Github"),
"Github",
"https://github.com/nmccarty",
simple-icon "github";
footer-link (^ "Gitlab"),
"Gitlab",
"https://gitlab.com/thatonelutenist",
simple-icon "gitlab";
];
];
div :class<socials>, [
div :class<footer-title>, span "Social Media";
div :class<footer-links>, [
footer-link "Mastodon",
"Mastodon",
"https://hachyderm.io/@thatonelutenist",
simple-icon "mastodon";
];
]
]
}

View file

@ -41,11 +41,15 @@ sub generate-head(BlogMeta:D $meta, $title?, $description?) is export {
:href</resources/code.css>;
link :rel<stylesheet>,
:href</resources/admonitions.css>;
# Verify mastodon
link :rel<me>, :href<https://hachyderm.io/@thatonelutenist>;
# Atribute on mastodon
meta :name<fediverse:creator>, :content<@thatonelutenist@hachyderm.io>;
# Tell dark reader that we'll behave
meta :name<color-scheme>, :content<light dark>;
# Tell feed readers about our feed
link :rel<alternate>, :type<application/atom+xml>, :title($meta.title),
:href</feed.xml>;
:href</atom.xml>;
]
}
@ -71,6 +75,6 @@ sub site-header(BlogMeta:D $meta) is export {
header-link 'Series', '/series.html', 'book';
header-link 'About', '/about.html', 'info-circle';
header-link 'Feed', '/atom.xml', 'rss';
];
], $meta.header-links.map(-> $link {header-link $link.text, $link.link, $link.icon});
]
}

View file

@ -3,6 +3,7 @@ unit module Render::Series;
use Render::Util;
use Render::Head;
use Render::Foot;
use Render::Post;
use DB::Post;
use DB::Series;
@ -93,7 +94,8 @@ sub series-page(Int:D $series-id, $db) is export {
series-header $series, $db;
div :class<series-blurbs>,
$series.post-ids.map(*.&generate-blurb($db));
]
];
generate-footer;
];
my $html = html :lang<en>, [
@ -133,6 +135,7 @@ sub series-list-page($db) is export {
div :class<series-list>, [
h1 "All Series"
], @series-blurbs;
generate-footer;
];
my $html = html :lang<en>, [

View file

@ -49,10 +49,18 @@ sub icon($icon) is export {
i(:class("bx bx-$icon"))
}
sub icon-solid($icon) is export {
i(:class("bx bxs-$icon"))
}
sub logo($logo) is export {
i(:class("bx bxl-$logo"))
}
sub simple-icon($icon) is export {
img :src("https://cdn.simpleicons.org/$icon/474747/b9b9b9")
}
sub mins-to-string($mins) is export {
if $mins < 60 {
$mins.Str ~ "m"

View file

@ -19,6 +19,7 @@ authors = "Nathan McCarty"
modules = Idris
, Posts.HelloWorld
, LessMacrosMoreTypes.Printf
, DependentNuggets.FreshLists
-- main file (i.e. file to load at REPL)
-- main =

View file

@ -0,0 +1,8 @@
# FreshLists: Containers That Only Accept "Fresh" Elements
```idris hide
module DependentNuggets.FreshLists
```
When programming, we quite frequently encounter the for a data structure that can contain at most one of each given element. Typically, we would use a `Set`, which satisfies this constraint, but does so as a runtime invariant that must be taken on trust, and results in ergonomic concerns when used as a component of API design.

View file

@ -5,13 +5,16 @@ module LessMacrosMoreTypes.Printf
import Data.List
import System
-- Only needed for addenda
import Data.Vect
```
While C can provide "convenient" string formatting by having hideously unsafe variadics, and dynamic languages, like python, can do the same, many type safe languages, such as Rust, are forced to provide such functionality through the use of a macro. Dependently typed languages, like Idris, can provide a `printf` like formatting interface, while maintaining both memory and type safety, without the need for macros. We will explore this by implementing a simplified version of `printf` in Idris from scratch.
This article is inspired by an exercise from chapter 6 of [Type Driven Development with Idris](https://www.manning.com/books/type-driven-development-with-idris), and is written as a literate Idris file, with the source available [here](https://git.stranger.systems/thatonelutenist/website/src/branch/trunk/projects/Idris/src/LessMacrosMoreTypes/Printf.md).
## Gameplan
## Game plan
Our goal is to provide a `printf` function that can be called much like it's C equivalent:
@ -29,7 +32,7 @@ Idris lacks a dedicated facility for variadics, but we can call functions in typ
Idris allows us to manipulate types as first class values, and we can use the runtime values of previous arguments to the function we are declaring as arguments to later type-level functions. These type-level functions can, furthermore, recurse into themselves to produce type signatures of varying length and content.
> [!TIP]
> Idris requires that these type level functions be [_total_](https://idris2.readthedocs.io/en/latest/tutorial/theorems.html#totality-checking) for them to be expanded at compile time, meaning they are known to the compiler to return a value in constant time for all possible inputs.
> Idris requires that these type level functions be [_total_](https://idris2.readthedocs.io/en/latest/tutorial/theorems.html#totality-checking) for them to be expanded at compile time, meaning they are known to the compiler to return a value in finite time for all possible inputs.
This allows us to produce variadic functions, so long as the number of arguments and their types can be determined from one of the arguments of the function before the variadic portion.
@ -43,7 +46,7 @@ First, we need a data structure to describe our format string. We define the `Fo
data Format : Type where
||| A slot that should be filled in with a number
Number : (next : Format) -> Format
||| A slot that should be filled in with a number, padded to a certian number
||| A slot that should be filled in with a number, padded to a certain number
||| of digits
PaddedNumber : (digits : Nat) -> (next : Format) -> Format
||| A slot that should be filled in with a string
@ -188,7 +191,7 @@ printfFmt (Just x) acc = printfFmt' x acc
### With a Format String
`printf` is easily defined a simple wrapper, converting the provided string to a `Format` and passing it to `printfFmt`. After accepting the format string argument, we parse a `Maybe Format` from it, providing the parsed value to our `PrintfType` function to calculate the rest of our type signature.
`printf` is easily defined as a simple wrapper, converting the provided string to a `Format` and passing it to `printfFmt`. After accepting the format string argument, we parse a `Maybe Format` from it, providing the parsed value to our `PrintfType` function to calculate the rest of our type signature.
> [!TIP]
> The `_` syntax that appears in this function introduces an anonymous hole. When the value of a hole is forced by type checking (resolved), the compiler fills in its value for you. Holes defined with `_` will result in an "unsolved holes" compile error if unresolved.
@ -260,7 +263,7 @@ failing
runtimeFormatString = printf (blackbox "%s %s") "Hello" "World"
```
We can construct a variant of `printf` that will work with runtime format strings by defining a custom, heterogenous list-like structure, and making a fallible version of our `printf` function based on this data structure:
We can construct a variant of `printf` that will work with runtime format strings by defining a custom, heterogeneous list-like structure, and making a fallible version of our `printf` function based on this data structure:
```idris
data PrintfInput : Type where
@ -328,6 +331,9 @@ We've made a fun little function that works much like C's `printf` function or R
Avoiding using a macro also gives us a lot of neat bonuses, like `printf` automatically playing nice with partial application and currying, with no extra effort on our part.
> [!NOTE]
> The `the (List _) x` construction here in explicit type annotation, needed because of an ambiguity in list-type literals introduced by importing `Data.Vect` for the addenda
```idris hide
-- @@test partial application
partialApplication : IO Bool
@ -336,7 +342,7 @@ partialApplication = do
```
<div class="unit-test">
```idris
map (printf "Hello %s") ["Alice", "Bob"] == ["Hello Alice", "Hello Bob"]
map (printf "Hello %s") (the (List _) ["Alice", "Bob"]) == ["Hello Alice", "Hello Bob"]
```
</div>
@ -393,5 +399,47 @@ The need for a bespoke data structure for `PrintfInput` also isn't ideal.
We will delve in ways to rectify all of these complaints in future entries in this series.
## Addenda
### Selecting from a list of format strings with matching type signatures
As long as the format strings are known at compile time, `PrintfType` will fully expand to a function type signature that contains no references to the string. For example, `PrintfType (parseFormat (unpack "%s"))` expands to `String -> String`.
We can take advantage of this by indexing a container type by the `PrintfType` of a "template" format string, and then our container can hold any formatting function of compatible type signature, like so:
```idris
formatChoices : Vect 3 (PrintfType (parseFormat (unpack "%s")))
formatChoices = [printf "Hello %s!", printf "Nice to meet you %s", printf "\"%s\""]
```
We can then index out a single format function from this container (it doesn't have to be a `Vect`, it could also be a map indexed by the format string itself, for instance):
```idris hide
-- @@test formatChoicesOne
formatChoicesOne : IO Bool
formatChoicesOne = do
pure $
```
<div class="unit-test">
```idris
let f = index 1 formatChoices in f "Alice" == "Nice to meet you Alice"
```
</div>
Or apply all of the contained formatting functions to the same string:
```idris hide
-- @@test formatChoicesAll
formatChoicesAll : IO Bool
formatChoicesAll = do
pure $
```
<div class="unit-test">
```idris
(formatChoices <*> replicate _ "World")
== ["Hello World!", "Nice to meet you World", "\"World\""]
```
</div>
[^1]: Ignoring for a second the special handling for format strings in modern compilers

View file

@ -1,5 +1,7 @@
# About Me
## About Me
My name is Nathan, I'm an engineering psychologist by training, and a systems
engineer by trade.

View file

@ -0,0 +1,11 @@
# Hire Me
## Hire Me!
I am available for consulting gigs, if your company is new to using Rust, or interested in introducing Rust is into your tech stack, I can train your team on Rust, provide code review, and help with introducing Rust to your existing code base.
I am also currently looking for a full time role. Professionally, I am most comfortable working in Rust in backend or systems programming roles, I have historically specialized in tooling and consensus protocols, but also have experience working in embedded and with ETL pipelines. I'm also comfortable working with Go, C#, and most functional programming languages like F# or Haskell.
A PDF copy of my CV can be found [here](https://static.stranger.systems/cv-no-phone.pdf), more information about me can be found on my [About](/about.html) page.
If you are interested in hiring me for a full time position, or retaining me for a consulting role, please reach out to me at <inquiries@stranger.systems>. My consulting rate varies depending on the scope of the work and the nature of your company, if you are worried about affordability, please do still reach out to me, I'm sure we can work something out.

360
resources/cactus.css Normal file
View file

@ -0,0 +1,360 @@
:root{
--cactus-text-color--soft: var(--dim-0);
--cactus-background-color--strong: var(--bg-1);
--cactus-border-color: var(--dim-0);
--cactus-box-shadow-color: transparent;
--cactus-button-text-color:inherit;
--cactus-button-color: var(--bg-2);
--cactus-button-color--strong: var(--bg-2);
--cactus-button-color--stronger: var(--bg-2);
--cactus-login-form-text-color:inherit;
--cactus-border-width:1px;
--cactus-border-radius:0.4em;
--cactus-text-color:inherit;
--cactus-background-color: var(--bg-0);
--cactus-error-color: var(--red)
}
.cactus-comment-header a {
color: var(--blue);
}
.cactus-container{
display:flex;
width: 100%;
max-width: var(--content-width);
padding: var(--box-padding-vert) var(--box-padding-horz);
border-radius: var(--box-radius);
flex-direction:column;
gap:1em;
color:var(--cactus-text-color);
background-color:var(--cactus-background-color)
}
.cactus-error{
padding:.5em;
padding-inline-end:1.5em;
border:var(--cactus-border-width) solid var(--cactus-error-color);
border-radius:var(--cactus-border-radius);
position:relative
}
.cactus-error-close{
position:absolute;
right:.2em;
top:0;
color:var(--cactus-button-color);
background-color:transparent;
border:none
}
.cactus-error-close:hover:not([disabled]){
color:var(--cactus-button-color--strong);
cursor:pointer
}
.cactus-error-close:active:not([disabled]){
color:var(--cactus-button-color--stronger);
cursor:pointer
}
.cactus-error-close-icon{
inline-size:20px;
block-size:20px
}
.cactus-error-text{
color:var(--cactus-error-color);
font-weight:700;
margin:0
}
.cactus-editor{
display:flex;
flex-direction:column;
gap:.5em
}
.cactus-editor>span{
display:flex
}
.cactus-editor-textarea{
display:flex;
flex:1;
height:9rem;
border-radius:var(--cactus-border-radius);
border:solid var(--cactus-border-width) var(--green);
padding:.5em;
box-sizing:content-box;
background-color:transparent;
color:inherit;
font:inherit
}
.cactus-editor-textarea::placeholder{
text-align:center;
line-height:8rem;
font-size:1.5rem;
color:var(--cactus-text-color--soft)
}
.cactus-editor-below{
display:flex;
flex-wrap:wrap;
gap:.5em;
justify-content:flex-end
}
.cactus-editor-name{
display:flex;
flex:1
}
.cactus-editor-name>span{
display:flex;
flex:1;
min-inline-size:20ch;
max-inline-size:40ch
}
.cactus-editor-name>span>input{
inline-size:100%;
border-radius:var(--cactus-border-radius);
border:solid var(--cactus-border-width) var(--green);
padding:.5em;
background-color:transparent;
color:inherit;
font-size:inherit
}
.cactus-editor-name>span>input::placeholder{
color:var(--cactus-text-color--soft)
}
.cactus-editor-buttons{
display:flex;
gap:.5em
}
.cactus-matrixdotto-only{
align-self:center;
text-decoration:none
}
.cactus-login-form-wrapper{
position:fixed;
top:0;
bottom:0;
left:0;
right:0;
z-index:1;
display:flex;
align-items:center;
justify-content:center
}
.cactus-login-form{
display:flex;
flex-direction:column;
gap:2em;
padding:2rem;
border-radius:var(--cactus-border-radius);
background-color:var(--cactus-background-color--strong);
color:var(--cactus-login-form-text-color);
box-shadow:0 .5em 1em .5em var(--cactus-box-shadow-color);
box-sizing:border-box;
inline-size:100%;
max-inline-size:300px
}
.cactus-login-close{
align-self:flex-end;
position:relative;
margin:-2em;
padding:0;
color:var(--cactus-button-color);
scale:2;
background-color:transparent;
border:none
}
.cactus-login-close:hover:not([disabled]){
color:var(--cactus-button-color--strong);
cursor:pointer
}
.cactus-login-close:active:not([disabled]){
color:var(--cactus-button-color--stronger);
cursor:pointer
}
.cactus-login-close-icon{
inline-size:20px;
block-size:20px
}
.cactus-login-title{
align-self:center;
font-size:1.17em;
font-weight:700;
margin:0
}
.cactus-login-client{
display:flex;
flex-direction:column;
gap:1em
}
.cactus-login-client-title{
font-size:1em;
font-weight:700;
margin:0
}
.cactus-matrixdotto-button{
justify-content:center;
text-decoration:none
}
.cactus-login-credentials{
display:flex;
flex-direction:column;
gap:1em
}
.cactus-login-credentials-title{
font-size:1em;
font-weight:700;
margin:0
}
.cactus-login-field{
display:flex;
flex-direction:column;
gap:.25em
}
.cactus-login-label{
font-size:1em;
padding-bottom:.25em;
color:var(--cactus-text-color--soft)
}
.cactus-login-error{
margin:0;
font-size:.8em;
color:var(--cactus-text-color--soft)
}
.cactus-login-field>input{
border-radius:var(--cactus-border-radius);
border:solid var(--cactus-border-width) var(--cactus-border-color);
padding:.5em;
background-color:transparent;
color:inherit;
font-size:inherit
}
.cactus-login-field>input::placeholder{
color:var(--cactus-text-color--soft)
}
.cactus-login-credentials-button{
justify-content:center
}
.cactus-comments-container{
display:flex;
flex-direction:column;
gap:1em
}
.cactus-comments-list{
display:flex;
flex-direction:column;
gap:.5em
}
.cactus-comment{
display:flex;
flex-direction:row;
gap:1em;
padding-block-end:.5em;
border-block-end: 2px dotted var(--dim-0);
}
.cactus-comment-avatar{
display:flex
}
.cactus-comment-avatar>*{
width:2.5rem;
height:2.5rem;
border-radius:50%;
margin:0
}
.cactus-comment-avatar-placeholder{
display:flex;
justify-content:center;
align-items:center;
background-color:var(--cactus-border-color);
color:var(--bg-2);
}
.cactus-comment-avatar-placeholder:before{
content:"?"
}
.cactus-comment-content{
display:flex;
flex-direction:column;
gap:.5em
}
.cactus-comment-header{
display:flex;
gap:.5em;
flex-wrap:wrap
}
.cactus-comment-displayname{
font-weight:700;
cursor:pointer;
text-decoration:none;
color:inherit
}
.cactus-comment-time{
color:var(--cactus-text-color--soft)
}
.cactus-message-text>:first-child{
margin-block-start:0
}
.cactus-message-text>:last-child{
margin-block-end:0
}
.cactus-message-emote{
padding-top:.5em;
color:var(--cactus-text-color--soft)
}
.cactus-message-image{
max-width:100%;
height:auto
}
.cactus-message-file{
line-height:3em;
margin-left:1em
}
.cactus-message-video{
max-width:100%
}
.cactus-button{
display:flex;
align-items:center;
padding-block:.6em;
padding-inline:1em;
background-color:var(--cactus-button-color);
font-weight:700;
border-radius:var(--cactus-border-radius);
color:var(--cactus-button-text-color);
font-size:inherit;
border:none
}
.cactus-button:hover:not([disabled]){
background-color:var(--cactus-button-color--strong);
cursor:pointer
}
.cactus-button:active:not([disabled]){
background-color:var(--cactus-button-color--stronger);
cursor:pointer
}
.cactus-view-more{
display:flex;
justify-content:center
}
.spinner{
align-self:center;
width:2em;
height:2em;
display:flex;
gap:.3em
}
.spinner>div{
flex:1;
background-color:var(--cactus-border-color);
animation:sk-stretchdelay 2.4s ease-in-out infinite
}
.spinner .rect2{
animation-delay:-2.2s
}
.spinner .rect3{
animation-delay:-2s
}
.spinner .rect4{
animation-delay:-1.8s
}
@keyframes sk-stretchdelay{
0%,40%,to{
transform:scaleY(.4)
}
20%{
transform:scaleY(1)
}
}
/*# sourceMappingURL=/style.css.map */

View file

@ -69,9 +69,20 @@ a:visited {
.post-title, .post-blurbs h1, .series-header h1, .series-list h1 {
color: var(--green);
}
.post-body h2, .post-body h3, .post-body h4 {
color: var(--fg-1);
.post-body h2 {
color: var(--red);
background-color: var(--bg-1);
}
.post-body h3 {
color: var(--blue);
}
.post-body h4 {
color: var(--violet);
}
.post-info > *, .series-info > *, .header-links > * {
background-color: var(--bg-2);
}
@ -91,6 +102,23 @@ blockquote {
.unit-test .bx-info-circle {
color: var(--yellow);
}
footer {
background-color: var(--bg-0);
}
footer > div {
background-color: var(--bg-1);
}
.footer-link {
background-color: var(--bg-2);
}
/* Formatting for special pages */
.special-post h2 {
color: var(--green);
}
.special-post h3 {
color: var(--red);
}
/* Colorization for idris code blocks */
code {

View file

@ -8,6 +8,7 @@
--box-padding-horz: 1rem;
--box-margin-vert: 0.5rem;
--box-margin-horz: 0.5rem;
--footer-padding: 0.25rem;
--box-gap: 0.5rem;
--box-radius: 1rem;
}
@ -78,11 +79,18 @@ body, .post, .series {
font-size: 1.1rem;
flex-wrap: wrap;
margin-top: var(--box-margin-vert);
justify-content: center;
}
.header-links > a > span, .post-series-tag > a > span, .post-tag > a > span {
.header-links > a > span,
.post-series-tag > a > span,
.post-tag > a > span,
.footer-link > a > div {
text-decoration: underline;
}
.header-links > a, .post-series-tag > a, .post-tag > a {
.header-links > a,
.post-series-tag > a,
.post-tag > a,
.footer-link > a {
text-decoration: none;
}
@ -121,6 +129,13 @@ body, .post, .series {
.post-body h2, .post-body h3, .post-body h4 {
text-align: center;
}
.post-body h2 {
padding: var(--box-margin-vert) var(--box-margin-horz);
border-radius: var(--box-radius);
width: 100%;
}
.post-blurbs, .series-blurbs, .series-list {
display: flex;
flex-direction: column;
@ -130,7 +145,7 @@ body, .post, .series {
padding: var(--box-padding-vert) var(--box-padding-horz);
border-radius: var(--box-radius);
}
.post-blurb, .series-list-blurb {
.post-blurb, .series-list-blurb, .series-list-blurb-title {
width: 100%;
display: block;
border-radius: var(--box-radius);
@ -174,7 +189,6 @@ blockquote {
.tag-blurb-links {
display: block;
border-radius: var(--box-radius);
border-radius: var(--box-radius);
display: flex;
flex-flow: row wrap;
gap: var(--box-gap);
@ -197,6 +211,69 @@ blockquote {
.tag-blurb-title {
margin-top: var(--box-margin-vert);
margin-bottom: 0;
font-size: 1.5em;
font-size: 1.5rem;
font-weight: bold;
}
/* Style the footer */
footer {
display: flex;
flex-flow: row wrap;
align-items: stretch;
gap: var(--box-gap);
max-width: var(--content-width);
width: 100%;
border-radius: var(--box-radius);
padding: var(--box-padding-vert) var(--box-padding-horz);
font-size: 0.8rem;
}
footer > div {
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-around;
flex: 1;
gap: var(--box-gap);
border-radius: var(--box-radius);
padding: var(--footer-padding);
}
.footer-title {
font-size: 1rem;
font-weight: bold;
}
footer i {
font-size: 1rem;
line-height: 0.8rem;
}
.footer-link > a {
margin-left: 0.25rem;
}
.footer-link > img {
height: 1rem;
width: 1rem;
margin: 0.1rem;
}
.footer-links {
display: flex;
flex-flow: row wrap;
align-items: center;
justify-content: center;
gap: var(--box-gap);
border-radius: var(--box-radius);
padding: var(--footer-padding);
}
.footer-link {
display: flex;
flex-flow: row nowrap;
align-content: center;
border-radius: 0.25rem;
padding: var(--footer-padding);
}
/* Formatting for special pages */
.special-post h2 {
font-size: 2rem;
}
.special-post h3 {
font-size: 1.5rem;
}