this repo has no description

Compare changes

Choose any two refs to compare.

Changed files
+3850 -216
.github
workflows
content
sass
static
styles
templates
themes
+2 -2
.github/workflows/rebuild.yml
···
name: Trigger Netlify rebuild
on:
schedule:
-
# Run every 4h
-
- cron: '0 */4 * * *'
+
# Run every 8h
+
- cron: '0 */8 * * *'
jobs:
trigger:
+3
.gitignore
···
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'
+
+
# vi: ft=ruby
+1
.vale.ini
···
[*.md]
BasedOnStyles = proselint, write-good
write-good.Passive = NO
+
write-good.TooWordy = NO
+11 -5
config.toml
···
theme = "zerm"
-
generate_feed = true
+
generate_feeds = true
minify_html = true
taxonomies = [
-
{name = "tags"},
-
{name = "categories"},
+
{name = "tags", feed = true},
]
[markdown]
···
[extra]
author = "Hauleth"
-
for_hire = false
+
for_hire = true
theme_color = "blue"
+
source = "https://tangled.sh/hauleth.dev/blog"
+
logo_text = "~hauleth"
logo_home_link = "/"
···
# 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://twitter.com/hauleth", name="twitter", 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"},
]
···
[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)
+6 -11
content/post/common-test-for-elixir.md
···
[taxonomies]
tags = [
-
"erlang",
"beam",
-
"elixir",
-
"testing",
-
"programming",
-
"common_test",
-
"commoner"
+
"testing"
]
[[extra.thanks]]
···
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
+
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 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:
+
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 informations (even coloured)
+
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)
+1 -5
content/post/eli5-ownership.md
···
[taxonomies]
tags = [
-
"rust",
-
"programming",
-
"ownership",
-
"eli5",
-
"borrowing"
+
"rust"
]
+++
+1 -4
content/post/elixir-application.md
···
[taxonomies]
tags = [
-
"elixir",
-
"erlang",
-
"beam",
-
"programming"
+
"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",
+
"beam",
"observability"
]
···
)
```
-
As we can see there, the report contains informations like:
+
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"
]
+++
-2
content/post/treachery-of-representation.md
···
[taxonomies]
tags = [
-
"programming",
-
"linguistic",
"culture"
]
+++
+2 -5
content/post/vim-for-elixir.md
···
[taxonomies]
tags = [
-
"elixir",
-
"erlang",
-
"vim",
-
"neovim",
-
"programming"
+
"beam",
+
"vim"
]
+++
+7 -9
content/post/who-watches-watchmen-i.md
···
[taxonomies]
tags = [
-
"elixir",
-
"programming",
-
"systemd",
-
"deployment"
+
"beam",
+
"systemd"
]
+++
···
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 informations about state of
+
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
-
withing given timespan after start of the VM, otherwise the process will be
+
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
···
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, it is just using `Environment=ERL_CRASH_DUMP_SECONDS=0`.
+
hard to achieve, just use `Environment=ERL_CRASH_DUMP_SECONDS=0`.
Our new, more secure, `hello.service` will look like:
···
This blog post is already quite lengthy, so I will split it into separate parts.
There probably will be 3 of them:
-
- <a href="#top">Part 1 - Basics, security, and FD passing (this one)</a>
-
- Part 2 - Socket activation
+
- [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."""
+
how to conceive plugin, how to go from an idea to the full fledged plugin."""
[taxonomies]
tags = [
-
"vim",
-
"viml"
+
"vim"
]
+++
+25 -9
flake.lock
···
{
"nodes": {
"flake-utils": {
+
"inputs": {
+
"systems": "systems"
+
},
"locked": {
-
"lastModified": 1656928814,
-
"narHash": "sha256-RIFfgBuKz6Hp89yRr7+NR5tzIAbn52h8vT6vXkYjZoM=",
+
"lastModified": 1731533236,
+
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide",
"repo": "flake-utils",
-
"rev": "7e2a3b3dfd9af950a856d66b0a7d01e3c18aa249",
+
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github"
},
"original": {
···
},
"nixpkgs": {
"locked": {
-
"lastModified": 1658430343,
-
"narHash": "sha256-cZ7dw+dyHELMnnMQvCE9HTJ4liqwpsIt2VFbnC+GNNk=",
-
"owner": "NixOS",
-
"repo": "nixpkgs",
-
"rev": "e2b34f0f11ed8ad83d9ec9c14260192c3bcccb0d",
-
"type": "github"
+
"lastModified": 0,
+
"narHash": "sha256-cnL5WWn/xkZoyH/03NNUS7QgW5vI7D1i74g48qplCvg=",
+
"path": "/nix/store/h15y13p2w17dhpiyh8pk42v1k4c38a0h-source",
+
"type": "path"
},
"original": {
"id": "nixpkgs",
···
"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"
}
}
},
+20 -16
flake.nix
···
outputs = { self, nixpkgs, flake-utils }:
flake-utils.lib.eachDefaultSystem (system:
let
-
pkgs = nixpkgs.legacyPackages.${system};
-
blog = pkgs.stdenv.mkDerivation {
+
pkgs = import nixpkgs { inherit system; };
+
blog = pkgs.stdenvNoCC.mkDerivation {
name = "hauleth-blog";
src = ./.;
nativeBuildInputs = [
pkgs.zola
-
pkgs.gitMinimal
];
buildPhase = ''
-
git submodule update --init --recursive --depth=1
-
zola build -o $out
-
'';
+
zola --version
+
zola build --output-dir $out
+
'';
dontInstall = true;
-
-
passthru = {
-
inherit (pkgs) zola;
-
};
};
-
in rec {
-
packages = flake-utils.lib.flattenTree {
+
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;
-
/* apps.hello = flake-utils.lib.mkApp { drv = packages.hello; }; */
-
/* defaultApp = apps.hello; */
devShells.default = pkgs.mkShell {
-
nativeBuildInputs = [
-
blog.zola
+
inputsFrom = [ blog ];
+
+
packages = [
+
# pkgs.netlify-cli
pkgs.vale
pkgs.mdl
];
+26 -5
netlify.toml
···
command = "zola build"
publish = "public/"
+
[build.environment]
+
ZOLA_VERSION = "0.20.0"
+
[context.deploy-preview]
-
command = "zola build --drafts"
+
command = "zola build --drafts --base-url $DEPLOY_PRIME_URL"
[[headers]]
for = "/*"
···
X-Frame-Options = "DENY"
X-XSS-Protection = "1; mode=block"
X-Content-Type-Options = "nosniff"
-
Content-Security-Policy = "default-src 'self'; script-src 'self' https://plausible.io; connect-src https://plausible.io; img-src https:"
+
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"
-
-
[[plugins]]
-
package = "netlify-plugin-submit-sitemap"
-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;
+32 -37
sass/_main.scss
···
@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;
-
font-size: 12pt;
-
line-height: 1.54;
background-color: var(--background);
-
color: var(--color);
+
// text-shadow: 0 0 3px currentcolor;
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 {
···
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 {
···
padding-right: 0;
}
-
&:before {
-
content: '”';
-
font-family: Georgia, serif;
-
font-size: 3.875rem;
-
position: absolute;
-
left: -40px;
-
top: -20px;
-
}
+
> :first-child {
+
margin-top: 0;
+
position: relative;
-
p:first-of-type {
-
margin-top: 0;
+
&:before {
+
content: '>';
+
display: block;
+
position: absolute;
+
left: -25px;
+
top: .1em;
+
color: var(--accent);
+
}
}
-
p:last-of-type {
+
> :last-child {
margin-bottom: 0;
-
}
-
-
p {
-
position: relative;
-
}
-
-
p:before {
-
content: '>';
-
display: block;
-
position: absolute;
-
left: -25px;
-
color: var(--accent);
}
}
···
// Todo change it to ::marker when Safari will support it
&::before {
+
margin-left: -2rem;
content: counters(li, ".") ". ";
}
}
}
-
-
.halmos {
-
text-align: right;
-
font-size: 1.5em;
-
}
+35 -1
sass/_post.scss
···
.post {
width: 100%;
-
text-align: left;
+
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;
···
.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;
+
$tablet-max-width: 1199px;
+1
sass/style.scss
···
@import 'post';
@import 'pagination';
@import 'footer';
+
@import 'rings';
:root {
--phoneWidth: (max-width: #{$phone-max-width + 1px});
+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
+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'
+
+
rule 'MD013', ignore_code_blocks: true
+46 -7
templates/index.html
···
{% extends "zerm/templates/index.html" %}
{% block fonts %}
-
{% endblock %}
+
{% endblock fonts %}
{% 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}}">
+
<link rel="alternate" type="application/atom+xml" title="{{ config.title }} Feed" href="{{ get_url(path="atom.xml", trailing_slash=false) }}">
{%- endif -%}
-
{% endblock %}
+
{% 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 -%}
···
{%- if config.extra.webmention -%}
<link rel="webmention" href="{{ config.extra.webmention }}" >
{%- endif -%}
-
{% endblock %}
+
{% 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>
+
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 async defer data-domain="hauleth.dev" src="https://plausible.io/js/plausible.js"></script>
+
<script defer data-domain="hauleth.dev" src="/js/script.js"></script>
{% endblock script %}
{% block css %}
···
{%- 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 %}
+
{% 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 %}
+10 -4
templates/macros/posts.html
···
[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,
···
{% endmacro tags %}
{% macro thanks(who) %}
-
{%- if who.why -%}
-
{{ who.name }} - {{ who.why }}
+
{%- 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 -%}
-
{{ who.name }}
+
<span class="p-name">{{ who }}</span>
{%- endif -%}
{% endmacro %}
-
+2 -12
templates/page.html
···
{%- if page.extra.thanks -%}
<hr />
<p>
-
<b>Special thanks</b>:
+
<b>Special thanks to</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 }}">{{ posts::thanks(who=person) }}</a>
-
{%- else -%}
-
<span class="p-name">{{ posts::thanks(who=person) }}</span>
-
{%- endif -%}
-
{%- else -%}
-
<span class="p-name">{{ person }}</span>
-
{%- endif -%}
-
</li>
+
<li class="h-card">{{ posts::thanks(who=person) }}</li>
{%- endfor -%}
</ul>
</p>
+4
templates/robots.txt
···
+
User-agent: *
+
Disallow: /cv/ /404/ /common-test-example/
+
Allow: /
+
Sitemap: {{ get_url(path="sitemap.xml") }}
+89 -48
templates/shortcodes/cv.md
···
## Personal information
Email:
-
<lukasz@niemier.pl>
+
<~@hauleth.dev>
Website:
<https://hauleth.dev>
···
## Experience
-
- Prograils - Junior Developer - 2013
+
- 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
-
- 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)
-
- Eiger - Senior Backend Engineer - 2022+
+
- Prograils - Junior Developer - 2013
### Organisations
···
* 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>
-
- StackOverflow: <https://stackoverflow.com/users/1017941/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
+
+ 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
+
+ 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
+
+ Add support for Elixir in diff
- Ecto:
-
+ Support aggregations over `*`
-
+ Better error on duplicated `schema` block
+
+ Support aggregations over `*`
+
+ Better error on duplicated `schema` block
- Elixir MongoDB driver
-
+ Support for transactions
+
+ 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:
···
### Languages and Frameworks
-
Expert:
-
- Elixir
+ Phoenix
+ Ecto
···
+ OpenTelemetry collaborator
+ EEF Member
+ OTP contributor
-
-
Advanced:
-
+
- Nix/NixOS
- Rust
-
- C
-
- SQL (pg/SQL)
+
- PostgreSQL
- 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
+
+
[`plug_caisson`]: https://github.com/supabase/plug_caisson
+3 -3
templates/shortcodes/readme.md
···
Observability WG</a></span> where we are trying to improve observability
features in Erlang ecosystem.
-
<!-- I am open for hire. If you want to inquiry me about my services then -->
-
<!-- contact me at <a class="u-email" -->
-
<!-- href="mailto:lukasz@niemier.pl">lukasz@niemier.pl</a>. -->
+
I am open for hire. If you want to inquiry me about my services then
+
contact me at <a class="u-email"
+
href="mailto:lukasz@niemier.pl">lukasz@niemier.pl</a>.
</div>
+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.