this repo has no description

Compare changes

Choose any two refs to compare.

Changed files
+5127 -216
.github
workflows
content
sass
static
styles
templates
themes
+16
.github/workflows/rebuild.yml
···
···
+
name: Trigger Netlify rebuild
+
on:
+
schedule:
+
# Run every 8h
+
- cron: '0 */8 * * *'
+
+
jobs:
+
trigger:
+
name: Send trigger
+
runs-on: ubuntu-latest
+
env:
+
BUILD_HOOK_URL: 'https://api.netlify.com/build_hooks/629f4bdea589057c66219efd'
+
steps:
+
- name: Run cURL request
+
run: |
+
curl -X POST -d '{}' "$BUILD_HOOK_URL"
+3
.gitignore
···
node_modules/
/public
···
node_modules/
/public
+
+
# Local Netlify folder
+
.netlify
-3
.gitmodules
···
-
[submodule "themes/zerm"]
-
path = themes/zerm
-
url = https://github.com/ejmg/zerm.git
···
+2
.mdlrc
···
style 'styles/markdown.rb'
···
style 'styles/markdown.rb'
+
+
# vi: ft=ruby
+2
.vale.ini
···
[*.md]
BasedOnStyles = proselint, write-good
···
[*.md]
BasedOnStyles = proselint, write-good
+
write-good.Passive = NO
+
write-good.TooWordy = NO
+25 -8
config.toml
···
-
base_url = "/"
default_language = "en"
title = "Hauleth's blog"
···
theme = "zerm"
-
generate_feed = true
taxonomies = [
-
{name = "tags"},
-
{name = "categories"},
]
[markdown]
highlight_code = true
-
highlight_theme = "nord"
[extra]
author = "Hauleth"
for_hire = true
theme_color = "blue"
logo_text = "~hauleth"
logo_home_link = "/"
···
copyright = "copyright by <a href=\"https://hauleth.dev\" rel=me>hauleth</a>"
# Menu items to display. You define a url and the name of the menu item.
-
# NOTE: `$BASE_URL/` must be included in the url name.
main_menu = [
{url="https://plan.cat/~hauleth", name=".plan", rel="me"},
-
{url="https://twitter.com/hauleth", name="twitter", rel="me"},
{url="https://github.com/hauleth", name="github", rel="me"},
{url="https://gitlab.com/hauleth", name="gitlab", rel="me"},
]
···
# feel like adding it.
read_other_posts = "read other posts"
-
favicon = "/favicon-32x32.png"
···
+
base_url = "https://hauleth.dev/"
default_language = "en"
title = "Hauleth's blog"
···
theme = "zerm"
+
generate_feeds = true
+
minify_html = true
taxonomies = [
+
{name = "tags", feed = true},
]
[markdown]
highlight_code = true
+
highlight_theme = "css"
+
# TODO: Write custom theme that will fit the overall colouring better
+
highlight_themes_css = [
+
{ theme = "nord", filename = "syntax-theme.css" },
+
]
[extra]
author = "Hauleth"
for_hire = true
theme_color = "blue"
+
+
source = "https://tangled.sh/hauleth.dev/blog"
logo_text = "~hauleth"
logo_home_link = "/"
···
copyright = "copyright by <a href=\"https://hauleth.dev\" rel=me>hauleth</a>"
# Menu items to display. You define a url and the name of the menu item.
main_menu = [
{url="https://plan.cat/~hauleth", name=".plan", rel="me"},
+
{url="https://fosstodon.org/@hauleth", name="toots", rel="me"},
+
{url="https://sr.ht/~hauleth", name="sourcehut", rel="me"},
{url="https://github.com/hauleth", name="github", rel="me"},
{url="https://gitlab.com/hauleth", name="gitlab", rel="me"},
]
···
# feel like adding it.
read_other_posts = "read other posts"
+
favicon = "favicon-32x32.png"
+
og_preview_img = "banner.png"
+
+
webmention = "https://webmention.io/hauleth.dev/webmention"
+
+
[extra.twitter]
+
site = "@hauleth"
+
creator = "@hauleth"
+
+
[[extra.webrings]]
+
name = "Beambloggers"
+
url = "https://beambloggers.com/"
+9
content/cv/_index.md
···
···
+
+++
+
title = "CV"
+
+
[extra]
+
no_comments = true
+
sitemap = false
+
+++
+
+
{{ cv() }}
-8
content/cv/index.md
···
-
+++
-
title = "CV"
-
-
[extra]
-
no_comments = true
-
+++
-
-
{{ cv() }}
···
+472
content/post/beam-process-memory-usage.md
···
···
+
+++
+
date = 2023-06-10
+
title = "How much memory is needed to run 1M Erlang processes?"
+
description = "How to not write benchmarks"
+
+
[taxonomies]
+
tags = [
+
"beam",
+
"performance"
+
]
+
+++
+
+
Recently [benchmark for concurrency implementation in different
+
languages][benchmark]. In this article [Piotr Kołaczkowski][] used Chat GPT to
+
generate the examples in the different languages and benchmarked them. This was
+
poor choice as I have found this article and read the Elixir example:
+
+
[benchmark]: https://pkolaczk.github.io/memory-consumption-of-async/ "How Much Memory Do You Need to Run 1 Million Concurrent Tasks?"
+
[Piotr Kołaczkowski]: https://github.com/pkolaczk
+
+
```elixir
+
tasks =
+
for _ <- 1..num_tasks do
+
Task.async(fn ->
+
:timer.sleep(10000)
+
end)
+
end
+
+
Task.await_many(tasks, :infinity)
+
```
+
+
And, well, it's pretty poor example of BEAM's process memory usage, and I am
+
not talking about the fact that it uses 4 spaces for indentation.
+
+
For 1 million processes this code reported 3.94 GiB of memory used by the process
+
in Piotr's benchmark, but with little work I managed to reduce it about 4 times
+
to around 0.93 GiB of RAM usage. In this article I will describe:
+
+
- how I did that
+
- why the original code was consuming so much memory
+
- why in the real world you probably should not optimise like I did here
+
- why using ChatGPT to write benchmarking code sucks (TL;DR because that will
+
nerd snipe people like me)
+
+
## What are Erlang processes?
+
+
Erlang is ~~well~~ known of being language which support for concurrency is
+
superb, and Erlang processes are the main reason for that. But what are these?
+
+
In Erlang *process* is the common name for what other languages call *virtual
+
threads* or *green threads*, but in Erlang these have small neat twist - each of
+
the process is isolated from the rest and these processes can communicate only
+
via message passing. That gives Erlang processes 2 features that are rarely
+
spotted in other implementations:
+
+
- Failure isolation - bug, unhandled case, or other issue in single process will
+
not directly affect any other process in the system. VM can send some messages
+
due to process shutdown, and other processes may be killed because of that,
+
but by itself shutting down single process will not cause problems in any
+
process not related to that.
+
- Location transparency - process can be spawned locally or on different
+
machine, but from the viewpoint of the programmer, there is no difference.
+
+
The above features and requirements results in some design choices, but for our
+
purpose only one is truly needed today - each process have separate and (almost)
+
independent memory stack from any other process.
+
+
### Process dictionary
+
+
Each process in Erlang VM has dedicated *mutable* memory space for their
+
internal uses. Most people do not use it for anything because in general it
+
should not be used unless you know exactly what you are doing (in my case, a bad
+
carpenter could count cases when I needed it, on single hand). In general it's
+
*here be dragons* area.
+
+
How it's relevant to us?
+
+
Well, OTP internally uses process dictionary (`pdict` for short) to store
+
metadata about given process that can be later used for debugging purposes. Some
+
data that it store are:
+
+
- Initial function that was run by the given process
+
- PIDs to all ancestors of the given process
+
+
Different processes abstractions (like `gen_server`/`GenServer`, Elixir's
+
`Task`, etc.) can store even more metadata there, `logger` store process
+
metadata in process dictionary, `rand` store state of the PRNGs in the process
+
dictionary. it's used quite extensively by some OTP features.
+
+
### "Well behaved" OTP process
+
+
In addition to the above metadata if the process is meant to be "well behaved"
+
process in OTP system, i.e. process that can be observed and debugged using OTP
+
facilities, it must respond to some additional messages defined by [`sys`][]
+
module. Without that the features like [`observer`][] would not be able to "see"
+
the content of the process state.
+
+
[`sys`]: https://erlang.org/doc/man/sys.html
+
[`observer`]: https://erlang.org/doc/man/observer.html
+
+
## Process memory usage
+
+
As we have seen above, the `Task.async/1` function form Elixir **must** do
+
much more than just simple "start process and live with it". That was one of the
+
most important problems with the original process, it was using system, that was
+
allocating quite substantial memory alongside of the process itself, just to
+
operate this process. In general, that would be desirable approach (as you
+
**really, really, want the debugging facilities**), but in synthetic benchmarks,
+
it reduce the feasibility of such benchmark.
+
+
If we want to avoid that additional memory overhead in our spawned processes we
+
need to go back to more primitive functions in Erlang, namely `erlang:spawn/1`
+
(`Kernel.spawn/1` in Elixir). But that mean that we cannot use
+
`Task.await_many/2` anymore, so we need to workaround it by using custom
+
function:
+
+
```elixir
+
defmodule Bench do
+
def await(pid) when is_pid(pid) do
+
# Monitor is internal feature of Erlang that will inform you (by sending
+
# message) when process you monitor die. The returned value is type called
+
# "reference" which is just simply unique value returned by the VM.
+
# If the process is already dead, then message will be delivered
+
# immediately.
+
ref = Process.monitor(pid)
+
+
receive do
+
{:DOWN, ^ref, :process, _, _} -> :ok
+
end
+
end
+
+
def await_many(pids) do
+
Enum.each(pids, &await/1)
+
end
+
end
+
+
tasks =
+
for _ <- 1..num_tasks do
+
# `Kernel` module is imported by default, so no need for `Kernel.` prefix
+
spawn(fn ->
+
:timer.sleep(10000)
+
end)
+
end
+
+
Bench.await_many(tasks)
+
```
+
+
We already removed one problem (well, two in fact, but we will go into
+
details in next section).
+
+
## All your lists are belongs to us now
+
+
Erlang, like most of the functional programming languages, have 2 built-in
+
sequence types:
+
+
- Tuples - which are non-growable product type of the values, so you can access
+
any field quite fast, but adding more values is performance no-no
+
- (Singly) linked lists - growable type (in most case it will have single type
+
values in it, but in Erlang that is not always the case), which is fast to
+
prepend or pop data from the beginning, but do not try to do anything else if
+
you care about performance.
+
+
In this case we will focus on the 2nd one, as there tuples aren't important at
+
all.
+
+
Singly linked list is simple data structure. It's either special value `[]`
+
(an empty list) or it's something called "cons-cell". Cons-cells are also
+
simple structures - it's 2ary tuple (tuple with 2 elements) where first value
+
is head - the value in the list cell, and another one is the "tail" of the list (aka
+
rest of the list). In Elixir the cons-cell is denoted like that `[head | tail]`.
+
Super simple structure as you can see, and perfect for the functional
+
programming as you can add new values to the list without modifying existing
+
values, so you can be immutable and fast. However if you need to construct the
+
sequence of a lot of values (like our list of all tasks) then we have problem.
+
Because Elixir promises that list returned from the `for` will be **in-order**
+
of the values passed to it. That mean that we either need to process our data
+
like that:
+
+
```elixir
+
def map([], _), do: []
+
+
def map([head | tail], func) do
+
[func.(head) | map(tail, func)]
+
end
+
```
+
+
Where we build call stack (as we cannot have tail call optimisation there, of
+
course sans compiler optimisations). Or we need to build our list in reverse
+
order, and then reverse it before returning (so we can have TCO):
+
+
```elixir
+
def map(list, func), do: do_map(list, func, [])
+
+
def map([], _func, agg), do: :lists.reverse(agg)
+
+
def map([head | tail], func, agg) do
+
map(tail, func, [func.(head) | agg])
+
end
+
```
+
+
Which one of these approaches is more performant is irrelevant[^erlang-perf],
+
what is relevant is that we need either build call stack or construct our list
+
*twice* to be able to conform to the Elixir promises (even if in this case we do
+
not care about order of the list returned by the `for`).
+
+
[^erlang-perf]: Sometimes body recursion will be faster, sometimes TCO will be
+
faster. it's impossible to tell without more benchmarking. For more info check
+
out [superb article by Ferd Herbert](https://ferd.ca/erlang-s-tail-recursion-is-not-a-silver-bullet.html).
+
+
Of course we could mitigate our problem by using `Enum.reduce/3` function (or
+
writing it on our own) and end with code like:
+
+
```elixir
+
defmodule Bench do
+
def await(pid) when is_pid(pid) do
+
ref = Process.monitor(pid)
+
+
receive do
+
{:DOWN, ^ref, :process, _, _} -> :ok
+
end
+
end
+
+
def await_many(pids) do
+
Enum.each(pids, &await/1)
+
end
+
end
+
+
tasks =
+
Enum.reduce(1..num_tasks, [], fn _, agg ->
+
# `Kernel` module is imported by default, so no need for `Kernel.` prefix
+
pid =
+
spawn(fn -> :timer.sleep(10000) end)
+
+
[pid | agg]
+
end)
+
+
Bench.await_many(tasks)
+
```
+
+
Even then we build list of all PIDs.
+
+
Here I can also go back to the "second problem* I have mentioned above.
+
`Task.await_many/1` *also construct a list*. it's list of return value from all
+
the processes in the list, so not only we constructed list for the tasks' PIDs,
+
we also constructed list of return values (which will be `:ok` for all processes
+
as it's what `:timer.sleep/1` returns), and immediately discarded all of that.
+
+
How we can better? See that **all** we care is that all `num_task` processes
+
have gone down. We do not care about any of the return values, all what we want
+
is to know that all processes that we started went down. For that we can just
+
send messages from the spawned processes and count the received messages count:
+
+
```elixir
+
defmodule Bench do
+
def worker(parent) do
+
:timer.sleep(10000)
+
send(parent, :done)
+
end
+
+
def start(0), do: :ok
+
def start(n) when n > 0 do
+
this = self()
+
spawn(fn -> worker(this) end)
+
+
start(n - 1)
+
end
+
+
def await(0), do: :ok
+
def await(n) when n > 0 do
+
receive do
+
:done -> await(n - 1)
+
end
+
end
+
end
+
+
Bench.start(num_tasks)
+
Bench.await(num_tasks)
+
```
+
+
Now we do not have any lists involved and we still do what the original task
+
meant to do - spawn `num_tasks` processes and wait till all go down.
+
+
## Arguments copying
+
+
One another thing that we can account there - lambda context and data passing
+
between processes.
+
+
You see, we need to pass `this` (which is PID of the parent) to our newly
+
spawned process. That is suboptimal, as we are looking for the way to reduce
+
amount of the memory (and ignore all other metrics at the same time). As Erlang
+
processes are meant to be "share nothing" type of processes there is problem -
+
we need to copy that PID to all processes. it's just 1 word (which mean 8 bytes
+
on 64-bit architectures, 4 bytes on 32-bit), but hey, we are microbenchmarking,
+
so we cut whatever we can (with 1M processes, this adds up to 8 MiBs).
+
+
Hey, we can avoid that by using yet another feature of Erlang, called
+
*registry*. This is yet another simple feature that allows us to assign PID of
+
the process to the atom, which allows us then to send messages to that process
+
using just name, we have given. While atoms are also 1 word that wouldn't make
+
sense to send it as well, but instead we can do what any reasonable
+
microbenchmarker would do - *hardcode stuff*:
+
+
```elixir
+
defmodule Bench do
+
def worker do
+
:timer.sleep(10000)
+
send(:parent, :done)
+
end
+
+
def start(0), do: :ok
+
def start(n) when n > 0 do
+
spawn(fn -> worker() end)
+
+
start(n - 1)
+
end
+
+
def await(0), do: :ok
+
def await(n) when n > 0 do
+
receive do
+
:done -> await(n - 1)
+
end
+
end
+
end
+
+
Process.register(self(), :parent)
+
+
Bench.start(num_tasks)
+
Bench.await(num_tasks)
+
```
+
+
Now we do not pass any arguments, and instead rely on the registry to dispatch
+
our messages to respective processes.
+
+
## One more thing
+
+
As you may have already noticed we are passing lambda to the `spawn/1`. That is
+
also quite suboptimal, because of [difference between remote and local call][remote-vs-local].
+
This mean that we are paying slight memory cost for these processes to keep the
+
old module in memory. Instead we can use either fully qualified function capture
+
or `spawn/3` function that accepts MFA (module, function name, arguments list)
+
argument. We end with:
+
+
[remote-vs-local]: https://www.erlang.org/doc/reference_manual/code_loading.html#code-replacement
+
+
```elixir
+
defmodule Bench do
+
def worker do
+
:timer.sleep(10000)
+
send(:parent, :done)
+
end
+
+
def start(0), do: :ok
+
def start(n) when n > 0 do
+
spawn(&__MODULE__.worker/0)
+
+
start(n - 1)
+
end
+
+
def await(0), do: :ok
+
def await(n) when n > 0 do
+
receive do
+
:done -> await(n - 1)
+
end
+
end
+
end
+
+
Process.register(self(), :parent)
+
+
Bench.start(num_tasks)
+
Bench.await(num_tasks)
+
```
+
+
## Results
+
+
With given Erlang compilation:
+
+
```txt
+
Erlang/OTP 25 [erts-13.2.2.1] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1]
+
+
Elixir 1.14.5 (compiled with Erlang/OTP 25)
+
```
+
+
> Note no JIT as Nix on macOS currently[^currently] disable it and I didn't bother to enable
+
> it in the derivation (it was disabled because there were some issues, but IIRC
+
> these are resolved now).
+
+
[^currently]: Nixpkgs rev `bc3ec5ea`
+
+
The results are as follow (in bytes of peak memory footprint returned by
+
`/usr/bin/time` on macOS):
+
+
| Implementation | 1k | 100k | 1M |
+
| -------------- | -------: | --------: | ---------: |
+
| Original | 45047808 | 452837376 | 4227715072 |
+
| Spawn | 43728896 | 318230528 | 2869723136 |
+
| Reduce | 43552768 | 314798080 | 2849304576 |
+
| Count | 43732992 | 313507840 | 2780540928 |
+
| Registry | 44453888 | 311988224 | 2787237888 |
+
| RemoteCall | 43597824 | 310595584 | 2771525632 |
+
+
As we can see we have reduced the memory use by about 30% by just changing
+
from `Task.async/1` to `spawn/1`. Further optimisations reduced memory usage
+
slightly, but with no such drastic changes.
+
+
Can we do better?
+
+
Well, with some VM flags tinkering - of course.
+
+
You see, by default Erlang VM will not only create some data required for
+
handling process itself[^word]:
+
+
[^word]: Again, word here mean 8 bytes on 64-bit and 4 bytes on 32-bit architectures.
+
+
> | Data Type | Memory Size |
+
> | - | - |
+
> | … | … |
+
> | Erlang process | 338 words when spawned, including a heap of 233 words. |
+
>
+
> -- [Erlang Efficiency Guide: 11. Advanced](https://erlang.org/doc/efficiency_guide/advanced.html#Advanced)
+
+
As we can see, there are 105 words that are required and 233 words which are
+
used for preallocated heap. But this is microbenchmarking, so as we do not need
+
that much of memory (because our processes basically does nothing), we can
+
reduce it. We do not care about time performance anyway. For that we can use
+
`+hms` flag and set it to some small value, for example `1`.
+
+
In addition to heap size Erlang by default load some additional data from the
+
BEAM files. That data is used for debugging and error reporting, but again, we
+
are microbenchmarking, and who need debugging support anyway (answer: everyone,
+
so **do not** do it in production). Luckily for us, the VM has yet another flag
+
for that purpose `+L`.
+
+
Erlang also uses some [ETS][] (Erlang Term Storage) tables by default (for
+
example to support process registry we have mentioned above). ETS tables can be
+
compressed, but by default it's not done, as it can slow down some kinds of
+
operations on such tables. Fortunately there is, another, flag `+ec` that has
+
description:
+
+
> Forces option compressed on all ETS tables. Only intended for test and
+
> evaluation.
+
+
[ETS]: https://erlang.org/doc/man/ets.html
+
+
Sounds good enough for me.
+
+
With all these flags enabled we get peak memory footprint at 996257792 bytes.
+
+
Compare it in more human readable units.
+
+
| | Peak Memory Footprint for 1M processes |
+
| ------------------------ | -------------------------------------- |
+
| Original code | 3.94 GiB |
+
| Improved code | 2.58 GiB |
+
| Improved code with flags | 0.93 GiB |
+
+
Result - about 76% of the peak memory usage reduction. Not bad.
+
+
## Summary
+
+
First of all:
+
+
> Please, do not use ChatGPT for writing code for microbenchmarks.
+
+
The thing about *micro*benchmarking is that we write code that does as little as
+
possible to show (mostly) meaningless features of the given technology in
+
abstract environment. ChatGPT cannot do that, not out of malice or incompetence,
+
but because it used (mostly) *good* and idiomatic code to teach itself,
+
microbenchmarks rarely are something that people will consider to have these
+
qualities. It also cannot consider other features that [wetware][] can take into
+
account (like our "we do not need lists there" thing).
+
+
[wetware]: https://en.wikipedia.org/wiki/Wetware_(brain)
+10 -15
content/post/common-test-for-elixir.md
···
[taxonomies]
tags = [
-
"erlang",
"beam",
-
"elixir",
-
"testing",
-
"programming",
-
"common_test",
-
"commoner"
]
+++
In my new job I have opportunity to work a little bit more with Erlang code and
···
Just check [this out](/common-test-example/simple/index.html). This is example report
generated by the Common Test. As you can see it contains a lot of information in
-
quite readable format. Not only it contains informations about current run, but
all previous runs as well, which is really handy when tracking regressions.
-
But can we store even more informations there? Yes, as CT includes simple
-
logging facility it is completely possible to log your own informations during
-
tests, for example, lets modify our test to log some informations:
```erlang
test_function_name(_Config) ->
···
2 = 1 + 1.
```
-
Now when we run tests again, then we will see more informations (even coloured)
in [our test log](/common-test-example/log/ct_run.ct@NiunioBook.2019-07-16_11.03.21/common-test-example.log.logs/run.2019-07-16_11.03.22/example_suite.test_function_name.html):
![Common Test log "Example message" on green background](/img/common-test/log.png)
···
- On [Elixir forum thread about Commoner][forum]
- On Twitter via `@hauleth`
-
-
### Special thanks
-
-
- José Valim - for reading it through and reviewing before publishing
[ct]: http://erlang.org/doc/apps/common_test/basics_chapter.html
[ctex]: https://github.com/Comcast/ctex
···
[taxonomies]
tags = [
"beam",
+
"testing"
]
+
+
[[extra.thanks]]
+
name = "José Valim"
+
why = "for reading it through and reviewing before publishing"
+++
In my new job I have opportunity to work a little bit more with Erlang code and
···
Just check [this out](/common-test-example/simple/index.html). This is example report
generated by the Common Test. As you can see it contains a lot of information in
+
quite readable format. Not only it contains information about current run, but
all previous runs as well, which is really handy when tracking regressions.
+
But can we store even more information there? Yes, as CT includes simple
+
logging facility it is completely possible to log your own information during
+
tests, for example, lets modify our test to log some information:
```erlang
test_function_name(_Config) ->
···
2 = 1 + 1.
```
+
Now when we run tests again, then we will see more information (even coloured)
in [our test log](/common-test-example/log/ct_run.ct@NiunioBook.2019-07-16_11.03.21/common-test-example.log.logs/run.2019-07-16_11.03.22/example_suite.test_function_name.html):
![Common Test log "Example message" on green background](/img/common-test/log.png)
···
- On [Elixir forum thread about Commoner][forum]
- On Twitter via `@hauleth`
[ct]: http://erlang.org/doc/apps/common_test/basics_chapter.html
[ctex]: https://github.com/Comcast/ctex
+1 -5
content/post/eli5-ownership.md
···
[taxonomies]
tags = [
-
"rust",
-
"programming",
-
"ownership",
-
"eli5",
-
"borrowing"
]
+++
···
[taxonomies]
tags = [
+
"rust"
]
+++
+1 -4
content/post/elixir-application.md
···
[taxonomies]
tags = [
-
"elixir",
-
"erlang",
-
"beam",
-
"programming"
]
+++
···
[taxonomies]
tags = [
+
"beam"
]
+++
+8
content/post/indie-web.md
···
···
+
+++
+
title = "IndieWeb"
+
date = 2022-06-08T11:25:55+00:00
+
draft = true
+
+
[taxonomies]
+
tags = []
+
+++
+71
content/post/jeopardy-world.md
···
···
+
+++
+
title = "Jeopardy! world"
+
date = 2025-07-28
+
+
[taxonomies]
+
tags = ["ai", "culture"]
+
+++
+
+
Some time ago, there was an anime available on Netflix — *Godzilla Singular
+
Point*. It wasn't a spectacular success, but it featured a plot device that I
+
think reflects something increasingly common today: you need to know the answer
+
to your question before you can ask it.
+
+
This is something I see all the time in the current wave of AI hype. You need to
+
know what the answer *should* be before you can write a useful prompt.
+
+
<!-- more -->
+
+
The issue I have with many AI use cases is this: unless you have specialized
+
knowledge about the topic you're asking about, you can't reliably tell the
+
difference between a solid AI answer and complete nonsense.
+
+
I've had a few discussions about this on various Discord servers. The example I
+
often use is this simple question posed to an AI:
+
+
> Does 6 character long identification number, that contains digits and upper
+
> case letters (with exception to 0, O, 1, I, and L) is enough to randomly
+
> assign unique identification numbers for 10 million records?
+
+
You can see for your self answer from ChatGPT [there][chatgpt].
+
+
At first glance, the answer looks valid and sensible. The math checks out. It
+
calculates the number of available combinations correctly. Everything seems
+
*fine*.
+
+
**BUT…**
+
+
There is huge issue there, and probably most of the people who have been working
+
with basic statistic or cryptography will notice it. ChatGPT (and any other AI
+
that I have tested out) fail to notice very important word there
+
+
> \[…] randomly \[…]
+
+
This single word invalidates the entire reasoning, despite the correct
+
calculations. Because of the [birthday problem][], the answer isn't feasible.
+
While it's technically possible to assign a unique ID to every record, doing so
+
randomly introduces a high probability of collisions.
+
+
- At around 35,000 generated IDs, there's already a 50% chance of a collision
+
- At around 90,000, the chance of at least one duplicate reaches 99%
+
+
So even though the math is correct, the logic fails under the randomness constraint.
+
+
## *Jeopardy!* world
+
+
This is my main issue with AI tools: if you already have knowledge about the
+
subject, you don’t really need to ask the AI. But if you don’t have that
+
knowledge, you have no reliable way of knowing whether the answer makes sense or
+
not. It’s like playing *Jeopardy!* — you need to know the answer before you can
+
phrase the right question.
+
+
In my view, AI is most useful in areas where the results can be quickly reviewed
+
and discarded if needed. That’s why the whole “vibe coding” (aka slop
+
generation) approach falls short. If you don’t have a good sense of what the
+
output should look like, you probably don’t have the expertise to verify it.
+
+
[And gods forbid you from allowing AI to do anything on production][replit-fuckup].
+
+
[chatgpt]: https://chatgpt.com/share/68879fe7-d4e0-8007-9a30-3a9e2ace791d
+
[birthday problem]: https://en.wikipedia.org/wiki/Birthday_problem
+
[replit-fuckup]: https://www.businessinsider.com/replit-ceo-apologizes-ai-coding-tool-delete-company-database-2025-7?op=1
+2 -3
content/post/log-all-the-things.md
···
[taxonomies]
tags = [
-
"elixir",
-
"programming",
"observability"
]
···
)
```
-
As we can see there, the report contains informations like:
- `:label` - that describes type of the event
- `:report` - content of the "main" event
···
[taxonomies]
tags = [
+
"beam",
"observability"
]
···
)
```
+
As we can see there, the report contains information like:
- `:label` - that describes type of the event
- `:report` - content of the "main" event
-1
content/post/stop-spreading-crap-at-my-home.md
···
[taxonomies]
tags = [
-
"programming",
"culture"
]
+++
···
[taxonomies]
tags = [
"culture"
]
+++
-2
content/post/treachery-of-representation.md
···
[taxonomies]
tags = [
-
"programming",
-
"linguistic",
"culture"
]
+++
···
[taxonomies]
tags = [
"culture"
]
+++
+2 -5
content/post/vim-for-elixir.md
···
[taxonomies]
tags = [
-
"elixir",
-
"erlang",
-
"vim",
-
"neovim",
-
"programming"
]
+++
···
[taxonomies]
tags = [
+
"beam",
+
"vim"
]
+++
+511
content/post/who-watches-watchmen-i.md
···
···
+
+++
+
title = "Who Watches Watchmen? - Part 1"
+
date = 2022-01-17T21:22:18+01:00
+
+
description = """
+
A lot of application use systems like Kubernetes for their deployment. In my
+
humble opinion it is often overkill as system, that offers most of the stuff such
+
thing provide, is already present in your OS. In this article I will try to
+
present how to utilise the most popular system supervisor from Elixir
+
applications.
+
"""
+
+
[taxonomies]
+
tags = [
+
"beam",
+
"systemd"
+
]
+
+++
+
+
I gave talk about this topic on CODE Beam V Americas, but I wasn't really
+
satisfied with it. In this post I will try to describe what my presentation was
+
meant to be about.
+
+
If you are wondering about the presentation, [the slides are on SpeakerDeck][slides].
+
+
[slides]: https://speakerdeck.com/hauleth/who-supervises-supervisors
+
+
## Abstract
+
+
Most of the operating systems are multi-process and multi-user operating
+
systems. This has a lot of positive aspects, like to be able to do more than one
+
thing at the time at our devices, but it introduces a lot of complexities that
+
in most cases are hidden from the users and developers. These things still need
+
to be handled in one or another way. The most basic problems are:
+
+
- some processes need to be started before user can interact with the OS
+
in meaningful (for them) way (for example mounting filesystems, logging,
+
etc.)
+
- some processes require strict startup ordering, for example you may need
+
logging to be started before starting HTTP server
+
- system operator somehow need to know when the process is ready to do their
+
work, which is often some time after process start
+
- system operator should be able to check process state in case when debugging
+
is needed, most commonly via logs
+
- shutdown of the processes should be handled in a way, that will allow other
+
processes to be shut down cleanly (for example application that uses DB should
+
be down before DB itself)
+
+
## Why we need system supervisor?
+
+
System supervisor is a process started early in the OS boot, that should handle
+
starting and managing all other processes that will be run on our system. It is
+
often the init process (first process started by the OS that is running with PID
+
1\) or it is first (and sometimes only) process started by the init process.
+
Popular examples of such supervisors (often integrated with init systems):
+
+
- SysV which is "traditional" implementation that originates at UNIX System
+
V (hence the name)
+
- BSD init that with some variations is used in BSD-based OSes (NetBSD,
+
FreeBSD), it shares some similarities to SysV init and services description is
+
provided by shell scripts
+
- OpenRC that also uses shell-based scripts for service description, used by
+
Linux distributions like Gentoo or Alpine
+
- `launchd` that is used on Darwin (macOS, iPadOS, iOS, watchOS) systems that uses
+
XML-based `plists` for services description
+
- `runit` which is small init and supervisor, but quite capable, for example
+
used by Void Linux
+
- Upstart created by Canonical Ltd. as a replacement for SysV-like init system
+
in Ubuntu (no longer in use in Ubuntu), still used in some distributions like
+
ChromeOS or Synology NAS
+
- `systemd` (this is the name, not "SystemD") that was created by Red Hat
+
employee, (in)famous Lennart Poettering, and later was adopted by almost all
+
major Linux distributions which spawned some heated discussion about it
+
+
In this article I will focus on systemd, and its approach to "new-style system
+
daemons".
+
+
---
+
+
**DISCLAIMER**
+
+
Each of the solutions mentioned above has its strong and weak points. I do not
+
want to start another flame war whether it is good or not. It has some good in
+
it, and it has some bad in it, but we can say that it "won" over the most used
+
distributions, and despite our love or hate towards it, we need to learn how to
+
live with that.
+
+
---
+
+
## Why `systemd`?
+
+
`systemd` became a thing because SysV approach to ordering services' startup was
+
mildly irritating and non-parallelizable. In short, SysV is starting processes
+
exactly in lexicographical order of files in given directory. This meant, that
+
even if your service didn't need the DB at all, but it somehow ended further in
+
the directory listing, you ended in waiting for the DB startup. Additionally,
+
SysV wasn't really monitoring services, it just assumed that when process forked
+
itself to the background, then it is "done" with the startup, and we can
+
continue. This is obviously not true in many cases, for example, if your
+
previous shutdown wasn't clean because of power shortage or other issue, then
+
your DB probably need a bit of time to rebuild state from journal. This causes
+
even more slowdown for the processes further in the list. This is highly
+
undesired in modern, cloud-based, environment, where you can often start the
+
machines on-demand during autoscaling actions. When there is a spike in the
+
traffic that need autoscaling, then the sooner new machine is in usable state
+
the sooner it can take load from other machines.
+
+
Different tools take different approach to solve that issue there. `systemd`
+
take approach that is derived from `launchd` - do not do stuff, that is not
+
needed. We can trigger on action of other services (obviously), but also on
+
stuff like socket activity, path creation/modification, mounts, connection or
+
disconnection of device, time events, etc.
+
+
---
+
+
**DIGRESSION**
+
+
This is exactly the reason why `systemd` has its infamous "feature creep", it
+
doesn't "digest" all services like Cron or `udev`. It is not that these are
+
"tightly" intertwined into `systemd`. You can still replace them with their
+
older counterparts, you will just lose all the features these bring with them.
+
+
---
+
+
Such lazy approach sometimes require changes into the service itself. For
+
example to let supervisor know, that you are ready (not just started), you need
+
some way to communicate with supervisor. In `systemd` you can do so via UNIX
+
socket pointed by `NOTIFY_SOCKET` environment variable passed to your
+
application. With the same socket you can implement another useful feature
+
\- watchdog/heartbeat process. This mean that if for any reason your process
+
became non-responsive (but it will refuse to die), then supervisor will
+
forcefully bring process down and restart it, assuming that the error was
+
accidental.
+
+
About restarting, we can define behaviour of service after main process die. It
+
can be restarted regardless of the exit code, it can be restarted on abnormal
+
exit, it can remain shut, etc. Does this ring a bell? This works similarly to
+
OTP supervisors, but "one level above". If your service utilize system
+
supervisor right, you can make your application almost ultimately self-healing
+
(by restarts).
+
+
## Basic setup
+
+
Now, when we know a little about how and why `systemd` works as it works, we
+
now can go to details on how to utilize that with services in Elixir.
+
+
As a base we will implement super simple Plug application:
+
+
```elixir
+
# hello/application.ex
+
defmodule Hello.Application do
+
use Application
+
+
def start(_type, _opts) do
+
children = [
+
{Plug.Cowboy, [scheme: :http, plug: Hello.Router] ++ cowboy_opts()},
+
{Plug.Cowboy.Drainer, refs: :all}
+
]
+
+
Supervisor.start_link(children, strategy: :one_for_one)
+
end
+
+
defp cowboy_opts do
+
[
+
port: String.to_integer(System.get_env("PORT", "4000"))
+
]
+
end
+
end
+
```
+
+
```elixir
+
# hello/router.ex
+
defmodule Hello.Router do
+
use Plug.Router
+
+
plug :match
+
plug :dispatch
+
+
get "/" do
+
send_resp(conn, 200, "Hello World!")
+
end
+
end
+
```
+
+
I will also assume that we are using [Mix release][mix-release] named `hello`
+
that we later copy to `/opt/hello`.
+
+
[mix-release]: https://hexdocs.pm/mix/Mix.Tasks.Release.html
+
+
### systemd unit file
+
+
We have only one thing left, we need to define our [`hello.service`][systemd.service]:
+
+
```ini
+
[Unit]
+
Description=Hello World service
+
+
[Service]
+
Environment=PORT=80
+
ExecStart=/opt/hello/bin/hello start
+
```
+
+
Now you can create file with that content in
+
`/usr/local/lib/systemd/system/hello.service` and then start it with:
+
+
```
+
# systemctl start hello.service
+
```
+
+
This is the simplest service imaginable, however from the start we have few
+
issues there:
+
+
- It will run service as user running supervisor, so if it is run using global
+
supervisor, then it will run as `root`. You do not want to run anything as
+
`root`.
+
- On error it will produce (BEAM) core dump, which may contain sensitive data.
+
- It can read (and, due to being run as `root`, write) everything in the system,
+
like private data of other processes.
+
+
[systemd.service]: https://www.freedesktop.org/software/systemd/man/systemd.service.html#
+
+
## Service readiness
+
+
Erlang VM isn't really the best tool out there wrt the startup times. In
+
addition to that our application may need some preparation steps before it can
+
be marked as "ready". This is problem that I sometimes encounter in Docker,
+
where some containers do not really have any health check, and then I need to
+
have loop with check in some of the containers that depend on another one. This
+
"workaround" is frustrating, error prone, and can cause nasty Heisenbugs when
+
the timing will be wrong.
+
+
Two possible solutions for this problem are:
+
+
- Readiness probe - another program that is ran after the main process is
+
started, that checks whether our application is ready to work.
+
- Notification system where our application uses some common protocol to inform
+
the supervisor that it finished setup and is ready for work.
+
+
systemd supports the second approach via [`sd_notify`][sd_notify]. The approach
+
there is simple - we have `NOTIFY_SOCKET` environment variable that contain path
+
to the Unix datagram socket, that we can use to send information about state of
+
our application. This socket accept set of different messages, but right now,
+
for our purposes, we will focus only on few of them:
+
+
- `READY=1` - marks our service as ready, aka it is ready to do its work (for
+
example accept incoming HTTP connections in our example). It need to be sent
+
within given timespan after start of the VM, otherwise the process will be
+
killed and possibly restarted
+
- `STATUS=name` - sets status of our application that can be checked via
+
`systemctl status hello.service`, this allows us to have better insight into
+
what is the high level state without manually traversing through logs
+
- `RELOADING=1` - marks, that our application is reloading, which in general may
+
mean a lot of things, but there it will be used to mark `:init.restart/0`-like
+
behaviour (due to [erlang/otp#4698][] there is wrapper for that function in
+
`systemd` library). The process need then to send `READY=1` within given
+
timespan, or the process will be marked as a malfunctioning, and will be
+
forcefully killed and possibly restarted
+
- `STOPPING=1` - marks, that our application began shutting down process, and
+
will be closing soon. If the process will not close within given timespan, it
+
will be forcefully killed
+
+
These messages provide us enough power to not only mark the service as ready,
+
but also provides additional information about system state, so even operator,
+
who knows a little about Erlang or our application runtime, will be able to
+
understand what is going on.
+
+
The main thing is that systemd will wait with activation of the dependants of
+
our system as well as the `systemctl start` and `systemctl restart` commands
+
will wait until our service declare that it is ready.
+
+
Usage of such feature is quite simple:
+
+
```ini
+
[Unit]
+
Description=Hello World service
+
+
[Service]
+
# Define `Type=` to `notify`
+
Type=notify
+
Environment=PORT=80
+
ExecStart=/opt/hello/bin/hello start
+
WatchdogSec=1min
+
```
+
+
And then in our supervisor tree we need add `:systemd.ready()` **after** last
+
process needed for proper functioning of our application, in our simple example
+
it is after `Plug.Cowboy`:
+
+
```elixir
+
# hello/application.ex
+
defmodule Hello.Application do
+
use Application
+
+
def start(_type, _opts) do
+
children = [
+
{Plug.Cowboy, [scheme: :http, plug: Hello.Router] ++ cowboy_opts()},
+
:systemd.ready(), # <-- it is function call, as it returns proper
+
# `child_spec/0`
+
{Plug.Cowboy.Drainer, refs: :all}
+
]
+
+
Supervisor.start_link(children, strategy: :one_for_one)
+
end
+
+
defp cowboy_opts do
+
[
+
port: String.to_integer(System.get_env("PORT", "4000"))
+
]
+
end
+
end
+
```
+
+
Now restarting our service will not finish immediately, but will wait until our
+
service will declare that it is ready.
+
+
```
+
# systemctl restart hello.service
+
```
+
+
About `STOPPING=1` - the magic thing is that the `systemd` library takes care of
+
it for you. As soon as the system will be scheduled to shutdown this message
+
will be automatically sent, and the operator will be notified about this fact.
+
+
We can also provide more information about state of our application. As you may
+
have already noticed, we have [`Plug.Cowboy.Drainer`][] there. It is process that
+
will delay shutdown of our application while there are still open connections.
+
This can take some time, so it would be handy if the operator would see that the
+
draining is in progress. We can easily achieve that by again changing our
+
supervision tree to:
+
+
```elixir
+
# hello/application.ex
+
defmodule Hello.Application do
+
use Application
+
+
def start(_type, _opts) do
+
children = [
+
{Plug.Cowboy, [scheme: :http, plug: Hello.Router] ++ cowboy_opts()},
+
:systemd.ready(),
+
:systemd.set_status(down: [status: "drained"]),
+
{Plug.Cowboy.Drainer, refs: :all, shutdown: 10_000},
+
:systemd.set_status(down: [status: "draining"])
+
]
+
+
Supervisor.start_link(children, strategy: :one_for_one)
+
end
+
+
defp cowboy_opts do
+
[
+
port: String.to_integer(System.get_env("PORT", "4000"))
+
]
+
end
+
end
+
```
+
+
Now when we will shutdown our application by:
+
+
```
+
# systemctl stop hello.service
+
```
+
+
And we have some connections open to our service (you can simulate that with
+
`wrk`) then when we ran `systemctl status hello.service` in separate terminal
+
(previous will be blocked until our service shuts down) then you will be able to
+
see something like:
+
+
```
+
● hello.service - Example Plug application
+
Loaded: loaded (/usr/local/lib/systemd/system/hello.service; static; vendor preset: enabled)
+
Active: deactivating (stop-sigterm) since Sat 2022-01-15 17:46:30 CET;
+
1s ago
+
Main PID: 1327 (beam.smp)
+
Status: "draining"
+
Tasks: 19 (limit: 1136)
+
Memory: 106.5M
+
```
+
+
You can notice that the `Status` is set to `"draining"`. As soon as all
+
connections will be drained it will change to `"drained"` and then the
+
application will shut down and service will be marked as `inactive`.
+
+
[sd_notify]: https://www.freedesktop.org/software/systemd/man/sd_notify.html
+
[erlang/otp#4698]: https://github.com/erlang/otp/issues/4698
+
[`Plug.Cowboy.Drainer`]: https://hexdocs.pm/plug_cowboy/2.5.2/Plug.Cowboy.Drainer.html
+
+
## Watchdog
+
+
Watchdog allows us to monitor our application for responsiveness (as mentioned
+
above). It is simple feature that requires our application to ping systemd
+
within specified interval, otherwise the application will be forcibly shut down
+
as malfunctioning. Fortunately for us, the `systemd` library that provides our
+
integration, have that feature out of the box, so all we need to do to achieve
+
expected result is set `WatchdogSec=` option in our `systemd.service` file:
+
+
```ini
+
[Unit]
+
Description=Hello World service
+
+
[Service]
+
Environment=PORT=80
+
Type=notify
+
ExecStart=/opt/hello/bin/hello start
+
WatchdogSec=1min
+
```
+
+
This configuration says that if the VM will not send healthy message each 1
+
minute interval, then the service will be marked as malfunctioning. From the
+
application side we can manage state of the watchdog in several ways:
+
+
- By setting `systemd.watchdog_check` configuration option we can configure the
+
function that will be called on each check, if that function return `true`
+
then it mean that application is healthy and the systemd should be notified
+
with ping, if it returns `false` or fail, then the check will be omitted.
+
- Manually sending trigger message in case of detected problems via
+
`:systemd.watchdog(trigger)`, it will immediately mark service as
+
malfunctioning and will trigger action defined in service unit file (by
+
default it will restart application)
+
- Disabling built in watchdog process via `:systemd.watchdog(:disable)` and then
+
manually sending `:systemd.watchdog(:ping)` within expected intervals
+
(discouraged)
+
+
## Security
+
+
We should start with changing default user and group which is assigned to our
+
process. We can do so in 2 different ways:
+
+
1. Use some existing user and group by defining `User=` and `Group=` directives
+
in our service definition; or
+
2. Create ephemeral user on-demand before our service starts, by using directive
+
`DynamicUser=true` in service definition.
+
+
I prefer second option, as it additionally provides a lot of other security
+
related options, like creating private `/tmp` directory, making system
+
read-only, etc. This has also some disadvantages, like removing all of given
+
data on service shutdown, however there are options to keep some data between
+
launches.
+
+
In addition to that we can add `PrivateDevices=true` that will hide all
+
physical devices from `/dev` leaving only pseudo devices like `/dev/null` or
+
`/dev/urandom` (so you will be able to use `:crypto` and `:ssl` modules without
+
problems).
+
+
Next thing is that we can do, is to [disable crash dumps generated by BEAM][crash].
+
While not strictly needed in this case, it is worth remembering, that it isn't
+
hard to achieve, just use `Environment=ERL_CRASH_DUMP_SECONDS=0`.
+
+
Our new, more secure, `hello.service` will look like:
+
+
```ini
+
[Unit]
+
Description=Hello World service
+
Requires=network.target
+
+
[Service]
+
Type=notify
+
Environment=PORT=80
+
ExecStart=/opt/hello/bin/hello start
+
WatchdogSec=1min
+
+
# We need to add capability to be able to bind on port 80
+
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
+
+
# Hardening
+
DynamicUser=true
+
PrivateDevices=true
+
Environment=ERL_CRASH_DUMP_SECONDS=0
+
```
+
+
The problem with that configuration is that our service is now capable on
+
binding **any** port under 1024, so for example, if there is some security
+
issue, then the malicious party can open any of the restricted ports and then
+
serve whatever data they want there. This can be quite problematic, and the
+
solution for that problem will be covered in Part 2, where we will cover socket
+
passing and socket activation for our service.
+
+
With that we achieved quite basic level of isolation to what Docker (or other
+
container runtime) is providing, but it do not require `overlayfs` or anything
+
more, than what you already have on your machine. That means, updates done by
+
your system package manager will be applied to all running services. With that
+
you do not need to rebuild all your containers when there is security patch
+
issued for any of your dependencies.
+
+
Of course it only scratches the surface of what is possible with systemd wrt
+
the hardening of the services. More information can be found in [RedHat
+
article][rh-systemd-hardening] and in [`systemd-analyze security` command
+
output][systemd-analyze-security]. Possible features are:
+
+
- creation of the private networks for your services
+
- disallowing creation of socket connections that are outside of the specified
+
set of families
+
- make only some paths readable
+
- hide some paths from the process
+
- etc.
+
+
Coverage of just that topic is a little bit out of scope for this blog post, so
+
I encourage you to read the documentation of [`systemd.exec`][systemd.exec] and
+
articles mentioned above for more details.
+
+
[crash]: https://erlef.github.io/security-wg/secure_coding_and_deployment_hardening/crash_dumps
+
[rh-systemd-hardening]: https://www.redhat.com/sysadmin/mastering-systemd
+
[systemd-analyze-security]: https://www.freedesktop.org/software/systemd/man/systemd-analyze.html#systemd-analyze%20security%20%5BUNIT...%5D
+
[systemd.exec]: https://www.freedesktop.org/software/systemd/man/systemd.exec.html
+
+
## Summary
+
+
This blog post is already quite lengthy, so I will split it into separate parts.
+
There probably will be 3 of them:
+
+
- [Part 1 - Basics, security, and FD passing (this one)](./#top)
+
- [Part 2 - Socket activation](@/post/who-watches-watchmen-ii.md)
+
- Part 3 - Logging
+412
content/post/who-watches-watchmen-ii.md
···
···
+
+++
+
title = "Who Watches Watchmen? - Part 2"
+
date = 2023-11-14
+
+
description = """
+
Continuation of travel into making systemd to work for us, not against us. This
+
time we will talk about socket activation and how to make our application run
+
only when we need it to run.
+
"""
+
+
[taxonomies]
+
tags = [
+
"beam",
+
"systemd"
+
]
+
+
[[extra.thanks]]
+
name = "Nicodemus"
+
why = "helping me with my poor English"
+
+++
+
+
This is continuation of [Part I][part-i] where I described the basics of the
+
supervising BEAM applications with systemd and how to create basic, secure
+
service for your Elixir application with it. In this article I will assume that
+
you have read [the previous one][part-i].
+
+
______________________________________________________________________
+
+
We already have our super simple service description. Just to refresh your
+
memory, it is the `hello.service` file once again:
+
+
```ini
+
[Unit]
+
Description=Hello World service
+
Requires=network.target
+
+
[Service]
+
Type=notify
+
Environment=PORT=80
+
ExecStart=/opt/hello/bin/hello start
+
WatchdogSec=1min
+
+
# We need to add capability to be able to bind on port 80
+
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
+
+
# Hardening
+
DynamicUser=true
+
PrivateDevices=true
+
Environment=ERL_CRASH_DUMP_SECONDS=0
+
```
+
+
However there is one small problem. It allows our service to listen on **any**
+
restricted port, not just `80` that we want to listen on. This can be
+
troublesome as an attacker that gains RCE on our server can then capture any
+
traffic on any port that we do not want to open (for example exposing port 22
+
using the [`ssh`] module).
+
+
It would be nice if we could somehow inject sockets for only the ports we want
+
to listen to into our application.
+
+
## Socket passing
+
+
Thanks to the [`systemd.socket`][systemd.socket] feature we can achieve that
+
with a little work on our side.
+
+
First we need to create new unit named `hello.socket` next to our
+
`hello.service`:
+
+
```ini
+
[Unit]
+
Description=Listening socket
+
Requires=sockets.target
+
+
[Socket]
+
ListenStream=80
+
BindIPv6Only=both
+
ReusePort=true
+
NoDelay=true
+
```
+
+
It will create a socket connected to TCP 80 (because we used `ListenStream=`,
+
and TCP is the stream protocol). By default it will bind that socket to a
+
service named the same as our socket, so now we need to edit our `hello.service`
+
a little bit:
+
+
```ini
+
[Unit]
+
Description=Hello World service
+
Requires=network.target
+
+
[Service]
+
Type=notify
+
Environment=PORT=80
+
ExecStart=/opt/hello/bin/hello start
+
WatchdogSec=1min
+
+
# See, we no longer need to insecurely allow binding to any port
+
# CapabilityBoundingSet=CAP_NET_BIND_SERVICE
+
+
# Hardening
+
DynamicUser=true
+
PrivateDevices=true
+
Environment=ERL_CRASH_DUMP_SECONDS=0
+
```
+
+
And we need to modify our `Hello.Application.cowboy_opts/0` to handle the socket
+
which is passed to us a file descriptor:
+
+
```elixir
+
# hello/application.ex
+
defmodule Hello.Application do
+
use Application
+
+
def start(_type, _opts) do
+
fds = :systemd.listen_fds()
+
+
children = [
+
{Plug.Cowboy, [scheme: :http, plug: Hello.Router] ++ cowboy_opts(fds)},
+
{Plug.Cowboy.Drainer, refs: :all}
+
]
+
+
Supervisor.start_link(children, strategy: :one_for_one)
+
end
+
+
# If there are no sockets passed to the application, then start listening on
+
# the port specified by the `PORT` environment variable
+
defp cowboy_opts([]) do
+
[port: String.to_integer(System.get_env("PORT", "5000"))]
+
end
+
+
# If there are any socket passed, then use first one
+
defp cowboy_opts([socket | _]) do
+
fd =
+
case socket do
+
# Sockets can be named, which will be passed as the second element in
+
# a tuple
+
{fd, _name} -> fd
+
# Or unnamed, and then it will be just the file descriptor
+
fd -> fd
+
end
+
+
[
+
net: :inet6, # (1)
+
port: 0, # (2)
+
fd: fd # (3)
+
]
+
end
+
end
+
```
+
+
1. Systemd sockets are IPv6 enabled (we explicitly said that we want to listen
+
on both). That means, that we need to mark our connection as an INET6
+
connection. This will not affect IPv4 (INET) connections.
+
1. We are required to pass `:port` key, but its value will be ignored, so we
+
just pass `0`.
+
1. We pass the file descriptor that will be then passed to the Cowboy listener.
+
+
Now when we will start our service:
+
+
```txt
+
# systemctl start hello.service
+
```
+
+
It will be available at `https://localhost/` while still running as an
+
unprivileged user.
+
+
### Multiple ports
+
+
The question may arise - how to allow our service to listen on more than one
+
port, for example you want to have your website available as HTTPS alongside
+
"regular" HTTP. This means that our application needs to listen on two
+
restricted ports:
+
+
- 80 - for HTTP
+
- 443 - for HTTPS
+
+
Now we need to slightly modify a little our socket service and add another one.
+
First rename our `hello.socket` to `hello-http.socket` and add a line
+
`Service=hello.service` and `FileDescriptorName=http` to `[Socket]` section, so
+
we end with:
+
+
```ini
+
[Unit]
+
Description=HTTP Socket
+
Requires=sockets.target
+
+
[Socket]
+
# We declare the name of the file descriptor here to simplify extraction
+
# in the application afterwards. By default it will be the socket name
+
# (so `hello-http` in our case), but `http` is much cleaner.
+
FileDescriptorName=http
+
ListenStream=80
+
Service=hello.service
+
BindIPv6Only=both
+
ReusePort=true
+
NoDelay=true
+
```
+
+
Next we create a similar file, but for HTTPS named `hello-https.socket`
+
+
```ini
+
[Unit]
+
Description=HTTPS Socket
+
Requires=sockets.target
+
+
[Socket]
+
FileDescriptorName=https
+
ListenStream=443
+
Service=hello.service
+
BindIPv6Only=both
+
ReusePort=true
+
NoDelay=true
+
```
+
+
And we add the dependency on both of our sockets to the `hello.service`:
+
+
```ini
+
[Unit]
+
Description=Hello World service
+
After=hello-http.socket hello-https.socket
+
BindTo=hello-http.socket hello-https.socket
+
+
[Service]
+
ExecStart=/opt/hello/bin/hello start
+
+
# Hardening
+
DynamicUser=true
+
PrivateDevices=true
+
Environment=ERL_CRASH_DUMB_SECONDS=0
+
```
+
+
Now we need to somehow differentiate between our sockets in the
+
`Hello.Application`, so we will be able to pass the proper FD to each of the
+
listeners. The `:systemd.listen_fds/0` will return a list of file descriptors,
+
and if they are named, the format will be a 2-tuple where the first element is
+
the file descriptor and the second is the name as a string:
+
+
```elixir
+
# hello/application.ex
+
defmodule Hello.Application do
+
use Application
+
+
def start(_type, _opts) do
+
fds = :systemd.listen_fds()
+
+
router = Hello.Router
+
+
children = [
+
{Plug.Cowboy, [
+
scheme: :http,
+
plug: router
+
] ++ cowboy_opts(fds, "http")},
+
{Plug.Cowboy, [
+
scheme: :https,
+
plug: router,
+
keyfile: "path/to/keyfile.pem",
+
certfile: "path/to/certfile.pem",
+
dhfile: "path/to/dhfile.pem"
+
] ++ cowboy_opts(fds, "https")},
+
{Plug.Cowboy.Drainer, refs: :all}
+
]
+
+
Supervisor.start_link(children, strategy: :one_for_one)
+
end
+
+
defp cowboy_opts(fds, protocol) do
+
case List.keyfind(fds, protocol, 1) do
+
# If there is socket passed for given protocol, then use that one
+
{fd, ^protocol} ->
+
[
+
net: :inet6,
+
port: 0,
+
fd: fd
+
]
+
+
# If there are no sockets passed to the application that match
+
# the protocol, then start listening on the port specified by
+
# `PORT_{protocol}` environment variable
+
_ ->
+
[
+
port: String.to_integer(System.get_env("PORT_#{protocol}", "5000"))
+
]
+
end
+
end
+
```
+
+
Now our application will listen on both - HTTP and HTTPS, despite running as
+
unprivileged user.
+
+
## Socket activation
+
+
Now, that we can inject sockets to our application with ease we can achieve even
+
more fascinating feature - socket activation.
+
+
Some of you may used `inetd` in the past, that allows you to dynamically start
+
processes on network requests. It is quite an interesting tool that detects
+
traffic on certain ports, then spawns a new process to handle it, passing data
+
to and from that process via `STDIN` and `STDOUT`. There was a quirk though, it
+
required the spawned process to shutdown after it handled the request and it was
+
starting a new instance for each request. That works poorly with VMs like BEAM
+
that have substantial startup time and are expected to be long-running systems.
+
BEAM is capable of handling network requests on it's own.
+
+
Fortunately for us, the way that we have implemented our systemd service is all
+
that we need to have our application dynamically activated. To observe that we
+
just need to shutdown everything:
+
+
```txt
+
# systemctl stop hello-http.socket hello-https.socket hello.service
+
```
+
+
And now relaunch **only the sockets**:
+
+
```txt
+
# systemctl start hello-http.socket hello-https.socket
+
```
+
+
We can check, that our service is not running:
+
+
```txt
+
$ systemctl status hello.service
+
● hello.service - Hello World service
+
Loaded: loaded (/usr/local/lib/systemd/system/hello.service; static; vendor preset: enabled)
+
Active: inactive (dead)
+
TriggeredBy: ● hello-http.socket ● hello-https.socket
+
```
+
+
We can see the `TriggeredBy` section that tells us, that this service will be
+
started by one of the sockets listed there. Let see what will happen when we
+
will try to request anything from our application:
+
+
```txt
+
$ curl http://localhost/
+
Hello World!
+
```
+
+
You can see that we got a response from our application. This mean that our
+
application must have started, and indeed when we check:
+
+
```txt
+
$ systemctl status hello.service
+
● hello.service - Hello
+
Loaded: loaded (/usr/local/lib/systemd/system/hello.service; static; vendor preset: enabled)
+
Active: active (running) since Thu 2022-02-03 13:20:27 CET; 4s ago
+
TriggeredBy: ● hello-http.socket ● hello-https.socket
+
Main PID: 1106 (beam.smp)
+
Tasks: 19 (limit: 1136)
+
Memory: 116.7M
+
CGroup: /system.slice/hello.service
+
├─1106 /opt/hello/erts-12.2/bin/beam.smp -- -root /opt/hello -progname erl -- -home /run/hello -- -noshell -s elixir start_cli -mode embedded -setcookie CR63SVI6L5JAMJSDL3H4XPNMOPHEWSV2FPHCHCAN65CY6ASHMXBA==== -sname hello -c>
+
└─1138 erl_child_setup 1024
+
```
+
+
It seems to be running, and if we stop it, then we will get information that it
+
still can be activated by our sockets:
+
+
```txt
+
# systemctl stop hello.service
+
Warning: Stopping hello.service, but it can still be activated by:
+
hello-http.socket hello-https.socket
+
```
+
+
That means, that systemd is still listening on the sockets that we defined, even
+
when our application is down, and will start our application again as soon as
+
there are any incoming requests.
+
+
Let test that out again:
+
+
```txt
+
$ curl http://localhost/
+
Hello World!
+
$ systemctl status hello.service
+
● hello.service - Hello
+
Loaded: loaded (/usr/local/lib/systemd/system/hello.service; static; vendor preset: enabled)
+
Active: active (running) since Thu 2022-02-03 13:22:27 CET; 4s ago
+
TriggeredBy: ● hello-http.socket ● hello-https.socket
+
Main PID: 3452 (beam.smp)
+
Tasks: 19 (limit: 1136)
+
Memory: 116.7M
+
CGroup: /system.slice/hello.service
+
├─3452 /opt/hello/erts-12.2/bin/beam.smp -- -root /opt/hello -progname erl -- -home /run/hello -- -noshell -s elixir start_cli -mode embedded -setcookie CR63SVI6L5JAMJSDL3H4XPNMOPHEWSV2FPHCHCAN65CY6ASHMXBA==== -sname hello -c>
+
└─3453 erl_child_setup 1024
+
```
+
+
Our application got launched again, automatically, just by the fact that
+
there was incoming TCP connection.
+
+
Does it work for HTTPS connection as well?
+
+
```txt
+
# systemctl stop hello.service
+
$ curl -k https://localhost/
+
Hello World!
+
```
+
+
It seems so. Independently of which port we try to reach our application on, it
+
will be automatically launched for us and the connection will be properly
+
handled. Do note that systemd will not shut down our process after serving the
+
request. It will continue to run from that point forward.
+
+
## Summary
+
+
I know that it took quite while since the last post (ca. 1.5 years), but I hope
+
that I will be able to write the final part much sooner than this.
+
+
- [Part 1 - Basics, security, and FD passing][part-i]
+
- [Part 2 - Socket activation (this one)](./#top)
+
- Part 3 - Logging
+
+
[part-i]: @/post/who-watches-watchmen-i.md
+
[systemd.socket]: https://www.freedesktop.org/software/systemd/man/systemd.socket.html
+
[`ssh`]: https://erlang.org/doc/man/ssh.html
+291
content/post/writing-tests.md
···
···
+
+++
+
date = 2023-11-20
+
title = "How do I write Elixir tests?"
+
+
draft = true
+
+
[taxonomies]
+
tags = [
+
"beam",
+
"testing"
+
]
+
+++
+
+
This post was created for myself to codify some basic guides that I use while
+
writing tests. If you, my dear reader, read this then there is one important
+
thing for you to remember:
+
+
These are **guides** not *rules*. Each code base is different deviations and are
+
expected and *will* happen. Just use the thing between your ears.
+
+
## `@subject` module attribute for module under test
+
+
While writing test in Elixir it is not always obvious what we are testing. Just
+
imagine test like:
+
+
```elixir
+
test "foo should frobnicate when bar" do
+
bar = pick_bar()
+
+
assert :ok == MyBehaviour.foo(MyImplementation, bar)
+
end
+
```
+
+
It is not obvious at the first sight what we are testing. And it is pretty
+
simplified example, in real world it can became even harder to notice what is
+
module under test (MUT).
+
+
To resolve that I came up with a simple solution. I create module attribute
+
named `@subject` that points to the MUT:
+
+
```elixir
+
@subject MyImplementation
+
+
test "foo should frobnicate when bar" do
+
bar = pick_bar()
+
+
assert :ok == MyBehaviour.foo(@subject, bar)
+
end
+
```
+
+
Now it is more obvious what is MUT and what is just wrapper code around it.
+
+
In the past I have been using `alias` with `:as` option, like:
+
+
```elixir
+
alias MyImplementation, as: Subject
+
```
+
+
However I find module attribute to be more visually outstanding and make it
+
easier for me to notice `@subject` than `Subject`. But your mileage may vary.
+
+
## `describe` with function name
+
+
That one is pretty basic, and I have seen that it is pretty standard for people:
+
when you are writing tests for module functions, then group them in `describe`
+
blocks that will contain name (and arity) of the function in the name. Example:
+
+
```elixir
+
# Module under test
+
defmodule Foo do
+
def a(x, y, z) do
+
# some code
+
end
+
end
+
+
# Tests
+
defmodule FooTest do
+
use ExUnit.Case, async: true
+
+
@subject Foo
+
+
describe "a/3" do
+
# Some tests here
+
end
+
end
+
```
+
+
This allows me to see what functionality I am testing.
+
+
Of course that doesn't apply to the Phoenix controllers, as there we do not test
+
functions, but tuples in form `{method, path}` which I then write as `METHOD
+
path`, for example `POST /users`.
+
+
## Avoid module mocking
+
+
In Elixir we have bunch of the mocking libraries out there, but most of them
+
have quite substantial issue for me - these prevent me from using `async: true`
+
for my tests. This often causes substantial performance hit, as it prevents
+
different modules to run in parallel (not single tests, *modules*, but that is
+
probably material for another post).
+
+
Instead of mocks I prefer to utilise dependency injection. Some people may argue
+
that "Elixir is FP, not OOP, there is no need for DI" and they cannot be further
+
from truth. DI isn't related to OOP, it just have different form, called
+
function arguments. For example, if we want to have function that do something
+
with time, in particular - current time. Then instead of writing:
+
+
```elixir
+
def my_function(a, b) do
+
do_foo(a, b, DateTime.utc_now())
+
end
+
```
+
+
Which would require me to use mocks for `DateTime` or other workarounds to make
+
tests time-independent. I would do:
+
+
```elixir
+
def my_function(a, b, now \\ DateTime.utc_now()) do
+
do_foo(a, b, now)
+
end
+
```
+
+
Which still provide me the ergonomics of `my_function/2` as above, but is way
+
easier to test, as I can pass the date to the function itself. This allows me to
+
run this test in parallel as it will not cause other tests to do weird stuff
+
because of altered `DateTime` behaviour.
+
+
## Avoid `ex_machina` factories
+
+
I have poor experience with tools like `ex_machina` or similar. These often
+
bring whole [Banana Gorilla Jungle problem][bgj] back, just changed a little, as
+
now instead of just passing data around, we create all needless structures for
+
sole purpose of test, even when they aren't needed for anything.
+
+
[bgj]: https://softwareengineering.stackexchange.com/q/368797
+
+
Start with example from [ExMachina README](https://github.com/beam-community/ex_machina#overview):
+
+
```elixir
+
defmodule MyApp.Factory do
+
# with Ecto
+
use ExMachina.Ecto, repo: MyApp.Repo
+
+
# without Ecto
+
use ExMachina
+
+
def user_factory do
+
%MyApp.User{
+
name: "Jane Smith",
+
email: sequence(:email, &"email-#{&1}@example.com"),
+
role: sequence(:role, ["admin", "user", "other"]),
+
}
+
end
+
+
def article_factory do
+
title = sequence(:title, &"Use ExMachina! (Part #{&1})")
+
# derived attribute
+
slug = MyApp.Article.title_to_slug(title)
+
%MyApp.Article{
+
title: title,
+
slug: slug,
+
# associations are inserted when you call `insert`
+
author: build(:user),
+
}
+
end
+
+
# derived factory
+
def featured_article_factory do
+
struct!(
+
article_factory(),
+
%{
+
featured: true,
+
}
+
)
+
end
+
+
def comment_factory do
+
%MyApp.Comment{
+
text: "It's great!",
+
article: build(:article),
+
author: build(:user) # That line is added by me
+
}
+
end
+
end
+
```
+
+
For start we can see a single problem there - we do not validate our factories
+
against our schema changesets. Without additional tests like:
+
+
```elixir
+
@subject MyApp.Article
+
+
test "factory conforms to changeset" do
+
changeset = @subject.changeset(%@subject{}, params_for(:article))
+
+
assert changeset.valid?
+
end
+
```
+
+
We cannot be sure that our tests test what we want them to test. And if we pass
+
custom attribute values in some tests it gets even worse, because we cannot be
+
sure if these are conforming either.
+
+
That mean that our tests may be moot, because we aren't testing against real
+
situations, but against some predefined state.
+
+
Another problem is that if we need to alter the behaviour of the factory it can
+
became quite convoluted. Imagine situation when we want to test if comments by
+
author of the post have some special behaviour (for example it has some
+
additional CSS class to be able to mark them in CSS). That require from us to do
+
some dancing around passing custom attributes:
+
+
```elixir
+
test "comments by author are special" do
+
post = insert(:post)
+
comment = insert(:comment, post: post, author: post.author)
+
+
# rest of the test
+
end
+
```
+
+
And this is simplified example. In the past I needed to deal with situations
+
where I was creating a lot of data to pass through custom attributes to make
+
test sensible.
+
+
Instead I prefer to do stuff directly in code. Instead of relying on some
+
"magical" functions provided by some "magical" macros from external library I
+
can use what I already have - functions in my application.
+
+
Instead of:
+
+
```elixir
+
test "comments by author are special" do
+
post = insert(:post)
+
comment = insert(:comment, post: post, author: post.author)
+
+
# rest of the test
+
end
+
```
+
+
Write:
+
+
```elixir
+
test "comments by author are special" do
+
author = MyApp.Users.create(%{
+
name: "John Doe",
+
email: "john@example.com"
+
})
+
post = MyApp.Blog.create_article(%{
+
author: author,
+
content: "Foo bar",
+
title: "Foo bar"
+
})
+
comment = MyApp.Blog.create_comment_for(article, %{
+
author: author,
+
content: "Foo bar"
+
})
+
+
# rest of the test
+
end
+
```
+
+
It may be a little bit more verbose, but it makes tests way more readable in my
+
opinion. You have all details just in place and you know what to expect. And if
+
you need some piece of data in all (or almost all) tests within
+
module/`describe` block, then you can always can use `setup/1` blocks. Or you
+
can create function per module that will generate data for you. As long as your
+
test module is self-contained and do not receive "magical" data out of thin air,
+
it is ok for me. But `ex_machina` is, in my opinion, terrible idea brought from
+
Rails world, that make little to no sense in Elixir.
+
+
If you really need such factories, then just write your own functions that will
+
use your contexts instead of relying on another library. For example:
+
+
```elixir
+
import ExUnit.Assertions
+
+
def create_user(name, email \\ nil, attrs \\ %{}) do
+
email = email || "#{String.replace(name, " ", ".")}@example.com"
+
attrs = Map.merge(attrs, %{name: name, email: email})
+
+
assert {:ok, user} = MyApp.Users.create(attrs)
+
+
user
+
end
+
+
# And so on…
+
```
+
+
This way you do not need to check if all tests use correct validations any
+
longer, as your system will do that for you.
+2 -3
content/post/writing-vim-plugin.md
···
date = 2019-11-04T18:21:18+01:00
description = """
Article about writing Vim plugins, but not about writing Vim plugins. It is
-
how to concieve plugin, how to go from an idea to the full fledged plugin."""
[taxonomies]
tags = [
-
"vim",
-
"viml"
]
+++
···
date = 2019-11-04T18:21:18+01:00
description = """
Article about writing Vim plugins, but not about writing Vim plugins. It is
+
how to conceive plugin, how to go from an idea to the full fledged plugin."""
[taxonomies]
tags = [
+
"vim"
]
+++
-22
default.nix
···
-
{ pkgs ? import <nixpkgs> {}, ... }:
-
-
with pkgs;
-
-
stdenv.mkDerivation {
-
name = "hauleth-blog";
-
src = ./.;
-
-
nativeBuildInputs = [ git ];
-
buildInputs = [ zola ];
-
-
buildPhase = ''
-
git submodule update --init --recursive --depth=1
-
zola build -o $out
-
'';
-
-
dontInstall = true;
-
-
passthru = {
-
inherit zola;
-
};
-
}
···
+57
flake.lock
···
···
+
{
+
"nodes": {
+
"flake-utils": {
+
"inputs": {
+
"systems": "systems"
+
},
+
"locked": {
+
"lastModified": 1731533236,
+
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
+
"owner": "numtide",
+
"repo": "flake-utils",
+
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
+
"type": "github"
+
},
+
"original": {
+
"owner": "numtide",
+
"repo": "flake-utils",
+
"type": "github"
+
}
+
},
+
"nixpkgs": {
+
"locked": {
+
"lastModified": 0,
+
"narHash": "sha256-cnL5WWn/xkZoyH/03NNUS7QgW5vI7D1i74g48qplCvg=",
+
"path": "/nix/store/h15y13p2w17dhpiyh8pk42v1k4c38a0h-source",
+
"type": "path"
+
},
+
"original": {
+
"id": "nixpkgs",
+
"type": "indirect"
+
}
+
},
+
"root": {
+
"inputs": {
+
"flake-utils": "flake-utils",
+
"nixpkgs": "nixpkgs"
+
}
+
},
+
"systems": {
+
"locked": {
+
"lastModified": 1681028828,
+
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
+
"owner": "nix-systems",
+
"repo": "default",
+
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
+
"type": "github"
+
},
+
"original": {
+
"owner": "nix-systems",
+
"repo": "default",
+
"type": "github"
+
}
+
}
+
},
+
"root": "root",
+
"version": 7
+
}
+51
flake.nix
···
···
+
{
+
description = "Flake utils demo";
+
+
inputs.flake-utils.url = "github:numtide/flake-utils";
+
+
outputs = { self, nixpkgs, flake-utils }:
+
flake-utils.lib.eachDefaultSystem (system:
+
let
+
pkgs = import nixpkgs { inherit system; };
+
blog = pkgs.stdenvNoCC.mkDerivation {
+
name = "hauleth-blog";
+
src = ./.;
+
+
nativeBuildInputs = [
+
pkgs.zola
+
];
+
+
buildPhase = ''
+
zola --version
+
zola build --output-dir $out
+
'';
+
+
dontInstall = true;
+
};
+
in
+
{
+
apps.publish = let
+
program = pkgs.writeShellScript "publish" ''
+
cp -r ${self.packages.${system}.blog} public
+
'';
+
in {
+
type = "app";
+
program = "${program}";
+
};
+
packages = {
+
inherit blog;
+
};
+
defaultPackage = blog;
+
+
devShells.default = pkgs.mkShell {
+
inputsFrom = [ blog ];
+
+
packages = [
+
# pkgs.netlify-cli
+
pkgs.vale
+
pkgs.mdl
+
];
+
};
+
}
+
);
+
}
+42 -2
netlify.toml
···
[[headers]]
for = "/*"
[headers.values]
Permission-Policy = "interest-cohort=()"
X-Frame-Options = "DENY"
X-XSS-Protection = "1; mode=block"
-
[context.deploy-preview]
-
command = "zola build --drafts"
[[redirects]]
from = "/post"
to = "/"
force = true
···
+
[build]
+
command = "zola build"
+
publish = "public/"
+
+
[build.environment]
+
ZOLA_VERSION = "0.20.0"
+
+
[context.deploy-preview]
+
command = "zola build --drafts --base-url $DEPLOY_PRIME_URL"
+
[[headers]]
for = "/*"
[headers.values]
+
# Disable Google cohort tracking
Permission-Policy = "interest-cohort=()"
+
# Disallow showing the website in frames
X-Frame-Options = "DENY"
X-XSS-Protection = "1; mode=block"
+
X-Content-Type-Options = "nosniff"
+
X-Clacks-Overhead = "GNU Terry Pratchett"
+
Content-Security-Policy = "default-src 'self'; script-src 'self'; connect-src 'self'; img-src https:"
+
Permissions-Policy = "accelerometer=(), ambient-light-sensor=(), autoplay=(), battery=(), camera=(), cross-origin-isolated=(), display-capture=(), document-domain=(), encrypted-media=(), execution-while-not-rendered=(), execution-while-out-of-viewport=(), fullscreen=(), geolocation=(), gyroscope=(), keyboard-map=(), magnetometer=(), microphone=(), midi=(), navigation-override=(), payment=(), picture-in-picture=(), publickey-credentials-get=(), screen-wake-lock=(), sync-xhr=(), usb=(), web-share=(), xr-spatial-tracking=(), clipboard-read=(), clipboard-write=(), gamepad=(), speaker-selection=()"
+
Referrer-Policy = "no-referrer-when-downgrade"
+
[[headers]]
+
for = "/.well-known/webfinger"
+
[headers.values]
+
Content-Type = "application/jrd+json; charset=utf-8"
[[redirects]]
from = "/post"
to = "/"
force = true
+
+
[[redirects]]
+
from = "/.well-known/webfinger"
+
to = "https://fosstodon.org/.well-known/webfinger?resource=acct:hauleth@fosstodon.org"
+
status = 200
+
+
[[redirects]]
+
from = "/js/script.js"
+
to = "https://plausible.io/js/script.js"
+
status = 200
+
+
[[redirects]]
+
from = "/api/event"
+
to = "https://plausible.io/api/event"
+
status = 200
+
+
[[plugins]]
+
package = "netlify-plugin-webmentions"
+
+
[plugins.inputs]
+
feedPath = "atom.xml"
-12
sass/_header.scss
···
@import "variables";
-
@mixin menu {
-
position: absolute;
-
background: var(--background);
-
box-shadow: var(--shadow);
-
color: white;
-
border: 2px solid;
-
margin: 0;
-
padding: 10px;
-
list-style: none;
-
z-index: 99;
-
}
-
.header {
@media print {
display: none;
···
@import "variables";
.header {
@media print {
display: none;
+62 -33
sass/_main.scss
···
@import "variables";
html {
box-sizing: border-box;
}
···
margin: 0;
padding: 0;
font-family: ui-monospace, monospace;
-
font-size: 12pt;
-
line-height: 1.54;
background-color: var(--background);
-
color: var(--color);
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-webkit-overflow-scrolling: touch;
-webkit-text-size-adjust: 100%;
-
-
@media print {
-
color: #000;
-
line-height: 1.2;
-
font-size: 10pt;
-
}
}
h1, h2, h3, h4, h5, h6 {
···
}
.zola-anchor {
-
font-size: 1.5rem;
visibility: hidden;
margin-left: 0.5rem;
vertical-align: 1%;
text-decoration: none;
border-bottom-color: transparent;
cursor: pointer;
@media (hover: none){
visibility: visible;
···
a {
color: inherit;
}
img {
···
padding: 1px 6px;
margin: 0 2px;
font-size: .95rem;
}
pre {
···
overflow: auto;
border-top: 1px solid rgba(255, 255, 255, .1);
border-bottom: 1px solid rgba(255, 255, 255, .1);
+ pre {
border-top: 0;
···
word-wrap: break-word;
}
code {
background: none !important;
margin: 0;
···
padding-right: 0;
}
-
&:before {
-
content: '”';
-
font-family: Georgia, serif;
-
font-size: 3.875rem;
-
position: absolute;
-
left: -40px;
-
top: -20px;
-
}
-
p:first-of-type {
-
margin-top: 0;
}
-
p:last-of-type {
margin-bottom: 0;
-
}
-
-
p {
-
position: relative;
-
}
-
-
p:before {
-
content: '>';
-
display: block;
-
position: absolute;
-
left: -25px;
-
color: var(--accent);
}
}
···
background: var(--border-color);
height: 1px;
}
···
@import "variables";
+
:root {
+
font-size: calc(1rem + 0.05vw);
+
line-height: 1.54;
+
color: var(--color);
+
+
@media print {
+
color: #000;
+
line-height: 1.2;
+
font-size: 10pt;
+
}
+
}
+
html {
box-sizing: border-box;
}
···
margin: 0;
padding: 0;
font-family: ui-monospace, monospace;
background-color: var(--background);
+
// text-shadow: 0 0 3px currentcolor;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-webkit-overflow-scrolling: touch;
-webkit-text-size-adjust: 100%;
}
h1, h2, h3, h4, h5, h6 {
···
}
.zola-anchor {
+
font-size: .75em;
visibility: hidden;
margin-left: 0.5rem;
vertical-align: 1%;
text-decoration: none;
border-bottom-color: transparent;
cursor: pointer;
+
color: var(--accent);
@media (hover: none){
visibility: visible;
···
a {
color: inherit;
+
+
&:hover {
+
color: var(--accent);
+
text-shadow: 0 0 .25em currentcolor;
+
};
}
img {
···
padding: 1px 6px;
margin: 0 2px;
font-size: .95rem;
+
hyphens: none;
}
pre {
···
overflow: auto;
border-top: 1px solid rgba(255, 255, 255, .1);
border-bottom: 1px solid rgba(255, 255, 255, .1);
+
position: relative;
+ pre {
border-top: 0;
···
word-wrap: break-word;
}
+
&[data-lang]::before {
+
content: attr(data-lang);
+
display: block;
+
position: absolute;
+
top: 0;
+
right: 0;
+
padding: .2em .5em;
+
font-weight: bold;
+
font-size: .95rem;
+
border-radius: 0 0 0 6px;
+
background-color: var(--accent-alpha-20);
+
}
+
code {
background: none !important;
margin: 0;
···
padding-right: 0;
}
+
> :first-child {
+
margin-top: 0;
+
position: relative;
+
&:before {
+
content: '>';
+
display: block;
+
position: absolute;
+
left: -25px;
+
top: .1em;
+
color: var(--accent);
+
}
}
+
> :last-child {
margin-bottom: 0;
}
}
···
background: var(--border-color);
height: 1px;
}
+
+
ol {
+
counter-reset: li;
+
list-style: none;
+
+
li {
+
counter-increment: li;
+
+
// Todo change it to ::marker when Safari will support it
+
&::before {
+
margin-left: -2rem;
+
content: counters(li, ".") ". ";
+
}
+
}
+
}
+39 -1
sass/_post.scss
···
.post {
width: 100%;
-
text-align: left;
margin: 20px auto;
padding: 20px 0 0 0;
@media (max-width: $tablet-max-width) {
margin: 0 auto;
···
padding: 1em;
margin: 2em 0;
}
···
.post {
width: 100%;
+
text-align: justify;
+
text-wrap: pretty;
+
hyphens: auto;
+
hyphenate-limit-chars: 10;
margin: 20px auto;
padding: 20px 0 0 0;
+
@media (max-width: $tablet-max-width) {
margin: 0 auto;
···
padding: 1em;
margin: 2em 0;
}
+
+
.webmentions .url-only {
+
line-break: anywhere;
+
}
+
+
.halmos {
+
text-align: right;
+
font-size: 1.5em;
+
}
+
+
.footnote-definition {
+
@media (min-width: #{$tablet-max-width + 1px}) {
+
position: absolute;
+
left: 105%;
+
+
width: 10vw;
+
+
margin-top: -7rem;
+
}
+
+
margin-top: 1rem;
+
+
font-size: .8em;
+
+
p {
+
padding-left: .5rem;
+
display: inline;
+
}
+
+
// For some reason `:last-of-type` doesn't work
+
&:has(+ .halmos) {
+
margin-bottom: -.5rem;
+
}
+
}
+40
sass/_rings.scss
···
···
+
.rings {
+
margin-top: 1rem;
+
text-align: center;
+
+
details > summary {
+
list-style: none;
+
cursor: pointer;
+
+
&::before, &::after {
+
margin: 0 .5rem;
+
}
+
+
&::before { content: '▶'; }
+
&::after { content: '◀'; }
+
+
&::-webkit-details-marker {
+
display: none;
+
}
+
}
+
+
details[open] {
+
summary {
+
&::before, &::after {
+
content: '▼';
+
}
+
+
margin-bottom: 1rem;
+
}
+
}
+
+
ul {
+
list-style: none;
+
margin: 0;
+
}
+
+
li {
+
margin: 0;
+
padding: 0;
+
}
+
}
+1 -1
sass/_variables.scss
···
$phone-max-width: 683px;
-
$tablet-max-width: 899px;
···
$phone-max-width: 683px;
+
$tablet-max-width: 1199px;
+22
sass/feed.scss
···
···
+
body {
+
color: #222;
+
font-family: apple-system, system-ui, sans-serif;
+
}
+
.container {
+
align-item: center;
+
display: flex;
+
justify-content: center;
+
}
+
.item {
+
max-width: 768px;
+
}
+
a {
+
color: #4166f5;
+
text-decoration: none;
+
}
+
a:visited {
+
color: #3f00ff;
+
}
+
a:hover {
+
text-decoration: underline;
+
}
+1
sass/style.scss
···
@import 'post';
@import 'pagination';
@import 'footer';
:root {
--phoneWidth: (max-width: #{$phone-max-width + 1px});
···
@import 'post';
@import 'pagination';
@import 'footer';
+
@import 'rings';
:root {
--phoneWidth: (max-width: #{$phone-max-width + 1px});
-14
shell.nix
···
-
{ pkgs ? import <nixpkgs> {}, ... }:
-
-
let
-
blog = import ./. {};
-
in
-
pkgs.mkShell {
-
buildInputs = [
-
blog.zola
-
pkgs.pandoc
-
pkgs.texlive.combined.scheme-small
-
pkgs.vale
-
pkgs.mdl
-
];
-
}
···
+9
static/_headers
···
···
+
*
+
Permission-Policy: interest-cohort=()
+
X-Frame-Options: DENY
+
X-XSS-Protection: 1; mode=block
+
X-Clacks-Overhead: GNU Terry Pratchet
+
X-Clacks-Overhead: GNU Joe Armstrong
+
Content-Security-Policy: default-src 'self'; script-src 'self'; connect-src 'self'; img-src 'self'; frame-src 'none'; frame-ancestors 'none'; form-action 'none'; base-uri 'self';
+
Permissions-Policy: accelerometer=(), ambient-light-sensor=(), autoplay=(), battery=(), camera=(), cross-origin-isolated=(), display-capture=(), document-domain=(), encrypted-media=(), execution-while-not-rendered=(), execution-while-out-of-viewport=(), fullscreen=(), geolocation=(), gyroscope=(), keyboard-map=(), magnetometer=(), microphone=(), midi=(), navigation-override=(), payment=(), picture-in-picture=(), publickey-credentials-get=(), screen-wake-lock=(), sync-xhr=(), usb=(), web-share=(), xr-spatial-tracking=(), clipboard-read=(), clipboard-write=(), gamepad=(), speaker-selection=()
+
Referrer-Policy: strict-origin-when-cross-origin
+3
static/_redirects
···
···
+
/js/script.js https://plausible.io/js/script.js 200
+
/api/event https://plausible.io/api/event 200
+
/.well-known/webfinger https://fosstodon.org/.well-known/webfinger?resource=acct:hauleth@fosstodon.org 200
static/android-chrome-192x192.png

This is a binary file and will not be displayed.

static/android-chrome-512x512.png

This is a binary file and will not be displayed.

static/apple-touch-icon.png

This is a binary file and will not be displayed.

+58
static/atom-style.xml
···
···
+
<?xml version="1.0" encoding="utf-8"?>
+
<xsl:stylesheet version="3.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd">
+
<xsl:output method="html" encoding="UTF-8" />
+
<xsl:template match="/atom:feed">
+
<html lang="en" dir="ltr">
+
<head>
+
<title><xsl:value-of select="atom:title"/> - RSS Feed</title>
+
<meta charset="UTF-8" />
+
<meta http-equiv="x-ua-compatible" content="IE=edge,chrome=1" />
+
<meta http-equiv="content-language" content="en_US" />
+
<meta name="viewport" content="width=device-width,minimum-scale=1,initial-scale=1,shrink-to-fit=no" />
+
<meta name="referrer" content="none" />
+
+
<!-- FAVICONS CAN GO HERE -->
+
<link href="/feed.css" rel="stylesheet" />
+
</head>
+
<body>
+
<div class="container">
+
<div class="item">
+
<header>
+
<h1>RSS Feed</h1>
+
<h2>
+
<xsl:value-of select="atom:title"/>
+
</h2>
+
<p>
+
<xsl:value-of select="atom:subtitle"/>
+
</p>
+
<a hreflang="en" target="_blank">
+
<xsl:attribute name="href">
+
<xsl:value-of select="atom:link[@rel='related']/@href"/>
+
</xsl:attribute>
+
Visit Website &#x2192;
+
</a>
+
</header>
+
<main>
+
<h2>Recent Posts</h2>
+
<xsl:for-each select="atom:entry">
+
<article>
+
<h3>
+
<a hreflang="en" target="_blank">
+
<xsl:attribute name="href">
+
<xsl:value-of select="atom:link/@href"/>
+
</xsl:attribute>
+
<xsl:value-of select="atom:title"/>
+
</a>
+
</h3>
+
<p>
+
<xsl:value-of select="atom:summary"/>
+
</p>
+
</article>
+
</xsl:for-each>
+
</main>
+
</div>
+
</div>
+
</body>
+
</html>
+
</xsl:template>
+
</xsl:stylesheet>
static/banner.png

This is a binary file and will not be displayed.

static/favicon-16x16.png

This is a binary file and will not be displayed.

static/favicon-32x32.png

This is a binary file and will not be displayed.

static/favicon-48x48.png

This is a binary file and will not be displayed.

static/favicon.ico

This is a binary file and will not be displayed.

+17
static/favicon.svg
···
···
+
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+
<svg width="100%" height="100%" viewBox="0 0 32 32" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-miterlimit:2;">
+
<g transform="matrix(1,0,0,1,-16,0)">
+
<g transform="matrix(2.62238,0,0,1.40473,7.10543e-15,1.77636e-15)">
+
<rect x="0" y="0" width="24.405" height="22.78" style="fill:rgb(35,176,255);"/>
+
</g>
+
<g id="Lambda" transform="matrix(1,0,0,1,16,0)">
+
<g transform="matrix(1,0,0,1,4.43135,0)">
+
<path d="M2.644,26.033L5.977,26.033L15.556,5.994L18.545,5.994" style="fill:none;stroke:black;stroke-width:3px;"/>
+
</g>
+
<g transform="matrix(0.990948,-0.019459,-0.019459,0.958168,4.84334,0.885678)">
+
<path d="M19.831,26.648L16.814,26.587L10.763,16.007" style="fill:none;stroke:black;stroke-width:3.14px;"/>
+
</g>
+
</g>
+
</g>
+
</svg>
+441
static/syntax-theme.css
···
···
+
/*
+
* theme "Nord" generated by syntect
+
*/
+
+
.z-code {
+
color: #d8dee9;
+
background-color: #2e3440;
+
}
+
+
.z-comment, .z-punctuation.z-definition.z-comment {
+
color: #616e88;
+
}
+
.z-constant.z-numeric {
+
color: #b48ead;
+
}
+
.z-constant.z-language {
+
color: #81a1c1;
+
}
+
.z-constant.z-character.z-escape {
+
color: #ebcb8b;
+
}
+
.z-constant.z-other.z-placeholder {
+
color: #ebcb8b;
+
}
+
.z-constant.z-other {
+
color: #d8dee9;
+
}
+
.z-entity.z-name.z-class {
+
color: #8fbcbb;
+
}
+
.z-entity.z-name.z-struct {
+
color: #8fbcbb;
+
}
+
.z-entity.z-name.z-enum {
+
color: #8fbcbb;
+
}
+
.z-entity.z-name.z-union {
+
color: #8fbcbb;
+
}
+
.z-entity.z-name.z-trait {
+
color: #8fbcbb;
+
}
+
.z-entity.z-name.z-interface {
+
color: #8fbcbb;
+
font-weight: bold;
+
}
+
.z-entity.z-name.z-type {
+
color: #81a1c1;
+
}
+
.z-entity.z-other.z-inherited-class {
+
color: #8fbcbb;
+
}
+
.z-entity.z-name.z-function {
+
color: #88c0d0;
+
}
+
.z-entity.z-name.z-function.z-constructor {
+
color: #88c0d0;
+
}
+
.z-entity.z-name.z-function.z-destructor {
+
color: #88c0d0;
+
}
+
.z-entity.z-name.z-namespace {
+
color: #8fbcbb;
+
}
+
.z-entity.z-name.z-constant {
+
color: #81a1c1;
+
}
+
.z-entity.z-name.z-label {
+
color: #5e81ac;
+
}
+
.z-entity.z-name.z-section {
+
color: #88c0d0;
+
}
+
.z-entity.z-name.z-tag {
+
color: #81a1c1;
+
}
+
.z-entity.z-other.z-attribute-name {
+
color: #8fbcbb;
+
}
+
.z-invalid.z-illegal {
+
color: #d8dee9;
+
background-color: #bf616a;
+
}
+
.z-invalid.z-deprecated {
+
color: #d8dee9;
+
background-color: #ebcb8b;
+
}
+
.z-keyword.z-control {
+
color: #81a1c1;
+
}
+
.z-keyword.z-control.z-conditional {
+
color: #81a1c1;
+
}
+
.z-keyword.z-control.z-import {
+
color: #81a1c1;
+
}
+
.z-punctuation.z-definition.z-keyword {
+
color: #81a1c1;
+
}
+
.z-keyword.z-other {
+
color: #81a1c1;
+
}
+
.z-keyword.z-operator, .z-keyword.z-operator.z-assignment, .z-keyword.z-operator.z-arithmetic, .z-keyword.z-operator.z-bitwise, .z-keyword.z-operator.z-logical, .z-keyword.z-operator.z-word {
+
color: #81a1c1;
+
}
+
.z-markup.z-heading {
+
color: #88c0d0;
+
}
+
.z-markup.z-list.z-unnumbered, .z-markup.z-list.z-numbered {
+
color: #d8dee9;
+
}
+
.z-markup.z-bold {
+
font-weight: bold;
+
}
+
.z-markup.z-italic {
+
font-style: italic;
+
}
+
.z-markup.z-inserted {
+
color: #a3be8c;
+
}
+
.z-markup.z-deleted {
+
color: #bf616a;
+
}
+
.z-markup.z-changed {
+
color: #ebcb8b;
+
}
+
.z-markup.z-quote {
+
color: #616e88;
+
}
+
.z-markup.z-raw.z-inline, .z-markup.z-raw.z-block {
+
color: #8fbcbb;
+
}
+
.z-markup.z-other {
+
background-color: #eceff4;
+
}
+
.z-punctuation.z-separator, .z-punctuation.z-terminator {
+
color: #eceff4;
+
}
+
.z-punctuation.z-separator.z-continuation, .z-punctuation.z-accessor {
+
color: #81a1c1;
+
}
+
.z-punctuation.z-definition.z-tag {
+
color: #81a1c1;
+
}
+
.z-storage.z-type, .z-storage.z-modifier {
+
color: #81a1c1;
+
}
+
.z-string.z-quoted.z-single, .z-string.z-quoted.z-double, .z-string.z-quoted.z-triple, .z-string.z-quoted.z-other, .z-string.z-unquoted {
+
color: #a3be8c;
+
}
+
.z-punctuation.z-definition.z-string.z-begin, .z-punctuation.z-definition.z-string.z-end {
+
color: #a3be8c;
+
}
+
.z-string.z-regexp {
+
color: #ebcb8b;
+
}
+
.z-support.z-constant {
+
color: #8fbcbb;
+
}
+
.z-support.z-function {
+
color: #88c0d0;
+
}
+
.z-support.z-module {
+
color: #8fbcbb;
+
}
+
.z-support.z-type {
+
color: #8fbcbb;
+
}
+
.z-support.z-class {
+
color: #8fbcbb;
+
}
+
.z-text.z-html.z-basic, .z-text.z-xml {
+
color: #eceff4;
+
}
+
.z-variable.z-other, .z-variable.z-other.z-readwrite {
+
color: #d8dee9;
+
}
+
.z-punctuation.z-definition.z-variable {
+
color: #81a1c1;
+
}
+
.z-variable.z-other.z-constant {
+
color: #81a1c1;
+
}
+
.z-variable.z-language {
+
color: #81a1c1;
+
}
+
.z-variable.z-parameter {
+
color: #d8dee9;
+
}
+
.z-variable.z-other.z-member {
+
color: #d8dee9;
+
}
+
.z-variable.z-function {
+
color: #88c0d0;
+
}
+
.z-constant.z-numeric.z-line-number.z-find-in-files {
+
color: #d8dee9;
+
}
+
.z-constant.z-numeric.z-line-number.z-match.z-find-in-files {
+
color: #88c0d0;
+
}
+
.z-entity.z-name.z-filename.z-find-in-files {
+
color: #8fbcbb;
+
}
+
.z-text.z-find-in-files {
+
color: #d8dee9;
+
}
+
.z-source.z-c .z-keyword.z-control.z-import.z-include.z-c, .z-source.z-c .z-meta.z-preprocessor.z-c .z-keyword.z-control.z-import.z-c, .z-source.z-c++ .z-keyword.z-control.z-import.z-include.z-c++, .z-source.z-c++ .z-meta.z-preprocessor.z-c++ .z-keyword.z-control.z-import.z-c++ {
+
color: #5e81ac;
+
}
+
.z-source.z-c .z-keyword.z-operator.z-word.z-c, .z-source.z-c++ .z-keyword.z-operator.z-word.z-c++ {
+
color: #88c0d0;
+
font-style: italic;
+
}
+
.z-source.z-css .z-support.z-type.z-property-name.z-css, .z-source.z-sass .z-support.z-type.z-property-name.z-sass, .z-source.z-css .z-support.z-type.z-vendor-prefix.z-css {
+
color: #d8dee9;
+
}
+
.z-source.z-css .z-constant.z-other.z-color.z-rgb-value.z-css, .z-source.z-sass .z-constant.z-other.z-rgb-value.z-sass {
+
color: #b48ead;
+
}
+
.z-source.z-css .z-support.z-constant.z-color.z-w3c-special-color-keyword.z-css, .z-source.z-css .z-support.z-constant.z-property-value.z-css, .z-source.z-sass .z-support.z-constant.z-property-value.z-sass {
+
color: #81a1c1;
+
}
+
.z-source.z-css .z-entity.z-other.z-pseudo-class.z-css {
+
color: #8fbcbb;
+
}
+
.z-source.z-css .z-keyword.z-control.z-at-rule.z-media.z-css, .z-source.z-css .z-punctuation.z-definition.z-keyword.z-css, .z-source.z-sass .z-keyword.z-control.z-at-rule.z-sass {
+
color: #d08770;
+
}
+
.z-source.z-css .z-support.z-constant.z-color.z-w3c-standard-color-name.z-css {
+
color: #81a1c1;
+
font-style: italic;
+
}
+
.z-source.z-css .z-entity.z-other.z-attribute-name.z-id.z-css, .z-source.z-sass .z-entity.z-other.z-attribute-name.z-id.z-sass {
+
font-weight: bold;
+
}
+
.z-source.z-diff .z-meta.z-diff.z-range.z-context {
+
color: #8fbcbb;
+
}
+
.z-source.z-diff .z-meta.z-diff.z-header.z-from-file {
+
color: #8fbcbb;
+
}
+
.z-source.z-diff .z-punctuation.z-definition.z-from-file {
+
color: #8fbcbb;
+
}
+
.z-source.z-diff .z-punctuation.z-definition.z-separator {
+
color: #81a1c1;
+
}
+
.z-source.z-go .z-support.z-function.z-builtin.z-go {
+
font-style: italic;
+
}
+
.z-text.z-html.z-basic .z-constant.z-character.z-entity.z-html {
+
color: #ebcb8b;
+
}
+
.z-variable.z-annotation.z-java, .z-punctuation.z-definition.z-annotation.z-java {
+
color: #d08770;
+
}
+
.z-punctuation.z-accessor.z-dot.z-java {
+
color: #eceff4;
+
}
+
.z-support.z-other.z-package.z-java {
+
color: #8fbcbb;
+
}
+
.z-source.z-java .z-comment.z-block.z-documentation.z-javadoc .z-punctuation.z-definition.z-entity.z-html {
+
color: #81a1c1;
+
}
+
.z-source.z-java .z-entity.z-name.z-constant {
+
color: #d8dee9;
+
}
+
.z-source.z-java .z-keyword.z-other.z-documentation, .z-source.z-java .z-comment.z-block.z-documentation.z-javadoc .z-punctuation.z-definition.z-keyword.z-javadoc {
+
color: #8fbcbb;
+
}
+
.z-source.z-java .z-variable.z-parameter.z-type.z-java {
+
color: #8fbcbb;
+
}
+
.z-source.z-java .z-entity.z-other.z-inherited-class.z-java {
+
color: #8fbcbb;
+
font-weight: bold;
+
}
+
.z-source.z-java .z-keyword.z-declaration.z-implements.z-java {
+
color: #81a1c1;
+
}
+
.z-source.z-json .z-meta.z-structure.z-dictionary.z-json .z-meta.z-structure.z-dictionary.z-key.z-json .z-string.z-quoted.z-double.z-json, .z-source.z-json.z-sublime .z-meta.z-structure.z-dictionary.z-json .z-meta.z-structure.z-dictionary.z-key.z-json .z-string.z-quoted.z-double.z-json {
+
color: #8fbcbb;
+
}
+
.z-source.z-json .z-meta.z-structure.z-dictionary.z-json .z-punctuation.z-definition.z-string.z-begin.z-json, .z-source.z-json .z-meta.z-structure.z-dictionary.z-json .z-punctuation.z-definition.z-string.z-end.z-json, .z-source.z-json.z-sublime .z-meta.z-structure.z-dictionary.z-json .z-punctuation.z-definition.z-string.z-begin.z-json, .z-source.z-json.z-sublime .z-meta.z-structure.z-dictionary.z-json .z-punctuation.z-definition.z-string.z-end.z-json {
+
color: #eceff4;
+
}
+
.z-text.z-html.z-markdown .z-punctuation.z-definition.z-raw.z-code-fence.z-begin.z-markdown, .z-text.z-html.z-markdown .z-punctuation.z-definition.z-raw.z-code-fence.z-end.z-markdown, .z-text.z-html.z-markdown .z-markup.z-raw.z-code-fence.z-markdown .z-constant.z-other.z-language-name.z-markdown {
+
color: #8fbcbb;
+
}
+
.z-text.z-html.z-markdown .z-punctuation.z-definition.z-list_item.z-markdown, .z-text.z-html.z-markdown .z-markup.z-list.z-numbered.z-bullet.z-markdown {
+
color: #81a1c1;
+
}
+
.z-text.z-html.z-markdown .z-markup.z-quote.z-markdown .z-punctuation.z-definition.z-blockquote.z-markdown {
+
color: #8fbcbb;
+
}
+
.z-text.z-html.z-markdown .z-meta.z-link.z-inline.z-description.z-markdown, .z-text.z-html.z-markdown .z-meta.z-image.z-inline.z-description.z-markdown, .z-text.z-html.z-markdown .z-meta.z-link.z-reference.z-description.z-markdown, .z-text.z-html.z-markdown .z-constant.z-other.z-reference.z-link.z-markdown, .z-text.z-html.z-markdown .z-entity.z-name.z-reference.z-link.z-markdown {
+
color: #88c0d0;
+
}
+
.z-text.z-html.z-markdown .z-punctuation.z-definition.z-link.z-begin.z-markdown, .z-text.z-html.z-markdown .z-punctuation.z-definition.z-link.z-end.z-markdown, .z-text.z-html.z-markdown .z-punctuation.z-definition.z-image.z-begin.z-markdown, .z-text.z-html.z-markdown .z-punctuation.z-definition.z-image.z-end.z-markdown, .z-text.z-html.z-markdown .z-punctuation.z-definition.z-constant.z-begin.z-markdown, .z-text.z-html.z-markdown .z-punctuation.z-definition.z-constant.z-end.z-markdown {
+
color: #81a1c1;
+
}
+
.z-text.z-html.z-markdown .z-punctuation.z-separator.z-table-cell.z-markdown, .z-text.z-html.z-markdown .z-punctuation.z-section.z-table-header.z-markdown {
+
color: #81a1c1;
+
}
+
.z-text.z-html.z-markdown .z-meta.z-table.z-header.z-markdown {
+
color: #88c0d0;
+
}
+
.z-text.z-html.z-markdown .z-punctuation.z-definition.z-thematic-break.z-markdown {
+
color: #81a1c1;
+
font-weight: bold;
+
}
+
.z-embedding.z-php .z-text.z-html.z-basic .z-punctuation.z-section.z-embedded.z-begin.z-php, .z-embedding.z-php .z-text.z-html.z-basic .z-punctuation.z-section.z-embedded.z-end.z-php {
+
color: #5e81ac;
+
}
+
.z-embedding.z-php .z-text.z-html.z-basic .z-comment.z-block.z-documentation.z-phpdoc.z-php .z-keyword.z-other.z-phpdoc.z-php {
+
color: #8fbcbb;
+
}
+
.z-embedding.z-php .z-text.z-html.z-basic .z-support.z-other.z-namespace.z-php {
+
color: #8fbcbb;
+
}
+
.z-source.z-python .z-meta.z-annotation.z-python .z-meta.z-qualified-name.z-python .z-support.z-function.z-builtin.z-python, .z-source.z-python .z-punctuation.z-definition.z-annotation.z-python {
+
color: #d08770;
+
}
+
.z-support.z-function.z-builtin.z-python {
+
font-style: italic;
+
}
+
.z-source.z-python .z-support.z-type.z-exception.z-python {
+
color: #8fbcbb;
+
}
+
.z-source.z-python .z-support.z-type.z-python {
+
color: #81a1c1;
+
}
+
.z-source.z-sass .z-variable.z-parameter.z-sass {
+
color: #d8dee9;
+
font-weight: bold;
+
}
+
.z-source.z-ruby .z-support.z-function.z-builtin.z-ruby {
+
font-style: italic;
+
}
+
.z-source.z-shell .z-support.z-function.z-builtin.z-shell {
+
font-style: italic;
+
}
+
.z-text.z-xml .z-entity.z-name.z-tag.z-namespace {
+
color: #8fbcbb;
+
}
+
.z-text.z-xml .z-meta.z-tag.z-sgml.z-doctype.z-xml, .z-text.z-xml .z-meta.z-tag.z-preprocessor .z-entity.z-name.z-tag, .z-text.z-xml .z-meta.z-tag.z-preprocessor.z-xml .z-punctuation.z-definition.z-tag.z-begin.z-xml, .z-text.z-xml .z-meta.z-tag.z-preprocessor.z-xml .z-punctuation.z-definition.z-tag.z-end.z-xml, .z-text.z-xml .z-meta.z-tag.z-sgml.z-doctype.z-xml .z-punctuation.z-definition.z-tag.z-begin.z-xml, .z-text.z-xml .z-meta.z-tag.z-sgml.z-doctype.z-xml .z-punctuation.z-definition.z-tag.z-end.z-xml {
+
color: #5e81ac;
+
}
+
.z-text.z-xml .z-string.z-unquoted.z-cdata.z-xml .z-punctuation.z-definition.z-string.z-begin.z-xml, .z-text.z-xml .z-string.z-unquoted.z-cdata.z-xml .z-punctuation.z-definition.z-string.z-end.z-xml {
+
color: #d08770;
+
font-style: italic;
+
}
+
.z-source.z-yaml .z-entity.z-name.z-tag {
+
color: #8fbcbb;
+
}
+
.z-sublimelinter.z-mark.z-warning {
+
color: #ebcb8b;
+
}
+
.z-sublimelinter.z-mark.z-error {
+
color: #bf616a;
+
}
+
.z-markup.z-ignored.z-git_gutter {
+
color: #616e88;
+
}
+
.z-markup.z-untracked.z-git_gutter {
+
color: #88c0d0;
+
}
+
.z-markup.z-inserted.z-git_gutter {
+
color: #a3be8c;
+
}
+
.z-markup.z-changed.z-git_gutter {
+
color: #ebcb8b;
+
}
+
.z-markup.z-deleted.z-git_gutter {
+
color: #bf616a;
+
}
+
.z-source.z-js .z-tag.z-decorator.z-js .z-entity.z-name.z-tag.z-js, .z-source.z-js .z-tag.z-decorator.z-js .z-punctuation.z-definition.z-tag.z-js {
+
color: #d08770;
+
}
+
.z-source.z-js .z-string.z-quoted.z-js, .z-source.z-js .z-string.z-quoted.z-jsx, .z-source.z-js .z-meta.z-class.z-js .z-meta.z-class.z-property.z-js .z-string.z-interpolated.z-js, .z-source.z-js .z-string.z-interpolated.z-js, .z-source.z-js .z-string.z-template.z-js {
+
color: #a3be8c;
+
}
+
.z-source.z-js .z-variable.z-other.z-constant.z-js {
+
color: #d8dee9;
+
font-weight: bold;
+
}
+
.z-source.z-js .z-constant.z-other.z-object.z-key.z-js .z-string.z-unquoted.z-label.z-js, .z-source.z-js .z-variable.z-other.z-readwrite.z-js {
+
color: #d8dee9;
+
}
+
.z-source.z-js .z-meta.z-class.z-js .z-meta.z-class.z-property.z-js .z-variable.z-other.z-property.z-js {
+
color: #88c0d0;
+
}
+
.z-source.z-js .z-variable.z-other.z-property.z-static.z-js {
+
font-style: italic;
+
}
+
.z-source.z-js .z-variable.z-other.z-class.z-js {
+
color: #8fbcbb;
+
}
+
.z-source.z-js .z-string.z-interpolated.z-js .z-keyword.z-other.z-substitution.z-begin.z-js, .z-source.z-js .z-string.z-interpolated.z-js .z-keyword.z-other.z-substitution.z-end.z-js, .z-source.z-js .z-punctuation.z-definition.z-template-expression.z-begin.z-js, .z-source.z-js .z-punctuation.z-definition.z-template-expression.z-end.z-js {
+
color: #5e81ac;
+
}
+
.z-source.z-js .z-keyword.z-operator.z-spread.z-js {
+
color: #81a1c1;
+
}
+
.z-source.z-js .z-meta.z-export.z-js .z-variable.z-other.z-readwrite.z-js {
+
color: #8fbcbb;
+
}
+
.z-source.z-ts .z-meta.z-class.z-ts .z-entity.z-other.z-inherited-class.z-ts {
+
color: #8fbcbb;
+
font-weight: bold;
+
}
+
.z-source.z-ts .z-entity.z-name.z-type.z-class.z-ts, .z-source.z-ts .z-entity.z-name.z-type.z-enum.z-ts {
+
color: #8fbcbb;
+
}
+
.z-source.z-ts .z-entity.z-name.z-type.z-ts {
+
color: #8fbcbb;
+
}
+
.z-source.z-ts .z-support.z-type.z-primitive.z-ts {
+
color: #81a1c1;
+
}
+
.z-source.z-ts .z-meta.z-class.z-ts .z-meta.z-method.z-declaration.z-ts .z-meta.z-parameters.z-ts .z-meta.z-decorator.z-ts .z-variable.z-other.z-readwrite.z-ts, .z-source.z-ts .z-meta.z-class.z-ts .z-meta.z-method.z-declaration.z-ts .z-meta.z-parameters.z-ts .z-meta.z-decorator.z-ts .z-punctuation.z-decorator.z-ts {
+
color: #d08770;
+
}
+
.z-source.z-ts .z-punctuation.z-definition.z-template-expression.z-begin.z-ts, .z-source.z-ts .z-punctuation.z-definition.z-template-expression.z-end.z-ts {
+
color: #5e81ac;
+
}
+
.z-source.z-ts .z-string.z-template.z-ts {
+
color: #a3be8c;
+
}
+
.z-source.z-ts .z-support.z-function.z-math.z-ts {
+
font-style: italic;
+
}
+
.z-source.z-ts .z-variable.z-other.z-constant.z-property.z-ts, .z-source.z-ts .z-variable.z-other.z-enummember.z-ts, .z-source.z-ts .z-variable.z-other.z-constant.z-ts {
+
color: #d8dee9;
+
font-weight: bold;
+
}
+
.z-source.z-ts .z-comment.z-block.z-documentation.z-ts .z-storage.z-type.z-class.z-jsdoc {
+
color: #8fbcbb;
+
}
+9
statichost.yml
···
···
+
image: ghcr.io/getzola/zola:v0.21.0
+
command: build
+
public: public
+
image_entrypoint: true
+
+
# TODO: Usa nix there
+
# image: ghcr.io/lix-project/lix:latest
+
# command: nix --extra-experimental-features nix-command --extra-experimental-features flakes run .#publish
+
# public: public
+2
styles/markdown.rb
···
all
exclude_rule 'MD002'
exclude_rule 'MD041'
···
all
exclude_rule 'MD002'
exclude_rule 'MD041'
+
+
rule 'MD013', ignore_code_blocks: true
+1
templates/anchor-link.html
···
···
+
<a class="zola-anchor" href="#{{ id }}" aria-label="Anchor link for: {{ id }}">#</a>
+40
templates/atom.xml
···
···
+
<?xml version="1.0" encoding="UTF-8"?>
+
<?xml-stylesheet type="text/xsl" href="/atom-style.xml"?>
+
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="{{ lang }}">
+
<title>{{ config.title }}
+
{%- if term %} - {{ term.name }}
+
{%- elif section.title %} - {{ section.title }}
+
{%- endif -%}
+
</title>
+
{%- if config.description %}
+
<subtitle>{{ config.description }}</subtitle>
+
{%- endif %}
+
<link href="{{ feed_url | safe }}" rel="self" type="application/atom+xml"/>
+
<link href="
+
{%- if section -%}
+
{{ section.permalink | escape_xml | safe }}
+
{%- else -%}
+
{{ config.base_url | escape_xml | safe }}
+
{%- endif -%}
+
" rel="related"/>
+
<generator uri="https://www.getzola.org/">Zola</generator>
+
<updated>{{ last_updated | date(format="%+") }}</updated>
+
<id>{{ feed_url | safe }}</id>
+
{%- for page in pages %}
+
<entry xml:lang="{{ page.lang }}">
+
<title>{{ page.title }}</title>
+
<published>{{ page.date | date(format="%+") }}</published>
+
<updated>{{ page.updated | default(value=page.date) | date(format="%+") }}</updated>
+
<link href="{{ page.permalink | safe }}" type="text/html"/>
+
<id>{{ page.permalink | safe }}</id>
+
<summary>
+
{%- if page.description -%}
+
{{ page.description }}
+
{%- elif page.summary -%}
+
{{ page.summary | safe }}
+
{%- endif -%}
+
</summary>
+
<content type="html">{{ page.content }}</content>
+
</entry>
+
{%- endfor %}
+
</feed>
+61 -6
templates/index.html
···
{% extends "zerm/templates/index.html" %}
{% block fonts %}
-
{% endblock %}
{% block rss %}
{%- if config.generate_feed -%}
-
<link rel="alternate" type="application/atom+xml" title="{{ config.title }} Feed" href="{{ get_url(path=config.feed_filename) | safe}}">
{%- endif -%}
-
{% endblock %}
{% block copyright %}
<div class="copyright">
<div class="copyright--user">{{ config.extra.copyright | safe }}</div>
<div class="copyright--tracking">
-
Public tracking available at <a href="https://plausible.io/hauleth.dev">Plausible.io</a>
</div>
</div>
{% endblock copyright %}
{% block script %}
-
<script async defer data-domain="hauleth.dev" src="https://plausible.io/js/plausible.js"></script>
{% endblock script %}
{% block css %}
<link rel="stylesheet" type="text/css" href="/style.css" />
{% if config.extra.theme_color != "orange" -%}
{% set color = "/color/" ~ config.extra.theme_color ~ ".css" -%}
<link rel="stylesheet" type="text/css" href="{{ color }}" />
{%- else -%}
<link rel="stylesheet" type="text/css" href="/color/orange.css" />
{% endif %}
{% endblock css %}
{% block header %}
···
</ul>
</nav>
</header>
-
{% endblock %}
···
{% extends "zerm/templates/index.html" %}
{% block fonts %}
+
{% endblock fonts %}
{% block rss %}
{%- if config.generate_feed -%}
+
<link rel="alternate" type="application/atom+xml" title="{{ config.title }} Feed" href="{{ get_url(path="atom.xml", trailing_slash=false) }}">
+
{%- endif -%}
+
{% endblock rss %}
+
+
{% block og_preview %}
+
{%- if section -%}
+
<link rel="canonical" href="{{ section.permalink }}" />
+
{%- elif page -%}
+
<link rel="canonical" href="{{ page.permalink }}" />
+
{%- else -%}
+
<link rel="canonical" href="{{ current_url }}" />
{%- endif -%}
+
{{ social::og_preview() }}
+
+
{%- if config.extra.twitter.site -%}
+
<meta name="twitter:site" content="{{ config.extra.twitter.site }}" />
+
{%- endif -%}
+
{%- if config.extra.twitter.creator -%}
+
<meta name="twitter:creator" content="{{ config.extra.twitter.creator }}" />
+
{%- endif -%}
+
+
{%- if config.extra.webmention -%}
+
<link rel="webmention" href="{{ config.extra.webmention }}" >
+
{%- endif -%}
+
{% endblock og_preview %}
{% block copyright %}
<div class="copyright">
<div class="copyright--user">{{ config.extra.copyright | safe }}</div>
<div class="copyright--tracking">
+
public tracking available at <a href="https://plausible.io/hauleth.dev">Plausible.io</a>
+
</div>
+
<div class="copyright--source">
+
<a href="{{ config.extra.source }}">source code</a>
</div>
</div>
{% endblock copyright %}
{% block script %}
+
<script defer data-domain="hauleth.dev" src="/js/script.js"></script>
{% endblock script %}
{% block css %}
<link rel="stylesheet" type="text/css" href="/style.css" />
+
<link rel="stylesheet" type="text/css" href="/syntax-theme.css" />
{% if config.extra.theme_color != "orange" -%}
{% set color = "/color/" ~ config.extra.theme_color ~ ".css" -%}
<link rel="stylesheet" type="text/css" href="{{ color }}" />
{%- else -%}
<link rel="stylesheet" type="text/css" href="/color/orange.css" />
{% endif %}
+
<meta name="theme-color" content="#1d1e28" />
{% endblock css %}
{% block header %}
···
</ul>
</nav>
</header>
+
{% endblock header %}
+
+
{% block general_meta %}
+
<meta http-equiv="content-type" content="text/html; charset=utf-8">
+
<meta name="viewport" content="width=device-width, initial-scale=1">
+
{%- if page.title -%}
+
<meta name="description" content="{{ config.description }} {{ page.title }} {{ page.description }}"/>
+
{%- else -%}
+
<meta name="description" content="{{ config.description }}"/>
+
{%- endif -%}
+
+
{%- if page.taxonomies.tags or page.taxonomies.categories -%}
+
<meta name="keywords" content="
+
{%- if page.taxonomies.categories -%}
+
{%- for cat in page.taxonomies.categories -%}
+
{{ cat }}, {% endfor -%}
+
{%- endif -%}
+
+
{%- if page.taxonomies.tags -%}
+
{%- for tag in page.taxonomies.tags -%}
+
{%- if loop.last -%}
+
{{ tag }}
+
{%- else -%}
+
{{ tag }}, {% endif -%}
+
{%- endfor -%}
+
{%- endif -%}
+
" />
+
{%- endif -%}
+
{% endblock general_meta %}
+16
templates/macros/extended_footer.html
···
···
+
{% macro extended_footer() %}
+
<div class="rings">
+
<details>
+
<summary>Webrings</summary>
+
<ul>
+
{%- for ring in config.extra.webrings -%}
+
<li>
+
<a href="{{ ring.url }}/prev?referrer={{ get_url(path = "/") | safe }}">&laquo;</a>
+
<a href="{{ ring.url }}">{{ ring.name }}</a>
+
<a href="{{ ring.url }}/next?referrer={{ get_url(path = "/") | safe }}">&raquo;</a>
+
</li>
+
{%- endfor -%}
+
</ul>
+
</details>
+
</div>
+
{% endmacro extended_footer %}
+15
templates/macros/posts.html
···
[Updated: <time class="dt-updated" datetime="{{ page.updated }}">{{ page.updated | date(format="%Y.%m.%d") }}</time>]
{%- endif -%}
</span>
{{ posts::taxonomies(taxonomy=page.taxonomies,
disp_cat=config.extra.show_categories,
···
#<a class="p-category" href="{{get_taxonomy_url(kind="tags", name=tag )}}">{{ tag }}</a>
{% endfor -%}
{% endmacro tags %}
···
[Updated: <time class="dt-updated" datetime="{{ page.updated }}">{{ page.updated | date(format="%Y.%m.%d") }}</time>]
{%- endif -%}
</span>
+
::
+
<time datetime="P{{ page.reading_time }}M">{{ page.reading_time }} min</time>
{{ posts::taxonomies(taxonomy=page.taxonomies,
disp_cat=config.extra.show_categories,
···
#<a class="p-category" href="{{get_taxonomy_url(kind="tags", name=tag )}}">{{ tag }}</a>
{% endfor -%}
{% endmacro tags %}
+
+
{% macro thanks(who) %}
+
{%- if who is object -%}
+
{%- if who.url -%}
+
<a class="u-url p-name" href="{{ who.url }}">{{ who.name }}</a>
+
{%- else -%}
+
<span class="p-name">{{ who.name }}</span>
+
{%- endif -%}
+
{%- if who.why %} for {{ who.why }}{%- endif -%}
+
{%- else -%}
+
<span class="p-name">{{ who }}</span>
+
{%- endif -%}
+
{% endmacro %}
+27 -13
templates/page.html
···
{%- block main -%}
<article class="post h-entry">
<h1 class="post-title p-name">
-
<a href="{{ page.permalink }}">{{ page.title }}</a>
</h1>
{{ posts::meta(page=page, author=config.extra.show_author) }}
{%- if page.toc | length >= 5 -%}
···
{%- endif -%}
<div class="post-content e-content">
{{ page.content | safe }}
</div>
{%- if config.extra.for_hire -%}
<div class="for-hire">
···
{%- if page.extra.thanks -%}
<hr />
<p>
-
<b>Special thanks</b>:
<ul>
{%- for person in page.extra.thanks -%}
-
<li class="h-card">
-
{%- if person is object -%}
-
{%- if person.url -%}
-
<a class="u-url p-name" href="{{ person.url }}">{{ person.name }}</a>
-
{%- else -%}
-
<span class="p-name">{{ person.name }}</span>
-
{%- endif -%}
-
{%- else -%}
-
<span class="p-name">{{ person }}</span>
-
{%- endif -%}
-
</li>
{%- endfor -%}
</ul>
</p>
···
<a href="mailto:~hauleth/blog@lists.sr.ht?subject=[Comment] {{
page.title }}">~hauleth/blog@lists.sr.ht</a>
(<a href="https://lists.sr.ht/~hauleth/blog">archive</a>).</p>
</div>
{%- endif -%}
</article>
···
{%- block main -%}
<article class="post h-entry">
<h1 class="post-title p-name">
+
<a id="top" href="{{ page.permalink }}">{{ page.title }}</a>
</h1>
{{ posts::meta(page=page, author=config.extra.show_author) }}
{%- if page.toc | length >= 5 -%}
···
{%- endif -%}
<div class="post-content e-content">
{{ page.content | safe }}
+
<div class="halmos">&#x220e;</div>
</div>
{%- if config.extra.for_hire -%}
<div class="for-hire">
···
{%- if page.extra.thanks -%}
<hr />
<p>
+
<b>Special thanks to</b>:
<ul>
{%- for person in page.extra.thanks -%}
+
<li class="h-card">{{ posts::thanks(who=person) }}</li>
{%- endfor -%}
</ul>
</p>
···
<a href="mailto:~hauleth/blog@lists.sr.ht?subject=[Comment] {{
page.title }}">~hauleth/blog@lists.sr.ht</a>
(<a href="https://lists.sr.ht/~hauleth/blog">archive</a>).</p>
+
</div>
+
+
<div class="webmentions">
+
{%- set webmentions = load_data(url="https://webmention.io/api/mentions.jf2?target=" ~ page.permalink, format="json") -%}
+
{%- if webmentions.children | length > 0 -%}
+
<p>Webmentions:</p>
+
<ul>
+
{%- for mention in webmentions.children -%}
+
<li>
+
{%- if mention.name -%}
+
<a href="{{ mention.url }}">{{ mention.name }}</a>
+
{%- else -%}
+
<a href="{{ mention.url }}" class="url-only">{{ mention.url }}</a>
+
{%- endif -%}
+
{% if mention.author.name != "" %}
+
by&nbsp;<a href="{{ mention.author.url }}">{{ mention.author.name | truncate(length=15) }}</a>
+
{%- endif -%}
+
</li>
+
{%- endfor -%}
+
</ul>
+
{%- else -%}
+
No webmentions yet, you can be first.
+
{%- endif -%}
</div>
{%- endif -%}
</article>
+4
templates/robots.txt
···
···
+
User-agent: *
+
Disallow: /cv/ /404/ /common-test-example/
+
Allow: /
+
Sitemap: {{ get_url(path="sitemap.xml") }}
+95 -55
templates/shortcodes/cv.md
···
## Personal information
-
Date of Birth:
-
1993-03-16
-
Email:
-
<lukasz@niemier.pl>
Twitter:
[@hauleth](https://twitter.com/hauleth)
···
## Experience
-
- Prograils - Junior Developer - 2013
- Nukomeet - Full-stack Developer - 2015-2016
-
- AppUnite - Full-stack Developer/DevOps - 2016-2019
-
- Kobil GmbH - Erlang/Elixir Developer - 2019-2020
-
- Remote Inc. - Senior Backend Engineer - 2020-2021
-
- Hauleth.dev - Consultant - 2021+
-
+ Erlang Solutions/Kloeckner GmbH - 2021 - Consultant for Elixir, Ruby, and SQL (PostgreSQL)
-
### Community awards
-
- ElixirForum Member of the Year 2021
-
- ElixirForum Member of the Year 2020
-
- ElixirForum Member of the Year 2019
-
- ElixirForum Member of the Month 2018
### Other fields
···
* Organizer - 2015
+ UEFA Championship 2012 - Poland-Ukraine
* ICT Accreditation support
### Languages
···
- GitHub: <https://github.com/hauleth>
- GitLab: <https://gitlab.com/hauleth>
-
- StackOverflow: <https://stackoverflow.com/users/1017941/hauleth>
### Notable contributions
- Elixir language:
-
+ Logger reimplementation on top of Erlang's `logger` module
-
+ `mix test --cover` CLI output
-
+ Support for `NO_COLOR` environment variable
-
+ `is_struct/1`
-
+ Fixing module inspection on case-insensitive file systems
- Erlang OTP:
-
+ Support for custom devices in `logger_std_h`
-
+ Fixing `socket` module to support broader set of protocols (for example
-
ICMP)
-
+ Support for global metadata in `logger`
-
+ Support for reconfiguration of `logger` (needed for better Mix and Rebar3
-
integration)
-
+ Several fixes to `logger` and `socket` modules
- Git:
-
+ Add support for Elixir in diff
- Ecto:
-
+ Support aggregations over `*`
-
+ Better error on duplicated `schema` block
- Elixir MongoDB driver
-
+ Support for transactions
### Notable projects
···
projects
- <https://github.com/hauleth/mix_unused> - Mix compiler for detecting unused
code
-
- Elixir's Logger implementation in 1.10+
- <https://github.com/open-telemetry/opentelemetry-erlang> - maintainer of
the Erlang's OpenTelemetry implementation
- Vim plugins:
···
+ <https://gitlab.com/hauleth/qfx.vim> - display signs next to QF matches
### Languages and Frameworks
-
-
Expert:
- Elixir
+ Phoenix
···
+ OpenTelemetry collaborator
+ EEF Member
+ OTP contributor
-
-
Advanced:
-
- Rust
-
- C
-
- SQL (pg/SQL)
- sh/Bash
- Ruby
+ Ruby on Rails
-
Fluent:
-
-
- C++
-
- JavaScript
-
### Technologies
-
Expert:
-
- Git
- Vim
-
-
Advanced:
-
- HashiStack
+ Terraform
+ Consul
···
- TDD/BDD methodologies
- Property testing
-
Fluent:
-
-
- SaltStack
-
- Puppet
-
## Other
- Viking reenactor
- Keyboard fan
- Sci-fi/Fantasy fan and Poznań's Sci-fi/Fantasy club member
···
## Personal information
Email:
+
<~@hauleth.dev>
+
+
Website:
+
<https://hauleth.dev>
Twitter:
[@hauleth](https://twitter.com/hauleth)
···
## Experience
+
- Hauleth.dev - Consultant - 2021+
+
+ DockYard/Karambit.ai - 2025
+
* Architectural analysis of Karambit product
+
* Prepared security analysis with detailed report with fixes
+
+ Eiger - 2022-2023
+
* Forte.io
+
- Implemented Interledger protocol for cross-chain transactions
+
* Aleo Blockchain
+
- Implemented GraphQL API for the on-chain data
+
- Created syntax colouring library for Aleo assembly-like language
+
for smart contracts
+
+ Erlang Solutions/Kloeckner GmbH - 2021 - Consultant for Elixir, Ruby,
+
and SQL (PostgreSQL)
+
* Optimised DB query performance by providing PostgreSQL structure
+
analysis and improving indices usage
+
+ Remote Inc. - Senior Backend Engineer - 2020-2021
+
* Architectural analysis of existing codebase
+
+ Kobil GmbH - Erlang/Elixir Developer - 2019-2020
+
* Maintained MongoDB driver for Elixir
+
* Implemented transactions for MongoDB driver in Elixir
+
- Supabase - 2023-2025
+
+ Logflare - logs aggregation service:
+
* Implemented on-the-fly decompression of incoming data that improved
+
ingestion possibilities and reduced transfer usage (created library
+
[`plug_caisson`][] for that purpose)
+
* Implemented DataDog-compatible ingestion endpoint for seamless
+
transition from DataDog provider to Logflare
+
* Improved BigQuery pipeline workflow to reduce congestion on database
+
connections
+
* Added support for AWS Cloud Events metadata extraction
+
* Improved CI usage by splitting different actions to separate steps ran
+
in parallel
+
* Replaced dynamic generation of connection modules for PostgreSQL
+
storage system with Ecto's dynamic repositories to avoid atom exhaustion
+
+ Supavisor - a cloud-native, multi-tenant Postgres connection pooler
+
* Deployment management
+
* Optimised metrics gathering system that resulted in an order of
+
magnitude performance boost
+
* Updated used OTP and Elixir versions from OTP 24 to OTP 27 and Elixir
+
from 1.14 to 1.18
+
* Reduced usage of mocking in tests to improve tests performance and
+
volatility, resulting in reduced CI usage and improved developer
+
experience
+
* Implemented e2e tests against existing Node.js PostgreSQL clients to
+
improve production issues
+
* Implemented multi-region deployment system to provide blue/green
+
deployments
+
* Improved system observability features by making it more resilient and
+
performant
+
* Replaced usage of `ct_slave` with newer `peer` module in OTP
+
- AppUnite - Full-stack Developer/DevOps - 2016-2019:
+
+ JaFolders/AlleFolders
+
* 2x performance improvement by optimising PostgreSQL usage
+
* Reduced geo-queries using PostGIS thanks to better indices and
+
materialised views usage
+
* Implemented UI and brochure viewer in Vue and SVG
+
+ OneMedical/Helium Health
+
* Architectural redesign of application from Rails/MongoDB to
+
Phoenix/PostgreSQL
+
* Prepared hybrid deployment with on-premise/in-cloud system
+
* Migrated of the existing deployments from MongoDB to PostgreSQL
- Nukomeet - Full-stack Developer - 2015-2016
+
- Prograils - Junior Developer - 2013
+
### Organisations
+
- Erlang Ecosystem Foundation - member of the Observability WG
+
- OpenTelemetry Project - member of the Erlang WG
### Other fields
···
* Organizer - 2015
+ UEFA Championship 2012 - Poland-Ukraine
* ICT Accreditation support
+
- Times Person of the Year - 2006
### Languages
···
- GitHub: <https://github.com/hauleth>
- GitLab: <https://gitlab.com/hauleth>
+
- SourceHut: <https://sr.ht/~hauleth>
+
- StackOverflow: <https://stackoverflow.com/u/1017941>
### Notable contributions
- Elixir language:
+
+ Logger reimplementation on top of Erlang's `logger` module
+
+ `mix test --cover` CLI output
+
+ Support for `NO_COLOR` environment variable
+
+ `is_struct/1`
+
+ Fixing module inspection on case-insensitive file systems
+
+ Support for parsing extra arguments via `mix eval` and `eval` command in
+
release
- Erlang OTP:
+
+ Support for custom devices in `logger_std_h`
+
+ Fixing `socket` module to support broader set of protocols (for example
+
ICMP)
+
+ Support for global metadata in `logger`
+
+ Support for reconfiguration of `logger` (needed for better Mix and Rebar3
+
integration)
+
+ Several fixes to `logger` and `socket` modules
+
+ Add support for τ constant in `math`
- Git:
+
+ Add support for Elixir in diff
- Ecto:
+
+ Support aggregations over `*`
+
+ Better error on duplicated `schema` block
- Elixir MongoDB driver
+
+ Support for transactions
### Notable projects
···
projects
- <https://github.com/hauleth/mix_unused> - Mix compiler for detecting unused
code
- <https://github.com/open-telemetry/opentelemetry-erlang> - maintainer of
the Erlang's OpenTelemetry implementation
- Vim plugins:
···
+ <https://gitlab.com/hauleth/qfx.vim> - display signs next to QF matches
### Languages and Frameworks
- Elixir
+ Phoenix
···
+ OpenTelemetry collaborator
+ EEF Member
+ OTP contributor
+
- Nix/NixOS
- Rust
+
- PostgreSQL
- sh/Bash
- Ruby
+ Ruby on Rails
### Technologies
- Git
- Vim
- HashiStack
+ Terraform
+ Consul
···
- TDD/BDD methodologies
- Property testing
## Other
- Viking reenactor
- Keyboard fan
- Sci-fi/Fantasy fan and Poznań's Sci-fi/Fantasy club member
+
+
[`plug_caisson`]: https://github.com/supabase/plug_caisson
+13
templates/sitemap.xml
···
···
+
<?xml version="1.0" encoding="UTF-8"?>
+
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
+
{%- for sitemap_entry in entries %}
+
{%- if sitemap_entry.extra | get(key="sitemap", default=true) %}
+
<url>
+
<loc>{{ sitemap_entry.permalink | escape_xml | safe }}</loc>
+
{%- if sitemap_entry.updated %}
+
<lastmod>{{ sitemap_entry.updated }}</lastmod>
+
{%- endif %}
+
</url>
+
{%- endif %}
+
{%- endfor %}
+
</urlset>
+1
themes/zerm/.gitignore
···
···
+
public
+22
themes/zerm/LICENSE.md
···
···
+
The MIT License (MIT)
+
+
Copyright (c) 2019 elias julian marko garcia
+
Copyright (c) 2019 Paweł Romanowski
+
Copyright (c) 2019 panr
+
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
+
this software and associated documentation files (the "Software"), to deal in
+
the Software without restriction, including without limitation the rights to
+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+
the Software, and to permit persons to whom the Software is furnished to do so,
+
subject to the following conditions:
+
+
The above copyright notice and this permission notice shall be included in all
+
copies or substantial portions of the Software.
+
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+61
themes/zerm/README.md
···
···
+
# zerm
+
+
a minimalist and dark theme for [Zola](https://getzola.org).
+
+
![Screenshot](../master/zerm-preview.png?raw=true)
+
+
[**Live Preview!**](https://zerm.ejmg.now.sh/)
+
+
Largely a port of Radek Kozieł's [Terminal
+
Theme](https://github.com/panr/hugo-theme-terminal) for Hugo. 4/5ths of my way
+
through porting this theme, I discovered Paweł Romanowski own independent fork
+
for Zola, [Terminimal](https://github.com/pawroman/zola-theme-terminimal),
+
which helped me get the PostCSS to Sass styling conversion done more
+
quickly. My sincerest thanks to both of you!
+
+
## differences
+
+
This theme is largely true to the original by Radek, but there are some mild
+
differences. They are almost all stylistic in nature and are intended to
+
emphasize minimalism even more. Some of them are as follows:
+
- tags are now included in a post's meta data.
+
- no post image previews.
+
- categories are included in the taxonomy.
+
- bullet points have slightly more margin and different symbols for nesting.
+
- no social media or comment support.
+
+
Some of these might be added later and [PR's are always
+
welcomed](https://github.com/ejmg/zerm/pulls).
+
+
## configuration
+
+
Please follow the Zola documentation for [how to use a
+
theme](https://www.getzola.org/documentation/themes/installing-and-using-themes/#installing-a-theme).
+
+
In `config.toml`, you will find all values for customization that are supported
+
thus far have documentation explaining how they are used. If there is any confusion or something is not working as intended, [please open an issue](https://github.com/ejmg/zerm/issues)!
+
+
## math
+
You can use KaTeX for mathematical typesetting.
+
Assets are only available if you opt-in on a per-page level through
+
a single line (`math=true`) on the extra section of the page frontmatter.
+
+
``` md
+
# index.md
+
+++
+
title="this page title"
+
...
+
+
[extra]
+
math=true
+
+++
+
+
Content
+
```
+
+
Pages wich doesn't opt-in are not affected in any way, so you doesn't have
+
to worry about any performance hit.
+
+
## license
+
+
MIT. See `LICENSE.md` for more details.
+131
themes/zerm/config.toml
···
···
+
# The URL the site will be built for
+
base_url = "/"
+
+
# Used in RSS by default
+
title = "My blog!"
+
description = "placeholder description text for your blog!"
+
+
# The default language, used in RSS
+
# TODO: I would love to support more languages and make this easy to handle
+
# with other facets of the theme.
+
default_language = "en"
+
+
# Whether to generate a RSS feed automatically
+
generate_feed = true
+
# 'atom.xml' (default if unspecified) and 'rss.xml' are officially supported
+
# values for feed_filename in this theme. All other filenames will assume a
+
# link type of 'application/rss+xml'.
+
# feed_filename = "atom.xml"
+
+
# Theme name to use.
+
# NOTE: should not need to mess with this if you are using zerm directly, i.e. cloning the
+
# repository at root and not using as directed by the Zola docs via themes/ directory.
+
# theme = ""
+
+
# Whether to automatically compile all Sass files in the sass directory
+
compile_sass = true
+
+
# Whether to do syntax highlighting
+
# Theme can be customised by setting the `highlight_theme` variable to a theme supported by Zola
+
highlight_code = true
+
+
# Syntax highlighting theme. See:
+
# https://www.getzola.org/documentation/getting-started/configuration/#syntax-highlighting
+
# for more information and themes built into Zola.
+
highlight_theme = "axar" # Other dark themes that work: "1337", "agola-dark",
+
# "visual-studio-dark"
+
+
# Whether to build a search index to be used later on by a JavaScript library
+
build_search_index = false
+
+
# Built in taxonomies of zerm.
+
taxonomies = [
+
{name = "tags"},
+
{name = "categories"},
+
]
+
+
[extra]
+
# Put all your custom variables here
+
#
+
# Many configurations are taken directly from Terminal's config.toml
+
# ---------------------------------------------------------
+
+
# Author name to be added to posts, if enabled.
+
author = "you!"
+
+
# Show author's name in blog post meta data.
+
show_author = false
+
+
# Show categories a blog post is marked with in its meta data.
+
show_categories = true
+
+
# Show tags a blog post is marked with in its meta data.
+
show_tags = true
+
+
# Theme color. You can have any color you want, so long as it's...
+
# ["orange", "blue", "red", "green", "pink"]
+
theme_color = "orange"
+
+
# Custom css to style over the defaults. This is useful when you only have a
+
# few small tweaks to make rather than a major rehaul to the theme.
+
# It would be best to make this a proper .sass or .scss file in sass/ rather
+
# than placing in static/
+
# custom_css = "custom.css"
+
+
# How many menu items to show on desktop. if you set this to 0, only submenu
+
# button will be visible.
+
show_menu_items = 2
+
+
# set theme to full screen width.
+
full_width = false
+
+
# center theme with default width.
+
center = false
+
+
# set a custom favicon. Must be placed in root of static/ directory...
+
# favicon = ""
+
+
+
# Set a custom preview image for your website when posted as a link.
+
# Must be placed in root of static/ directory...
+
# og_preview_img = ""
+
+
# Copyright notice if desired. Defaults to
+
# copyright = "copyright notice here"
+
+
# What is displayed in the top left corner of the website. Default is zerm.
+
logo_text = "zerm"
+
+
# Link in logo. Default returns you to $BASE_URL.
+
logo_home_link = "/"
+
+
# Menu items to display. You define a url and the name of the menu item.
+
# NOTE: `$BASE_URL/` must be included in the url name.
+
main_menu = [
+
{url="/about/", name="about"},
+
{url="/contact/", name="contact"},
+
{url="https://google.com", name="Google", external=true},
+
]
+
+
# Displayed as title of drop-down menu when size of main_menu > show_menu_items.
+
menu_more = "show more"
+
+
# Displayed after teaser text for a blog post.
+
read_more = "read more"
+
+
# not currently used from previous theme, but leaving here for now in case I
+
# feel like adding it.
+
read_other_posts = "read other posts"
+
+
+
# Enable math typesetting with KaTeX
+
# Show math in pages with `math=true` in the TOML frontmatter
+
enable_katex = true
+
+
# Options for disqus
+
disqus = { enabled=false, short_name="" }
+
+
# generate Table of Contents for all articles
+
# Table of Contents can be generated for individual articles
+
# by adding `ToC = true` in [extra] section in frontmatter
+
# ToC = true
+6
themes/zerm/content/_index.md
···
···
+
+++
+
sort_by = "date"
+
transparent = true
+
paginate_by = 3
+
insert_anchor_links = "right"
+
+++
+10
themes/zerm/content/about/_index.md
···
···
+
+++
+
title = "about"
+
path = "about"
+
+
[extra]
+
date = 2019-03-21
+
+++
+
+
Yet another theme for yet another static site generator; that said, I hope you
+
like it.
+14
themes/zerm/content/contact/_index.md
···
···
+
+++
+
title="contact"
+
description="a basic demo of zola. Does it work?"
+
+
[extra]
+
date=2019-03-26
+
+++
+
+
# some links
+
+
- [zola, the static site generator written in rust](https://getzola.org)
+
- [source code for zerm](https://github.com/ejmg/zerm)
+
- [Terminal, the theme zerm was derived from](https://github.com/panr/hugo-theme-terminal)
+
- [Terminimal, another theme for zola based on Terminal](https://github.com/pawroman/zola-theme-terminimal)
+65
themes/zerm/content/demo/index.md
···
···
+
+++
+
title="demo"
+
description="a basic demo of zola."
+
date=2019-08-06
+
+
[taxonomies]
+
tags = ["demo", "zola", "highlighting"]
+
categories = ["programming", "wu tang",]
+
+
[extra]
+
+++
+
+
Here's a general demo of Zola and how things look with zerm.
+
+
# Header I
+
+
Inline code: `println!("Wu Tang!");`
+
+
Zola has built in syntax highlighting. If there's not a theme you like, you can
+
easily add more.
+
+
zerm uses Fira Code fonts, which means we get ligatures in addition to
+
Zola's powerful syntax highlighting ✨.
+
+
```rs
+
fn foo(arg: String) -> Result<u32, Io::Error> {
+
println!("Nice!"); // TODO: the thingy
+
if 1 != 0 {
+
println!("How many ligatures can I contrive??");
+
println!("Turns out a lot! ==> -/-> <!-- <$> >>=");
+
}
+
Ok(42)
+
}
+
```
+
+
## Header II
+
+
Want block quotes? We got block quotes.
+
+
Remember the wise words of Ras Kass:
+
+
> In Hotel Rwanda, reminder to honor these street scholars who ask why
+
U.S. Defense is twenty percent of the tax dollar. Bush gave 6.46 billion to
+
Halliburton for troops support efforts in Iraq; meanwhile, the hood is hurting,
+
please believe that.
+
>
+
> -- "Verses", _Wu-Tang Meets The Indie Culture_
+
+
### Header III
+
+
| members | age | notable album | to be messed with? |
+
|------------------|-----|----------------------------------------------|-------------------------------------------------------------------------|
+
| GZA | 52 | Liquid Swords | no |
+
| ODB | 35 | Return to the 36 Chambers: The Dirty Version | absolutely not |
+
| Raekwon Da Chef | 49 | Only Built 4 Cuban Linx... | `"no"` that's spanish for "no" |
+
| Ghostface Killah | 49 | Fishscale | i swear you keep asking that question and the answer ain't gonna change |
+
| Inspectah Deck | 49 | CZARFACE | `protect ya neck, boy` |
+
+
+
#### Header IV
+
+
Here's a video of my rabbit, Smalls, loaf'n to lofi beats:
+
+
{{ youtube(id="UUpuz8IObcs") }}
+
+13
themes/zerm/content/fiz/index.md
···
···
+
+++
+
title="fiz"
+
description="a basic demo of zola. Does it work?"
+
date=2019-03-25
+
author="elias"
+
+
[taxonomies]
+
tags = ["rust", "test"]
+
# categories = ["misc."]
+
+++
+
+
+
Foo Bar Buzz Fizz Qux Fum
+21
themes/zerm/content/technology_is_hell/index.md
···
···
+
+++
+
title="technology is hell!"
+
description="Yet another blog post ranting about XYZ technology for ABC reasons"
+
date=2019-03-26
+
+
[taxonomies]
+
tags = ["rust", "test"]
+
categories = ["misc."]
+
+++
+
+
+
Nullam eu ante vel est convallis dignissim. Fusce suscipit, wisi nec facilisis
+
facilisis, est dui fermentum leo, quis tempor ligula erat quis odio. Nunc
+
porta vulputate tellus. Nunc rutrum turpis sed pede. Sed bibendum. Aliquam
+
posuere. Nunc aliquet, augue nec adipiscing interdum, lacus tellus malesuada
+
massa, quis varius mi purus non odio. Pellentesque condimentum, magna ut
+
suscipit hendrerit, ipsum augue ornare nulla, non luctus diam neque sit amet
+
urna. Curabitur vulputate vestibulum lorem. Fusce sagittis, libero non
+
molestie mollis, magna orci ultrices dolor, at vulputate neque nulla lacinia
+
eros. Sed id ligula quis est convallis tempor. Curabitur lacinia pulvinar
+
nibh. Nam a sapien.
+57
themes/zerm/content/using_katex/index.md
···
···
+
+++
+
title="Using KaTeX for mathematical typesetting"
+
date=2021-06-16
+
+
[taxonomies]
+
categories=["test"]
+
tags=["math", "zola"]
+
+
[extra]
+
math=true
+
+++
+
+
The usual way to include LaTeX is to use `$$`, as shown in the examples below.
+
+
These examples are taken from <http://khan.github.io/KaTeX/>
+
+
### Example 1
+
If the text between `$$` contains newlines it will rendered in display mode:
+
```
+
$$
+
f(x) = \int_{-\infty}^\infty\hat f(\xi)\,e^{2 \pi i \xi x}\,d\xi
+
$$
+
```
+
+
$$
+
f(x) = \int_{-\infty}^\infty\hat f(\xi)\,e^{2 \pi i \xi x}\,d\xi
+
$$
+
+
### Example 2
+
+
```
+
$$
+
\frac{1}{\Bigl(\sqrt{\phi \sqrt{5}}-\phi\Bigr) e^{\frac25 \pi}} = 1+\frac{e^{-2\pi}} {1+\frac{e^{-4\pi}} {1+\frac{e^{-6\pi}} {1+\frac{e^{-8\pi}} {1+\cdots} } } }
+
$$
+
+
1(ϕ5−ϕ)e25π=1+e−2π1+e−4π1+e−6π1+e−8π1+⋯ \frac{1}{\Bigl(\sqrt{\phi \sqrt{5}}-\phi\Bigr) e^{\frac25 \pi}} = 1+\frac{e^{-2\pi}} {1+\frac{e^{-4\pi}} {1+\frac{e^{-6\pi}} {1+\frac{e^{-8\pi}} {1+\cdots} } } }
+
```
+
+
$$
+
\frac{1}{\Bigl(\sqrt{\phi \sqrt{5}}-\phi\Bigr) e^{\frac25 \pi}} = 1+\frac{e^{-2\pi}} {1+\frac{e^{-4\pi}} {1+\frac{e^{-6\pi}} {1+\frac{e^{-8\pi}} {1+\cdots} } } }
+
$$
+
+
1(ϕ5−ϕ)e25π=1+e−2π1+e−4π1+e−6π1+e−8π1+⋯ \frac{1}{\Bigl(\sqrt{\phi \sqrt{5}}-\phi\Bigr) e^{\frac25 \pi}} = 1+\frac{e^{-2\pi}} {1+\frac{e^{-4\pi}} {1+\frac{e^{-6\pi}} {1+\frac{e^{-8\pi}} {1+\cdots} } } }
+
+
### Example 3
+
+
```
+
$$
+
1 + \frac{q^2}{(1-q)}+\frac{q^6}{(1-q)(1-q^2)}+\cdots = \prod_{j=0}^{\infty}\frac{1}{(1-q^{5j+2})(1-q^{5j+3})}, \quad\quad \text{for }\lvert q\rvert<1.
+
$$
+
```
+
+
$$
+
1 + \frac{q^2}{(1-q)}+\frac{q^6}{(1-q)(1-q^2)}+\cdots = \prod_{j=0}^{\infty}\frac{1}{(1-q^{5j+2})(1-q^{5j+3})}, \quad\quad \text{for }\lvert q\rvert<1.
+
$$
+
+
+42
themes/zerm/content/waz/index.md
···
···
+
+++
+
title="waz"
+
description="a basic demo of zola. Does it work? This old man, he played one. He played knick knack on my drum."
+
date=2019-03-27
+
+
[taxonomies]
+
tags = ["rust", "test", "zola"]
+
categories = ["programming", "misc.", "programming languages"]
+
+
[extra]
+
+++
+
+
# Hello Hello
+
+
Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Donec hendrerit
+
tempor tellus. Donec pretium posuere tellus. Proin quam nisl, tincidunt et,
+
mattis eget, convallis nec, purus. Cum sociis natoque penatibus et magnis dis
+
parturient montes, nascetur ridiculus mus. Nulla posuere. Donec vitae dolor.
+
Nullam tristique diam non turpis. Cras placerat accumsan nulla. Nullam
+
rutrum. Nam vestibulum accumsan nisl.
+
+
+
```python
+
def foo(bar, **kwargs):
+
print("yo, this is nice!")
+
```
+
+
## a list
+
+
* Donec hendrerit tempor tellus.
+
* Nam a sapien.
+
* Phasellus at dui in ligula mollis ultricies.
+
* Mauris mollis tincidunt felis.
+
* Nullam rutrum.
+
+
### Yet another list
+
1. Nunc aliquet, augue nec adipiscing interdum, lacus tellus malesuada massa,
+
quis varius mi purus non odio.
+
2. Donec hendrerit tempor tellus.
+
3. Nunc aliquet, augue nec adipiscing interdum, lacus tellus malesuada massa,
+
quis varius mi purus non odio.
+
+69
themes/zerm/content/zerm/index.md
···
···
+
+++
+
title="what is zerm?"
+
description="a summary of what zerm is and why it is different."
+
date=2019-08-07
+
updated=2021-02-03
+
+
[taxonomies]
+
tags = ["rust", "test", "zola"]
+
categories = ["programming", "misc.",]
+
+
[extra]
+
ToC = true
+
+++
+
+
# hello
+
+
This is zerm, a minimalist theme for Zola based[^1] off of [panr's](https://twitter.com/panr)
+
theme for Hugo.
+
+
While it's largely faithful to the original, there are some changes:
+
- no prism.js integration, instead we use Zola's syntax highlighting to reduce overhead.
+
- removal of PostCSS and leveraging Zola's use of Sass for simple styling.
+
- much thanks to [Paweł
+
Romanowski's](https://github.com/pawroman/zola-theme-terminimal/)
+
independent fork of Terminal. Their Sass stylings saved me the overhead of
+
figuring it out myself.
+
- no preview images. I want a theme that is focused on content.
+
- support for anchor links.
+
- Other small, opinionated changes that I think lend to the minimalism and
+
aesthetic of zerm.
+
+
+
Things this theme does not have but either Terminal or Terminimal might:
+
- better short-codes for things like embedding videos or images, though I will
+
work on this over time.
+
- better support for things like comments and social media. As of now, I have
+
no plans to add this but [PR's are always
+
**welcomed**](https://github.com/ejmg/zerm/pulls).
+
+
## A quick demo
+
+
`println!("inline code");`
+
+
```rs
+
fn main(n: String) {
+
println!("hello, zola!");
+
}
+
```
+
+
### Header III
+
+
> a somewhat kinda maybe large quote that maybe spans
+
> more than one line but I mean really who even cares
+
> okay maybe I do but point being is yes nice.
+
+
#### Header IV
+
+
| hello | tables | nice |
+
|:-----:|:---------:|------|
+
| wow | much love | yes |
+
+
+
Like zerm? Then [install
+
Zola](https://www.getzola.org/documentation/getting-started/installation/) and
+
[get started](https://www.getzola.org/documentation/themes/installing-and-using-themes/#installing-a-theme)!
+
+
---
+
+
[^1]: fork? port? a little bit of the former, more of the latter?
+7
themes/zerm/package.json
···
···
+
{
+
"scripts":
+
{
+
"install": "curl -L -O https://github.com/getzola/zola/releases/download/v0.13.0/zola-v0.13.0-x86_64-unknown-linux-gnu.tar.gz && tar -xzf zola-v0.13.0-x86_64-unknown-linux-gnu.tar.gz",
+
"build": "./zola build"
+
}
+
}
+29
themes/zerm/sass/_buttons.scss
···
···
+
button,
+
.button,
+
a.button {
+
position: relative;
+
display: flex;
+
align-items: center;
+
justify-content: center;
+
padding: 8px 18px;
+
margin-bottom: 5px;
+
text-decoration: none;
+
text-align: center;
+
border-radius: 8px;
+
border: 1px solid transparent;
+
appearance: none;
+
cursor: pointer;
+
outline: none;
+
}
+
+
a.read-more,
+
a.read-more:hover,
+
a.read-more:active {
+
display: inline-flex;
+
background: none;
+
box-shadow: none;
+
padding: 0;
+
margin: 20px 0;
+
max-width: 100%;
+
}
+
+15
themes/zerm/sass/_font.scss
···
···
+
@font-face {
+
font-family: 'Fira Code';
+
font-style: normal;
+
font-weight: 400;
+
font-display: swap;
+
src: url("assets/fonts/FiraCode-Regular.woff2") format("woff2"), url("assets/fonts/FiraCode-Regular.woff") format("woff");
+
}
+
+
@font-face {
+
font-family: 'Fira Code';
+
font-style: normal;
+
font-weight: 800;
+
font-display: swap;
+
src: url("assets/fonts/FiraCode-Bold.woff2") format("woff2"), url("assets/fonts/FiraCode-Bold.woff") format("woff");
+
}
+67
themes/zerm/sass/_footer.scss
···
···
+
@import "variables";
+
+
.footer {
+
padding: 40px 0;
+
flex-grow: 0;
+
opacity: .5;
+
+
&__inner {
+
display: flex;
+
align-items: center;
+
justify-content: space-between;
+
margin: 0;
+
width: 760px;
+
max-width: 100%;
+
+
@media (max-width: $tablet-max-width) {
+
flex-direction: column;
+
}
+
}
+
+
a {
+
color: inherit;
+
}
+
+
.copyright {
+
display: flex;
+
flex-direction: row;
+
align-items: center;
+
font-size: 1rem;
+
// so `--light-color-secondary` color exists no where else in the stylings
+
// color: var(--light-color-secondary);
+
// As a substitute, I'm going to use the alpha-70 version of accent.
+
color: var(--accent-alpha-70);
+
+
&--user {
+
margin: auto;
+
text-align: center;
+
}
+
+
& > *:first-child:not(:only-child) {
+
margin-right: 10px;
+
+
@media (max-width: $tablet-max-width) {
+
border: none;
+
padding: 0;
+
margin: 0;
+
}
+
}
+
+
@media (max-width: $tablet-max-width) {
+
flex-direction: column;
+
margin-top: 10px;
+
}
+
}
+
+
// .copyright-theme-sep {
+
// @media (max-width: $tablet-max-width) {
+
// display: none;
+
// }
+
// }
+
+
// .copyright-theme {
+
// @media (max-width: $tablet-max-width) {
+
// font-size: 0.75rem;
+
// }
+
// }
+
}
+139
themes/zerm/sass/_header.scss
···
···
+
@import "variables";
+
+
@mixin menu {
+
background: var(--background);
+
box-shadow: var(--shadow);
+
color: var(--color);
+
border: 2px solid;
+
margin: 0;
+
padding: 10px;
+
list-style: none;
+
z-index: 99;
+
}
+
+
.header {
+
display: flex;
+
flex-direction: column;
+
position: relative;
+
+
&__inner {
+
display: flex;
+
align-items: center;
+
justify-content: space-between;
+
}
+
+
&__logo {
+
display: flex;
+
flex: 1;
+
+
&:after {
+
content: '';
+
background: repeating-linear-gradient(90deg, var(--accent), var(--accent) 2px, transparent 0, transparent 10px);
+
display: block;
+
width: 100%;
+
right: 10px;
+
}
+
+
a {
+
flex: 0 0 auto;
+
max-width: 100%;
+
text-decoration: none;
+
}
+
}
+
+
.menu {
+
margin: 20px 0;
+
--shadow-color: var(--accent-alpha-70);
+
--shadow: 0 10px var(--shadow-color), -10px 10px var(--shadow-color), 10px 10px var(--shadow-color);
+
+
@media (max-width: $phone-max-width) {
+
@include menu;
+
position: absolute;
+
top: 50px;
+
right: 0;
+
}
+
+
&__inner {
+
// @include menu;
+
display: flex;
+
flex-wrap: wrap;
+
list-style: none;
+
margin: 0;
+
padding: 0;
+
+
&--desktop {
+
@media (max-width: $phone-max-width) {
+
display: none;
+
}
+
}
+
+
&--mobile {
+
display: none;
+
+
@media (max-width: $phone-max-width) {
+
display: block;
+
}
+
}
+
+
li {
+
// &.active {
+
// color: var(--accent-alpha-70);
+
// }
+
&:not(:last-of-type) {
+
margin-right: 20px;
+
margin-bottom: 10px;
+
flex: 0 0 auto;
+
}
+
}
+
+
@media (max-width: $phone-max-width) {
+
flex-direction: column;
+
align-items: flex-start;
+
padding: 0;
+
+
li {
+
margin: 0;
+
padding: 5px;
+
}
+
}
+
}
+
+
&__sub-inner {
+
position: relative;
+
list-style: none;
+
padding: 0;
+
margin: 0;
+
+
&:not(:only-child) {
+
margin-left: 20px;
+
}
+
+
&-more {
+
@include menu;
+
top: 35px;
+
left: 0;
+
+
&-trigger {
+
color: var(--accent);
+
user-select: none;
+
cursor: pointer;
+
}
+
+
li {
+
margin: 0;
+
padding: 5px;
+
white-space: nowrap;
+
}
+
}
+
}
+
+
&-trigger {
+
color: var(--accent);
+
border: 2px solid;
+
margin-left: 10px;
+
height: 100%;
+
padding: 3px 8px;
+
position: relative;
+
}
+
}
+
}
+8
themes/zerm/sass/_logo.scss
···
···
+
.logo {
+
display: flex;
+
align-items: center;
+
text-decoration: none;
+
background: var(--accent);
+
color: black;
+
padding: 5px 10px;
+
}
+335
themes/zerm/sass/_main.scss
···
···
+
@import "variables";
+
+
html {
+
box-sizing: border-box;
+
}
+
+
*,
+
*:before,
+
*:after {
+
box-sizing: inherit;
+
}
+
+
body {
+
margin: 0;
+
padding: 0;
+
font-family: 'Fira Code', Menlo, DejaVu Sans Mono, Monaco, Consolas, Ubuntu Mono, monospace;
+
font-size: 1rem;
+
line-height: 1.54;
+
background-color: var(--background);
+
color: var(--color);
+
text-rendering: optimizeLegibility;
+
-webkit-font-smoothing: antialiased;
+
-webkit-overflow-scrolling: touch;
+
-webkit-text-size-adjust: 100%;
+
font-feature-settings: "liga";
+
+
@media (max-width: $phone-max-width) {
+
font-size: 1rem;
+
}
+
}
+
+
h1, h2, h3, h4, h5, h6 {
+
line-height: 1.3;
+
+
&:not(first-child) {
+
margin-top: 40px;
+
}
+
+
.zola-anchor {
+
font-size: 1.5rem;
+
visibility: hidden;
+
margin-left: 0.5rem;
+
vertical-align: 1%;
+
text-decoration: none;
+
border-bottom-color: transparent;
+
cursor: pointer;
+
+
@media(max-width: $phone-max-width){
+
visibility: visible;
+
}
+
}
+
+
&:hover {
+
.zola-anchor {
+
visibility: visible;
+
}
+
}
+
}
+
+
// Actually keeping Pawroman's stylings here for font-size over h1-h6.
+
// I prefer differentiated header height.
+
+
// OLD
+
// ---------------------
+
// h1, h2, h3 {
+
// font-size: 1.4rem;
+
// }
+
+
// h4, h5, h6 {
+
// font-size: 1.2rem;
+
// }
+
+
// Pawroman's
+
// ---------------------
+
h1 {
+
font-size: 1.4rem;
+
}
+
+
h2 {
+
font-size: 1.3rem;
+
}
+
+
h3 {
+
font-size: 1.2rem;
+
}
+
+
h4, h5, h6 {
+
font-size: 1.15rem;
+
}
+
+
+
a {
+
color: inherit;
+
}
+
+
img {
+
display: block;
+
max-width: 100%;
+
+
&.left {
+
margin-right: auto;
+
}
+
+
&.center {
+
margin-left: auto;
+
margin-right: auto;
+
}
+
+
&.right {
+
margin-left: auto;
+
}
+
}
+
+
p {
+
margin-bottom: 20px;
+
}
+
+
figure {
+
display: table;
+
max-width: 100%;
+
margin: 25px 0;
+
+
&.left {
+
// img {
+
margin-right: auto;
+
// }
+
}
+
+
&.center {
+
// img {
+
margin-left: auto;
+
margin-right: auto;
+
// }
+
}
+
+
&.right {
+
// img {
+
margin-left: auto;
+
// }
+
}
+
+
figcaption {
+
font-size: 14px;
+
padding: 5px 10px;
+
margin-top: 5px;
+
background: var(--accent);
+
color: var(--background);
+
+
&.left {
+
text-align: left;
+
}
+
+
&.center {
+
text-align: center;
+
}
+
+
&.right {
+
text-align: right;
+
}
+
}
+
}
+
+
code {
+
font-family: 'Fira Code', Menlo, DejaVu Sans Mono, Monaco, Consolas, Ubuntu Mono, monospace;
+
font-feature-settings: normal;
+
background: var(--accent-alpha-20);
+
color: var(--accent);
+
padding: 1px 6px;
+
margin: 0 2px;
+
font-size: .95rem;
+
}
+
+
pre {
+
font-family: 'Fira Code', Menlo, DejaVu Sans Mono, Monaco, Consolas, Ubuntu Mono, monospace;
+
font-feature-settings: "liga";
+
padding: 20px 10px;
+
font-size: .95rem;
+
overflow: auto;
+
border-top: 1px solid rgba(255, 255, 255, .1);
+
border-bottom: 1px solid rgba(255, 255, 255, .1);
+
+
+ pre {
+
border-top: 0;
+
margin-top: -40px;
+
}
+
+
@media (max-width: $phone-max-width) {
+
white-space: pre-wrap;
+
word-wrap: break-word;
+
}
+
+
code {
+
background: none !important;
+
margin: 0;
+
padding: 0;
+
font-size: inherit;
+
border: none;
+
+
table {
+
table-layout: auto;
+
border-collapse: collapse;
+
box-sizing: border-box;
+
width: 100%;
+
margin: 00px 0;
+
}
+
+
table, th, td {
+
border: none;
+
padding: 0px;
+
}
+
+
table tr td:first-child {
+
padding-right: 10px;
+
}
+
+
}
+
}
+
+
blockquote {
+
border-top: 1px solid var(--accent);
+
border-bottom: 1px solid var(--accent);
+
margin: 40px 0;
+
padding: 25px;
+
+
@media (max-width: $phone-max-width) {
+
padding-right: 0;
+
}
+
+
&:before {
+
content: '”';
+
font-family: Georgia, serif;
+
font-size: 3.875rem;
+
position: absolute;
+
left: -40px;
+
top: -20px;
+
}
+
+
p:first-of-type {
+
margin-top: 0;
+
}
+
+
p:last-of-type {
+
margin-bottom: 0;
+
}
+
+
p {
+
position: relative;
+
}
+
+
p:before {
+
content: '>';
+
display: block;
+
position: absolute;
+
left: -25px;
+
color: var(--accent);
+
}
+
}
+
+
table {
+
table-layout: fixed;
+
border-collapse: collapse;
+
width: 100%;
+
margin: 40px 0;
+
}
+
+
table, th, td {
+
border: 1px dashed var(--accent);
+
padding: 10px;
+
}
+
+
th {
+
color: var(--accent);
+
}
+
+
ul, ol {
+
margin-left: 30px;
+
padding: 0;
+
+
li {
+
position: relative;
+
margin-top: 5px;
+
margin-bottom: 5px;
+
}
+
+
@media (max-width: $phone-max-width) {
+
margin-left: 20px;
+
}
+
+
ul, ol {
+
margin-top: 10px;
+
margin-bottom: 10px;
+
}
+
}
+
+
ol ol {
+
list-style-type: lower-alpha;
+
}
+
+
.container {
+
display: flex;
+
flex-direction: column;
+
padding: 40px;
+
max-width: 864px;
+
min-height: 100vh;
+
border-right: 1px solid rgba(255, 255, 255, 0.1);
+
+
&.full,
+
&.center {
+
border: none;
+
margin: 0 auto;
+
}
+
+
&.full {
+
max-width: 100%;
+
}
+
+
@media (max-width: $phone-max-width) {
+
padding: 20px;
+
}
+
}
+
+
.content {
+
display: flex;
+
}
+
+
hr {
+
width: 100%;
+
border: none;
+
background: var(--border-color);
+
height: 1px;
+
}
+
+
.hidden {
+
display: none;
+
}
+63
themes/zerm/sass/_pagination.scss
···
···
+
@import 'variables';
+
+
.pagination {
+
margin-top: 50px;
+
+
&__buttons {
+
display: flex;
+
align-items: center;
+
justify-content: center;
+
+
// @media (max-width: $phone-max-width) {
+
// flex-direction: column;
+
// }
+
+
a {
+
text-decoration: none;
+
}
+
}
+
}
+
+
.button {
+
position: relative;
+
display: inline-flex;
+
align-items: center;
+
justify-content: center;
+
font-size: 1rem;
+
border-radius: 8px;
+
max-width: 40%;
+
padding: 0;
+
cursor: pointer;
+
appearance: none;
+
+
+ .button {
+
margin-left: 10px;
+
}
+
+
// @media (max-width: $phone-max-width) {
+
// max-width: 80%;
+
// }
+
+
a {
+
display: flex;
+
padding: 8px 16px;
+
text-decoration: none;
+
text-overflow: ellipsis;
+
white-space: nowrap;
+
overflow: hidden;
+
}
+
+
&__text {
+
text-overflow: ellipsis;
+
white-space: nowrap;
+
overflow: hidden;
+
}
+
+
&.next .button__icon {
+
margin-left: 8px;
+
}
+
+
&.previous .button__icon {
+
margin-right: 8px;
+
}
+
}
+114
themes/zerm/sass/_post.scss
···
···
+
@import "variables";
+
+
// .posts {
+
// margin: 0 auto;
+
// }
+
+
.post {
+
width: 100%;
+
text-align: left;
+
margin: 20px auto;
+
padding: 20px 0;
+
+
@media (max-width: $tablet-max-width) {
+
max-width: 660px;
+
}
+
+
&:not(:last-of-type) {
+
border-bottom: 1px solid var(--border-color);
+
}
+
+
// %meta {
+
// font-size: 1rem;
+
// margin-bottom: 10px;
+
// color: var(--accent-alpha-70);
+
// }
+
+
// &-meta {
+
// @extend %meta;
+
// }
+
// &-meta-inline {
+
// @extend %meta;
+
+
// display: inline;
+
// }
+
+
&-meta {
+
font-size: 1rem;
+
margin-bottom: 10px;
+
color: var(--accent-alpha-70);
+
}
+
+
&-title {
+
--border: 3px dotted var(--accent);
+
position: relative;
+
color: var(--accent);
+
margin: 0 0 15px;
+
padding-bottom: 15px;
+
border-bottom: var(--border);
+
+
&:after {
+
content: '';
+
position: absolute;
+
bottom: 2px;
+
display: block;
+
width: 100%;
+
border-bottom: var(--border);
+
}
+
+
a {
+
text-decoration: none;
+
}
+
}
+
+
&-content {
+
margin-top: 30px;
+
}
+
+
ul {
+
list-style: none;
+
+
li:before {
+
content: '⦿';
+
position: absolute;
+
left: -20px;
+
color: var(--accent);
+
}
+
ul {
+
+
li:before {
+
content: '■';
+
position: absolute;
+
left: -20px;
+
color: var(--accent);
+
}
+
+
ul {
+
+
li:before {
+
content: '►';
+
position: absolute;
+
left: -20px;
+
color: var(--accent);
+
}
+
}
+
}
+
}
+
}
+
+
// TODO: try adapting this using a properly nested selector in the block above
+
// for ul items.
+
.tag-list {
+
@media(max-width: $phone-max-width) {
+
margin-left: 5%;
+
}
+
}
+
+
.footnote-definition {
+
color: var(--accent);
+
+
p {
+
display: inline;
+
color: var(--footnote-color);
+
}
+
}
+3
themes/zerm/sass/_semantic.scss
···
···
+
section, article, aside, footer, header, nav {
+
display: block;
+
}
+6
themes/zerm/sass/_toc.scss
···
···
+
div.toc {
+
--shadow-color: var(--accent-alpha-70);
+
--shadow: 10px 10px var(--shadow-color);
+
@include menu;
+
margin: 20px 0;
+
}
+10
themes/zerm/sass/_variables.scss
···
···
+
:root {
+
// *NOTE*:
+
// ------------------------------------------------
+
//Keep the same as the values in variables.scss!!!!!
+
--phoneWidth: (max-width: 684px);
+
--tabletWidth: (max-width: 900px);
+
}
+
+
$phone-max-width: 683px;
+
$tablet-max-width: 899px;
+9
themes/zerm/sass/color/blue.scss
···
···
+
:root {
+
--accent: rgb(35,176,255);
+
--accent-alpha-20: rgba(35,176,255,.2);
+
--accent-alpha-70: rgba(35,176,255,.7);
+
--background: #1D1E28;
+
--color: whitesmoke;
+
--border-color: rgba(255, 255, 255, .1);
+
--footnote-color: rgba(255, 255, 255, .5);
+
}
+12
themes/zerm/sass/color/green.scss
···
···
+
:root {
+
// --accent: rgb(120,226,160);
+
// --accent-alpha-20: rgba(120,226,160,.2);
+
// --accent-alpha-70: rgba(120,226,160,.7);
+
--accent: rgb(72, 251, 53);
+
--accent-alpha-20: rgba(72, 251, 53,.2);
+
--accent-alpha-70: rgba(72, 251, 53,.7);
+
--background: #1F222A;
+
--color: whitesmoke;
+
--border-color: rgba(255, 255, 255, .1);
+
--footnote-color: rgba(255, 255, 255, .5);
+
}
+9
themes/zerm/sass/color/orange.scss
···
···
+
:root {
+
--accent: rgb(255,168,106);
+
--accent-alpha-20: rgba(255, 168, 106, .2);
+
--accent-alpha-70: rgba(255, 168, 106,.7);
+
--background: #211f1a;
+
--color: whitesmoke;
+
--border-color: rgba(255, 255, 255, .1);
+
--footnote-color: rgba(255, 255, 255, .5);
+
}
+9
themes/zerm/sass/color/pink.scss
···
···
+
:root {
+
--accent: rgb(238,114,241);
+
--accent-alpha-20: rgba(238,114,241,.2);
+
--accent-alpha-70: rgba(238,114,241,.7);
+
--background: #21202C;
+
--color: whitesmoke;
+
--border-color: rgba(255, 255, 255, .1);
+
--footnote-color: rgba(255, 255, 255, .5);
+
}
+9
themes/zerm/sass/color/red.scss
···
···
+
:root {
+
--accent: rgb(255,98,102);
+
--accent-alpha-20: rgba(255,98,102,.2);
+
--accent-alpha-70: rgba(255,98,102,.7);
+
--background: #221F29;
+
--color: whitesmoke;
+
--border-color: rgba(255, 255, 255, .1);
+
--footnote-color: rgba(255, 255, 255, .5);
+
}
+10
themes/zerm/sass/style.scss
···
···
+
@import 'buttons';
+
@import 'font';
+
@import 'header';
+
@import 'logo';
+
@import 'main';
+
@import 'post';
+
@import 'pagination';
+
@import 'footer';
+
@import 'semantic';
+
@import 'toc';
themes/zerm/screenshot.png

This is a binary file and will not be displayed.

+1
themes/zerm/static/assets/js/main.js
···
···
+
!function(e){var t={};function n(o){if(t[o])return t[o].exports;var r=t[o]={i:o,l:!1,exports:{}};return e[o].call(r.exports,r,r.exports,n),r.l=!0,r.exports}n.m=e,n.c=t,n.d=function(e,t,o){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:o})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var o=Object.create(null);if(n.r(o),Object.defineProperty(o,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var r in e)n.d(o,r,function(t){return e[t]}.bind(null,r));return o},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="",n(n.s=0)}([function(e,t,n){n(1),e.exports=n(2)},function(e,t){},function(e,t){var n=document.querySelector(".container"),o=document.querySelector(".menu"),r=document.querySelector(".menu-trigger"),i=(document.querySelector(".menu__inner--desktop"),document.querySelector(".menu__sub-inner-more-trigger")),u=document.querySelector(".menu__sub-inner-more"),c=getComputedStyle(document.body).getPropertyValue("--phoneWidth"),d=function(){return window.matchMedia(c).matches},s=function(){r&&r.classList.toggle("hidden",!d()),o&&o.classList.toggle("hidden",d()),u&&u.classList.toggle("hidden",!d())};o&&o.addEventListener("click",function(e){return e.stopPropagation()}),u&&u.addEventListener("click",function(e){return e.stopPropagation()}),s(),document.body.addEventListener("click",function(){d()||!u||u.classList.contains("hidden")?d()&&!o.classList.contains("hidden")&&o.classList.add("hidden"):u.classList.add("hidden")}),window.addEventListener("resize",s),r&&r.addEventListener("click",function(e){e.stopPropagation(),o&&o.classList.toggle("hidden")}),i&&i.addEventListener("click",function(e){e.stopPropagation(),u&&u.classList.toggle("hidden"),u.getBoundingClientRect().right>n.getBoundingClientRect().right&&(u.style.left="auto",u.style.right=0)})}]);
+1
themes/zerm/templates/.gitignore
···
···
+
!tags
+12
themes/zerm/templates/404.html
···
···
+
{% extends "index.html" -%}
+
+
{%- block title %}
+
<title>Page not found</title>
+
{%- endblock title -%}
+
+
{%- block main -%}
+
+
Sorry, this page doesn't exist.
+
+
Go back&nbsp<a href="{%- if config.extra.logo_home_link -%}{{ config.extra.logo_home_link }}{%- else -%}{{ config.base_url }}{%- endif -%}">home?</a>
+
{%- endblock main -%}
+1
themes/zerm/templates/anchor-link.html
···
···
+
<a class="zola-anchor" href="#{{ id }}" aria-label="Anchor link for: {{ id }}">§</a>
+18
themes/zerm/templates/categories/list.html
···
···
+
{% extends "index.html" %}
+
{%- block main -%}
+
<div class="post">
+
+
<h1 class="post-title">{categories}</h1>
+
<ul>
+
{%- for cat in terms -%}
+
<li class="tag-list">
+
<a href="{{ get_taxonomy_url(kind="categories", name=cat.name) }}">{{ "{" }}{{ cat.name }}{{ "}" }}</a>
+
({{ cat.pages | length }} post{{ cat.pages | length | pluralize }})
+
</li>
+
{# End of pagination for-loop #}
+
{%- endfor -%}
+
{#- I don't put pagination here like Terminal does. I don't like how
+
the buttons move with the size of post entries in the content div. -#}
+
</ul>
+
</div>
+
{%- endblock main -%}
+30
themes/zerm/templates/categories/single.html
···
···
+
{%- extends "index.html"-%}
+
{%- block main-%}
+
<div class="post">
+
<h1 class="post-title">
+
categories ∋ {{ "{" }}{{ term.name }}{{ "}" }}
+
({{ term.pages | length }} post{{ term.pages | length | pluralize }})
+
</h1>
+
+
<ul>
+
{%- for post in term.pages -%}
+
<li class="tag-list">
+
{{ post.date | date(format="%Y.%m.%d") }}
+
:: <a href="{{ post.permalink }}">{{ post.title }}</a>
+
:: {{ "{" }}
+
{%- for cat in post.taxonomies["categories"] -%}
+
{%- set _cat = get_taxonomy_url(kind="categories", name=cat) -%}
+
{%- if loop.last -%}
+
<a href="{{ _cat }}">{{ cat }}</a>
+
{%- else -%}
+
<a href="{{ _cat }}">{{ cat }}</a>,&nbsp;
+
{%- endif -%}
+
{% endfor %}{{ "}" }}
+
</li>
+
{# End of pagination for-loop #}
+
{%- endfor -%}
+
{#- I don't put pagination here like Terminal does. I don't like how
+
the buttons move with the size of post entries in the content div. -#}
+
</ul>
+
</div>
+
{%- endblock main-%}
+97
themes/zerm/templates/index.html
···
···
+
{% import "macros/head.html" as head -%}
+
{% import "macros/logo.html" as logo -%}
+
{% import "macros/header.html" as header -%}
+
{% import "macros/extended_header.html" as extended_header -%}
+
{% import "macros/lists.html" as lists -%}
+
{% import "macros/posts.html" as posts -%}
+
{% import "macros/social.html" as social -%}
+
{% import "macros/utils.html" as utils -%}
+
{% import "macros/menu.html" as menu -%}
+
{% import "macros/pagination.html" as pagination -%}
+
{% import "macros/footer.html" as footer -%}
+
{% import "macros/extended_footer.html" as extended_footer -%}
+
{% import "macros/comments.html" as comments -%}
+
+
<!DOCTYPE html>
+
<html lang="{{ lang }}">
+
<head>
+
{%- block title -%}
+
<title>{{ config.title }}</title>
+
{%- endblock title -%}
+
+
{%- block general_meta -%}
+
{{ head::general_meta() }}
+
{%- endblock general_meta -%}
+
+
{%- block og_preview -%}
+
{{ social::og_preview() }}
+
{%- endblock og_preview -%}
+
+
{%- block fonts -%}
+
{{ head::fonts() }}
+
{%- endblock fonts -%}
+
+
{%- block css -%}
+
{{ head::styling() }}
+
{%- endblock css -%}
+
+
{%- block favicon -%}
+
{{ head::favicon() }}
+
{%- endblock favicon -%}
+
+
{%- block rss -%}
+
{{ head::rss() }}
+
{%- endblock rss -%}
+
+
{%- block math -%}
+
{%- endblock math -%}
+
</head>
+
<body>
+
{#
+
"container full" when width == True, regardless of center
+
"container center" when only center == True
+
"container" when both false.
+
#}
+
{%- if config.extra.full_width -%}
+
{%- set container = "container full" -%}
+
{%- elif config.extra.center -%}
+
{%- set container = "container center" -%}
+
{%- else -%}
+
{%- set container = "container" -%}
+
{%- endif -%}
+
+
<div class="{{ container }}">
+
{%- block header -%}
+
{{ header::header() }}
+
{%- endblock header -%}
+
<div class="content">
+
{%- block main -%}
+
{{ lists::list_pages() }}
+
{%- endblock main -%}
+
</div>
+
{#-
+
I keep pagination out of list, unlike Terminal, because I don't
+
like how the pagination buttons move with the width of the content
+
div.
+
-#}
+
{%- block pagination -%}
+
{{ pagination::paginate() }}
+
{%- endblock pagination -%}
+
+
{%- block footer -%}
+
<footer class="footer">
+
<div class="footer__inner">
+
{%- block copyright -%}
+
{{ footer::copyright() }}
+
{%- endblock copyright -%}
+
+
{%- block script -%}
+
{{ footer::script() }}
+
{%- endblock script -%}
+
</div>
+
{{ extended_footer::extended_footer() }}
+
</footer>
+
{%- endblock footer -%}
+
</div>
+
</body>
+
</html>
+30
themes/zerm/templates/macros/comments.html
···
···
+
{% macro comments() %}
+
{% if config.extra.disqus.enabled is defined and config.extra.disqus.enabled == true %}
+
{{ comments::disqus() }}
+
{% endif %}
+
{% endmacro comments %}
+
+
+
{% macro disqus() %}
+
+
<div id="disqus_thread"></div>
+
<script>
+
/**
+
* RECOMMENDED CONFIGURATION VARIABLES: EDIT AND UNCOMMENT THE SECTION BELOW TO INSERT DYNAMIC VALUES FROM YOUR PLATFORM OR CMS.
+
* LEARN WHY DEFINING THESE VARIABLES IS IMPORTANT: https://disqus.com/admin/universalcode/#configuration-variables */
+
+
var disqus_config = function () {
+
this.page.url = '{{ page.permalink | safe }}'; // Replace PAGE_URL with your page's canonical URL variable
+
this.page.identifier = '{{ page.permalink | safe }}'; // Replace PAGE_IDENTIFIER with your page's unique identifier variable
+
};
+
+
(function() { // DON'T EDIT BELOW THIS LINE
+
var d = document, s = d.createElement('script');
+
s.src = 'https://{{config.extra.disqus.short_name}}.disqus.com/embed.js';
+
s.setAttribute('data-timestamp', +new Date());
+
(d.head || d.body).appendChild(s);
+
})();
+
</script>
+
<noscript>Please enable JavaScript to view the <a href="https://disqus.com/?ref_noscript">comments powered by Disqus.</a></noscript>
+
+
{% endmacro disqus %}
+2
themes/zerm/templates/macros/extended_footer.html
···
···
+
{% macro extended_footer() %}
+
{% endmacro extended_footer %}
+2
themes/zerm/templates/macros/extended_header.html
···
···
+
{% macro extended_header() %}
+
{% endmacro extended_header %}
+14
themes/zerm/templates/macros/footer.html
···
···
+
{% macro copyright() %}
+
{%- if config.extra.copyright -%}
+
<div class="copyright copyright--user">{{ config.extra.copyright | safe }}</div>
+
{%- else -%}
+
<div class="copyright">
+
<span>© {{ now() | date(format="%Y") }} <a href="https://github.com/ejmg/zerm">zerm</a> :: Powered by <a href="https://www.getzola.org/">Zola</a></span>
+
<span>:: Theme made by <a href="https://github.com/ejmg">ejmg</a></span>
+
</div>
+
{%- endif -%}
+
{% endmacro copyright %}
+
+
{% macro script() %}
+
<script type="text/javascript" src="{{ get_url(path="assets/js/main.js") }}"></script>
+
{% endmacro script %}
+84
themes/zerm/templates/macros/head.html
···
···
+
{#- TODO:
+
- [x] add rss
+
- [x] favicons
+
- [x] media preview
+
- [ ] twitter
+
- [ ] maybe google adsense
+
-#}
+
+
{% macro fonts() %}
+
<link rel="preload" href="{{ get_url(path="/assets/fonts/FiraCode-Regular.woff2") }}" as="font" type="font/woff2" crossorigin="anonymous">
+
<link rel="preload" href="{{ get_url(path="/assets/fonts/FiraCode-Bold.woff2") }}" as="font" type="font/woff2" crossorigin="anonymous">
+
{% endmacro fonts %}
+
+
{% macro styling() %}
+
<link rel="stylesheet" href="{{ get_url(path="style.css", cachebust=true) }}">
+
{% if config.extra.theme_color != "orange" -%}
+
{% set color = "color/" ~ config.extra.theme_color ~ ".css" -%}
+
<link rel="stylesheet" href="{{ get_url(path=color, cachebust=true) }}">
+
{%- else -%}
+
<link rel="stylesheet" href=" {{ get_url(path="color/orange.css", cachebust=true) }}">
+
{% endif %}
+
{%- if config.extra.custom_css is defined -%}
+
<link rel="stylesheet" href="{{ get_url(path="custom.css", cachebust=true) }}">
+
{% endif %}
+
{% endmacro styling %}
+
+
{% macro favicon() %}
+
{%- if config.extra.favicon is defined -%}
+
<link rel="shortcut icon" href="{{ get_url(path=config.extra.favicon) }}" type="image/x-icon" />
+
{%- endif -%}
+
{% endmacro favicon %}
+
+
{% macro rss() %}
+
{%- if config.generate_feed -%}
+
<link rel="alternate" type={% if config.feed_filename == "atom.xml" %}"application/atom+xml"{% else %}"application/rss+xml"{% endif %} title="{{ config.title }} RSS" href="{{ get_url(path=config.feed_filename) }}">
+
{%- endif -%}
+
{% endmacro rss %}
+
+
{% macro general_meta() %}
+
<meta http-equiv="content-type" content="text/html; charset=utf-8">
+
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1">
+
{%- if page.title -%}
+
<meta name="description" content="{{ config.description }} {{ page.title }} {{ page.description }}"/>
+
{%- else -%}
+
<meta name="description" content="{{ config.description }}"/>
+
{%- endif -%}
+
+
{%- if page.taxonomies.tags or page.taxonomies.categories -%}
+
<meta name="keywords" content="
+
{%- if page.taxonomies.categories -%}
+
{%- for cat in page.taxonomies.categories -%}
+
{{ cat }}, {% endfor -%}
+
{%- endif -%}
+
+
{%- if page.taxonomies.tags -%}
+
{%- for tag in page.taxonomies.tags -%}
+
{%- if loop.last -%}
+
{{ tag }}
+
{%- else -%}
+
{{ tag }}, {% endif -%}
+
{%- endfor -%}
+
{%- endif -%}
+
" />
+
{%- endif -%}
+
{% endmacro general_meta %}
+
+
{% macro katex() %}
+
{% if config.extra.enable_katex %}
+
<link rel="stylesheet" href="{{ get_url(path="assets/katex/katex.min.css") }}">
+
+
<script defer type="text/javascript" src="{{ get_url(path="assets/katex/katex.min.js") | safe }}"></script>
+
<script defer type="text/javascript" src="{{ get_url(path="assets/katex/mathtex-script-type.min.js") | safe }}"></script>
+
+
<script defer src="{{ get_url(path="assets/katex/auto-render.min.js") | safe }}"
+
onload="renderMathInElement(document.body, {
+
delimiters: [
+
{left: '$$', right: '$$', display: true},
+
{left: '$', right: '$', display: false},
+
{left: '\\(', right: '\\)', display: false},
+
{left: '\\[', right: '\\]', display: true}
+
]
+
});"></script>
+
{% endif %}
+
{% endmacro katek %}
+16
themes/zerm/templates/macros/header.html
···
···
+
{% macro header() %}
+
<header class="header">
+
<div class="header__inner">
+
<div class="header__logo">
+
{{ logo::logo() }}
+
</div>
+
<div class="menu-trigger">menu</div>
+
</div>
+
{# Check if there are menu items to render, yes if > 0 #}
+
{%- set num = config.extra.main_menu | length -%}
+
{% if num > 0 -%}
+
{{ menu::menu() }}
+
{% endif -%}
+
</header>
+
{% endmacro header %}
+
+39
themes/zerm/templates/macros/lists.html
···
···
+
{% macro list_pages() %}
+
<section class="posts">
+
+
{%- for page in paginator.pages -%}
+
+
<div class="post on-list">
+
<header>
+
<h1 class="post-title">
+
<a href="{{ page.permalink }}">{{ page.title }}</a>
+
</h1>
+
</header>
+
+
+
{{ posts::meta(page=page, author=config.extra.show_author) }}
+
+
{#- NOTE -#}
+
{#- -------------------------------- -#}
+
{#- Skipping the Cover page implementation. Not included/covered for now. -#}
+
+
<div class="post-content">
+
{% if page.description -%}
+
{{ page.description }}
+
{#- end if-check for description -#}
+
{% elif page.summary -%}
+
{{ page.summary | safe }}
+
{% endif -%}
+
</div>
+
{% if page.description or page.summary -%}
+
<div>
+
<a class="read-more button" href="{{ page.permalink }}">{{ config.extra.read_more }} →</a>
+
</div>
+
{% endif -%}
+
</div>
+
{# End of pagination for-loop #}
+
{%- endfor -%}
+
{#- I don't put pagination here like Terminal does. I don't like how
+
the buttons move with the size of post entries in the content div. -#}
+
</section>
+
{% endmacro list_pages %}
+7
themes/zerm/templates/macros/logo.html
···
···
+
{%- macro logo() -%}
+
<a href="{%- if config.extra.logo_home_link -%}{{ config.extra.logo_home_link }}{%- else -%}{{ config.base_url }}{%- endif -%}">
+
<div class="logo">
+
{{config.extra.logo_text | default(value="Terminal") }}
+
</div>
+
</a>
+
{%- endmacro logo -%}
+58
themes/zerm/templates/macros/menu.html
···
···
+
{% macro menu() %}
+
<nav class="menu">
+
<ul class="menu__inner menu__inner--desktop">
+
{#
+
I am almost certain this check isn't necessary, that it will always
+
return true, but the only alternative I can think of is the case where
+
this config value simply doesn't exist (null)? IDK, just copying from
+
original implementation for now.
+
#}
+
{% if config.extra.show_menu_items is defined -%}
+
{% for menu_item in config.extra.main_menu | slice(end=config.extra.show_menu_items) -%}
+
{#
+
Original theme has sub-children checks on main-menu, not worrying about that here.
+
#}
+
<li>
+
<a href="{{ menu::get_link(item=menu_item) }}">{{ menu_item.name }}</a>
+
</li>
+
{% endfor -%}
+
+
{%- set main_len = config.extra.main_menu | length -%}
+
{%- set show_len = config.extra.show_menu_items -%}
+
+
{%- if main_len > show_len -%}
+
<ul class="menu__sub-inner">
+
<li class="menu__sub-inner-more-trigger">{{ config.extra.menu_more }} ▾</li>
+
<ul class="menu__sub-inner-more hidden">
+
{{ menu::items(menu=config.extra.main_menu | slice(start=config.extra.show_menu_items)) }}
+
</ul>
+
</ul>
+
{%- endif -%}
+
{# Continues the original if-check at top of file for show_menu_items #}
+
{% else -%}
+
{{ menu::items(menu=config.extra.main_menu) }}
+
{%- endif -%}
+
</ul>
+
+
<ul class="menu__inner menu__inner--mobile">
+
{{ menu::items(menu=config.extra.main_menu) }}
+
</ul>
+
</nav>
+
{% endmacro menu %}
+
+
{% macro items(menu) %}
+
{%- for menu_item in menu -%}
+
{# skipping sub-child check #}
+
<li>
+
<a href="{{ menu::get_link(item=menu_item) }}">{{ menu_item.name }}</a>
+
</li>
+
{%- endfor-%}
+
{% endmacro items %}
+
+
{% macro get_link(item) %}
+
{% if item.external is defined and item.external == true %}
+
{{ item.url }}
+
{% else %}
+
{{ get_url(path=item.url) }}
+
{% endif %}
+
{% endmacro get_link %}
+24
themes/zerm/templates/macros/pagination.html
···
···
+
{% macro paginate() %}
+
<div class="pagination">
+
<div class="pagination__buttons">
+
{% if paginator.previous -%}
+
<span class="button previous">
+
<a href="{{ paginator.previous }}">
+
<span class="button__icon">←</span>
+
<span class="button__text">newer</span>
+
</a>
+
</span>
+
{# end of if-check for page.previous #}
+
{% endif -%}
+
{% if paginator.next -%}
+
{# end of if-check for page.next #}
+
<span class="button next">
+
<a href="{{ paginator.next }}">
+
<span class="button__text">older</span>
+
<span class="button__icon">→</span>
+
</a>
+
</span>
+
{% endif-%}
+
</div>
+
</div>
+
{% endmacro paginate %}
+100
themes/zerm/templates/macros/posts.html
···
···
+
{% macro section_meta(section, author) %}
+
<div class="post-meta">
+
<span class="post-date">
+
+
{%- if section.extra["date"] -%}
+
{{ section.extra["date"] | date(format="%Y.%m.%d") }}
+
{# end of section.date if-check #}
+
{%- endif -%}
+
</span>
+
+
<span class="post-author">
+
{%- if author -%}
+
{{ utils::author(page=section) }}
+
{%- endif -%}
+
</span>
+
</div>
+
{% endmacro section_meta %}
+
+
{% macro langs(page) %}
+
{% if page.translations | length > 1 %}
+
<div class="post-langs" style="opacity: .5;">
+
<span>Translations: </span>{# TODO translate the span content too #}
+
{% for translated in page.translations %}
+
<a href="{{ translated.permalink }}">{{ translated.lang }}</a>
+
{% endfor %}
+
</div>
+
{% endif %}
+
{% endmacro langs %}
+
+
{% macro meta(page, author) %}
+
<div class="post-meta">
+
<span class="post-date">
+
{%- if page.date -%}
+
{{ page.date | date(format="%Y.%m.%d") }}
+
{# end of page.date if-check #}
+
{%- endif -%}
+
+
{%- if page.updated -%}
+
[Updated: {{ page.updated | date(format="%Y.%m.%d") }}]
+
{# end of page.updated if-check #}
+
{%- endif -%}
+
</span>
+
+
<span class="post-author">
+
{%- if author -%}
+
{{ utils::author(page=page) }}
+
{%- endif -%}
+
</span>
+
+
{{ posts::taxonomies(taxonomy=page.taxonomies,
+
disp_cat=config.extra.show_categories,
+
disp_tag=config.extra.show_tags) }}
+
</div>
+
{% endmacro meta %}
+
+
{% macro taxonomies(taxonomy, disp_cat, disp_tag) %}
+
+
{% if disp_cat and disp_tag -%}
+
{% if taxonomy.categories -%}
+
{{ posts::categories(categories=taxonomy.categories) }}
+
{# end if-check for categories #}
+
{%- endif -%}
+
+
{% if taxonomy.tags -%}
+
{{ posts::tags(tags=taxonomy.tags) }}
+
{# end if-check for tags #}
+
{% endif -%}
+
{% elif disp_cat -%}
+
{% if taxonomy.categories-%}
+
{{ posts::categories(categories=taxonomy.categories) }}
+
{# end if-check for categories #}
+
{% endif -%}
+
{% elif disp_tag -%}
+
{% if taxonomy.tags -%}
+
{{ posts::tags(tags=taxonomy.tags) }}
+
{# end if-check for tags #}
+
{% endif -%}
+
{# end if-check for BOTH disp bools #}
+
{% endif -%}
+
{% endmacro taxonomies %}
+
+
{% macro categories(categories) %}
+
:: {
+
{%- for cat in categories -%}
+
{%- if loop.last -%}
+
<a href="{{ get_taxonomy_url(kind="categories", name=cat ) }}">{{ cat }}</a>
+
{%- else -%}
+
<a href="{{ get_taxonomy_url(kind="categories", name=cat ) }}">{{ cat }}</a>,
+
{# end of if-check for whether last item or not #}
+
{%- endif -%}
+
{%- endfor -%}} {# <--- NOTE: OPEN CURLY BRACE #}
+
{% endmacro categories %}
+
+
{% macro tags(tags) %}
+
::
+
{% for tag in tags -%}
+
#<a href="{{get_taxonomy_url(kind="tags", name=tag )}}">{{ tag }}</a>
+
{# end of tags for-loop #}
+
{% endfor -%}
+
{% endmacro tags %}
+46
themes/zerm/templates/macros/social.html
···
···
+
{% macro og_preview() %}
+
<meta property="og:title" content="{{ social::og_title() }}" />
+
<meta property="og:type" content="website"/>
+
{%- if current_url -%}
+
<meta property="og:url" content="{{ current_url }}"/>
+
{%- endif -%}
+
<meta property="og:description" content="{{ social::og_description() }}"/>
+
{%- if config.extra.og_preview_img -%}
+
<meta property="og:image" content="{{ get_url(path=config.extra.og_preview_img) }}"/>
+
{%- endif -%}
+
{% endmacro og_preview %}
+
+
{% macro og_description() %}
+
{%- if section -%}
+
{%- if section.description -%}
+
{{ section.description }}
+
{%- else -%}
+
{{ config.description }}
+
{%- endif -%}
+
{%- elif page -%}
+
{%- if page.summary | string -%}
+
{{ page.summary | striptags | truncate(length=200) }}
+
{%- elif page.description -%}
+
{{ page.description }}
+
{%- else -%}
+
{{ config.description }}
+
{%- endif -%}
+
{%- endif -%}
+
{% endmacro og_description %}
+
+
{% macro og_title() -%}
+
{{ config.title }} -&nbsp;
+
{%- if section -%}
+
{%- if section.title -%}
+
{{ section.title | striptags }}
+
{%- else -%}
+
{{ config.description }}
+
{%- endif -%}
+
{%- elif page -%}
+
{%- if page.title -%}
+
{{ page.title | striptags }}
+
{%- else -%}
+
{{ config.description }}
+
{%- endif -%}
+
{%- endif -%}
+
{% endmacro og_title %}
+61
themes/zerm/templates/macros/toc.html
···
···
+
{% macro toc (t) %}
+
{% if t %}
+
<div class="toc" id="nav-container">
+
<p class="toc-head">Table of Contents</p>
+
<div id="nav-content" >
+
<ul>
+
{% for h1 in page.toc %}
+
<li>
+
<a href="{{ h1.permalink | safe }}">{{ h1.title }}</a>
+
{% if h1.children %}
+
<ul>
+
{% for h2 in h1.children %}
+
<li>
+
<a href="{{ h2.permalink | safe }}">{{ h2.title }}</a>
+
</li>
+
{% if h2.children %}
+
<ul>
+
{% for h3 in h2.children %}
+
<li>
+
<a href="{{ h3.permalink | safe }}">{{ h3.title }}</a>
+
{% if h3.children %}
+
<ul>
+
{% for h4 in h3.children %}
+
<li>
+
<a href="{{ h4.permalink | safe }}">{{ h4.title }}</a>
+
</li>
+
{% if h4.children %}
+
<ul>
+
{% for h5 in h4.children %}
+
<li>
+
<a href="{{ h5.permalink | safe }}">{{ h5.title }}</a>
+
{% if h5.children %}
+
<ul>
+
{% for h6 in h5.children %}
+
<li>
+
<a href="{{ h5.permalink | safe }}">{{ h6.title }}</a>
+
</li>
+
{% endfor %}
+
</ul>
+
{% endif %}
+
</li>
+
{% endfor %}
+
</ul>
+
{% endif %}
+
{% endfor %}
+
</ul>
+
{% endif %}
+
</li>
+
{% endfor %}
+
</ul>
+
{% endif %}
+
{% endfor %}
+
</ul>
+
{% endif %}
+
</li>
+
{% endfor %}
+
</ul>
+
</div>
+
</div>
+
{% endif %}
+
{% endmacro %}
+8
themes/zerm/templates/macros/utils.html
···
···
+
{% macro author(page) %}
+
::
+
{% if page.extra.author -%}
+
{{ page.extra.author }}
+
{%- else -%}
+
{{ config.extra.author }}
+
{%- endif -%}
+
{%- endmacro author-%}
+41
themes/zerm/templates/page.html
···
···
+
{% import "macros/head.html" as head -%}
+
{% import "macros/toc.html" as toc -%}
+
{% extends "index.html" -%}
+
+
{%- block math -%}
+
{% if page.extra.math %}
+
{{ head::katex() }}
+
{% endif %}
+
{%- endblock math -%}
+
+
{%- block title %}
+
<title>{{ page.title }} - {{ config.extra.author }}</title>
+
{# TODO: make some kind of social media linking, i guess? #}
+
{#%- include "snippets/social.html" %#}
+
{%- endblock title -%}
+
+
{%- block main -%}
+
<article class="post">
+
<header>
+
<h1 class="post-title">
+
<a href="{{ page.permalink }}">{{ page.title }}</a>
+
</h1>
+
{{ posts::meta(page=page, author=config.extra.show_author) }}
+
{{ posts::langs(page=page) }}
+
+
{%- block ToC -%}
+
{%- if page.extra.ToC or config.extra.ToC -%}
+
{{ toc::toc(t=page.toc) }}
+
{%- endif -%}
+
{%- endblock ToC -%}
+
</header>
+
+
{#- Skipping logic for cover as was in original Terminal theme -#}
+
+
{{ page.content | safe }}
+
+
{{ comments::comments() }}
+
{# TODO: Decide if any sort of commenting functionality is desired? #}
+
{#%- include "snippets/comments.html" -%#}
+
</article>
+
{%- endblock main -%}
+24
themes/zerm/templates/section.html
···
···
+
{% extends "index.html" -%}
+
+
{%- block title %}
+
<title>{{ section.title }} - {{ config.extra.author }}</title>
+
{# TODO: make some kind of social media linking, i guess? #}
+
{#%- include "snippets/social.html" %#}
+
{%- endblock title -%}
+
+
{%- block main -%}
+
<article class="post">
+
<header>
+
<h1 class="post-title">
+
<a href="{{ section.permalink }}">{{ section.title }}</a>
+
</h1>
+
{{ posts::section_meta(section=section, author=config.extra.show_author) }}
+
</header>
+
+
{#- Skipping logic for cover as was in original Terminal theme -#}
+
+
{{ section.content | safe }}
+
{# TODO: Decide if any sort of commenting functionality is desired? #}
+
{#%- include "snippets/comments.html" -%#}
+
</article>
+
{%- endblock main -%}
+12
themes/zerm/templates/static.html
···
···
+
{% extends "index.html" %}
+
+
{% block header %}
+
<title>{{ page.title }} | elias </title>
+
{% endblock header %}
+
+
{% block content %}
+
<article>
+
<h1>{{ page.title }}</h1>
+
{{ page.content | safe }}
+
</article>
+
{% endblock content %}
+18
themes/zerm/templates/tags/list.html
···
···
+
{% extends "index.html" %}
+
{%- block main -%}
+
<div class="post">
+
+
<h1 class="post-title">#tags</h1>
+
<ul>
+
{%- for term in terms -%}
+
<li class="tag-list">
+
<a href="{{ get_taxonomy_url(kind="tags", name=term.name) }}">#{{ term.name }}</a>
+
({{ term.pages | length }} post{{ term.pages | length | pluralize}})
+
</li>
+
{# End of pagination for-loop #}
+
{%- endfor -%}
+
{#- I don't put pagination here like Terminal does. I don't like how
+
the buttons move with the size of post entries in the content div. -#}
+
</ul>
+
</div>
+
{%- endblock main -%}
+23
themes/zerm/templates/tags/single.html
···
···
+
{%- extends "index.html"-%}
+
{%- block main-%}
+
<div class="post">
+
<h1 class="post-title">
+
tags ∋ #{{ term.name }}
+
({{ term.pages | length }} post{{ term.pages | length | pluralize}})
+
</h1>
+
+
<ul>
+
{%- for post in term.pages -%}
+
<li class="tag-list">
+
{{ post.date | date(format="%Y.%m.%d") }}
+
:: <a href="{{ post.permalink }}">{{ post.title }}</a>
+
:: {% for tag in post.taxonomies["tags"] -%} <a href="{%- set _tag = get_taxonomy_url(kind="tags", name=tag) -%}{{ _tag }}">#{{ tag }}</a> {% endfor %}
+
</li>
+
{# End of pagination for-loop #}
+
{%- endfor -%}
+
{#- I don't put pagination here like Terminal does. I don't like how
+
the buttons move with the size of post entries in the content div. -#}
+
</ul>
+
+
</div>
+
{%- endblock main-%}
+26
themes/zerm/theme.toml
···
···
+
name = "zerm"
+
description = "A minimalistic and dark theme based on Radek Kozieł's theme for Hugo"
+
license = "MIT"
+
homepage = "https://github.com/ejmg/zerm"
+
# The minimum version of Zola required
+
min_version = "0.8.0"
+
# An optional live demo URL
+
demo = "https://zerm.ejmg.now.sh/"
+
+
# Any variable there can be overriden in the end user `config.toml`
+
# You don't need to prefix variables by the theme name but as this will
+
# be merged with user data, some kind of prefix or nesting is preferable
+
# Use snake_casing to be consistent with the rest of Zola
+
[extra]
+
+
# The theme author info: you!
+
[author]
+
name = "elias julian marko garcia"
+
homepage = "https://github.com/ejmg"
+
+
# If this is porting a theme from another static site engine, provide
+
# the info of the original author here
+
[original]
+
author = "Radek Kozieł"
+
homepage = "https://radoslawkoziel.pl/"
+
repo = "https://github.com/panr/hugo-theme-terminal"
themes/zerm/zerm-preview.png

This is a binary file and will not be displayed.