Beautiful Terminal Output in Markdown
Have you ever wanted to show off your beautiful terminal output in a markdown file? Simply copying the text into a raw block will result in bland, colorless text. Including a screenshot on the other hand is bloated and hard to maintain, in case the content changes. I recently found a nice solution, so let me tell you about it!
Here's a sneak-peak of the end result:
.rw-r--r--@ 3 remo 28 Feb 00:29 .gitignore drwxr-xr-x@ - remo 8 Mar 22:51 repo drwxr-xr-x@ - remo 8 Mar 23:12 working_copy
I'll use the above output of eza for demonstration purposes as we progress. Below you can see the output of jj in my current repo.
@ kqlsx remo (no description set) ○ uxkrx remo improve recipe for new blog posts ○ qwmqx remo main* add color theme for aha-generated blocks │ • qwmqx hidden remo main@origin add color theme for aha-generated blocks ├─╯ • lumlx remo add just recipe for new blog posts │ ~
You'll notice that this is just HTML, you can even copy the text. The colors are easily stylable with CSS, which we'll get into later.
The heavy lifting will be done by aha (Ansi HTML Adapter). It reads from standard in and writes the matching HTML to standard out.
Not every markdown renderer supports inline HTML! If yours doesn't, this blog post is not for you... sorry! Also, for the pretty colors, a little bit of CSS is required. Inline styles get the job done, but ideally you can include a separate stylesheet. (GitHub no worky 😢)
You can scroll to the bottom for a TL;DR 🙂
Step by step
The best way to install aha depends on your environment.
For example, Fedora has it in the offical repo, so you can simply run sudo dnf install aha
.
Let's try it out by running eza -la .jj | aha
, its raw output is:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<!-- This file was created with the aha Ansi HTML Adapter. https://github.com/theZiz/aha -->
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="application/xml+xhtml; charset=UTF-8"/>
<title>stdin</title>
</head>
<body>
<pre>
.rw-r--r--@ 3 remo 28 Feb 00:29 .gitignore
drwxr-xr-x@ - remo 8 Mar 22:51 repo
drwxr-xr-x@ - remo 8 Mar 23:24 working_copy
</pre>
</body>
</html>
Okay, that's not very nice.
By default, aha produces a complete HTML document, including body, header and everything.
We don't want that, we just want to include inline HTML snippets in our markdown file.
This can be achieved with the --no-header
flag.
eza -la .jj | aha --no-header
produces:
.rw-r--r--@ 3 remo 28 Feb 00:29 .gitignore drwxr-xr-x@ - remo 8 Mar 22:51 repo drwxr-xr-x@ - remo 8 Mar 23:24 working_copy
Still not very nice.
We don't even have any colors!
Many programs try to detect if their standard out is connected to a terminal, in which case they will emit colorful output.
But if they're writing to a file or a pipe, they will emit plain text only.
Most programs accept a CLI flag or environment variable to override this.
For example, --color always
is very common.
Remember to check the man page of the program you're using if that doesn't work.
eza -la .jj --color always | aha --no-header
produces:
.rw-r--r--@ 3 remo 28 Feb 00:29 .gitignore drwxr-xr-x@ - remo 8 Mar 22:51 repo drwxr-xr-x@ - remo 8 Mar 23:24 working_copy
Okay, this is slightly better. We have colors, but they're ugly. Also, line breaks are somehow broken. Let's tackle that next.
With the --no-header
flag, aha just emits HTML <span>
s with single line breaks.
But markdown ignores single line breaks!
We can solve this by wrapping the entire thing in a <pre>...</pre>
tag.
echo "<pre>" ; eza -la .jj --color always | aha --no-header ; echo "</pre>"
produces:
.rw-r--r--@ 3 remo 28 Feb 00:29 .gitignore drwxr-xr-x@ - remo 8 Mar 22:51 repo drwxr-xr-x@ - remo 8 Mar 23:24 working_copy
Great!
Line breaks are fixed.
Note that the background color changed, because I have pre { background-color: #151515; }
in my stylesheet.
The command is getting more tedious to write, but don't worry, we'll improve that later.
Next, let's spruce up those colors.
In order to color its output, aha adds inline styles to <span>
s, for example:
<span style="color:blue;"> 8 Mar 23:24</span>
That makes it really hard to customize the color.
Fortunately, aha accepts a flag --stylesheet
, which adds classes to the spans instead of inline styles.
The stylesheet is then included in the header.
Remember that we previously removed the header with the --no-header
flag, so let's omit that for a moment.
The raw output of eza -la .jj --color always | aha --stylesheet
is:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<!-- This file was created with the aha Ansi HTML Adapter. https://github.com/theZiz/aha -->
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="application/xml+xhtml; charset=UTF-8"/>
<title>stdin</title>
<style type="text/css">
.reset {color: black;}
.bg-reset {background-color: white;}
.inverted {color: white;}
.bg-inverted {background-color: black;}
.dimgray {color: dimgray;}
.red {color: red;}
.green {color: green;}
.yellow {color: olive;}
.blue {color: blue;}
.purple {color: purple;}
.cyan {color: teal;}
.white {color: gray;}
.bg-black {background-color: black;}
.bg-red {background-color: red;}
.bg-green {background-color: green;}
.bg-yellow {background-color: olive;}
.bg-blue {background-color: blue;}
.bg-purple {background-color: purple;}
.bg-cyan {background-color: teal;}
.bg-white {background-color: gray;}
.underline {text-decoration: underline;}
.bold {font-weight: bold;}
.italic {font-style: italic;}
.blink {text-decoration: blink;}
.crossed-out {text-decoration: line-through;}
.highlighted {filter: contrast(70%) brightness(190%);}
</style>
</head>
<body>
<pre>
.<span class="bold yellow ">r</span><span class="bold red ">w</span><span class="bold highlighted dimgray ">-</span><span class="yellow ">r</span><span class="bold highlighted dimgray ">--</span><span class="yellow ">r</span><span class="bold highlighted dimgray ">--</span>@ <span class="green ">3</span> <span class="bold yellow ">remo</span> <span class="blue ">28 Feb 00:29</span> .gitignore
<span class="bold blue ">d</span><span class="bold yellow ">r</span><span class="bold red ">w</span><span class="bold green ">x</span><span class="yellow ">r</span><span class="bold highlighted dimgray ">-</span><span class="green ">x</span><span class="yellow ">r</span><span class="bold highlighted dimgray ">-</span><span class="green ">x</span>@ <span class="bold highlighted dimgray ">-</span> <span class="bold yellow ">remo</span> <span class="blue "> 8 Mar 22:51</span> <span class="bold blue ">repo</span>
<span class="bold blue ">d</span><span class="bold yellow ">r</span><span class="bold red ">w</span><span class="bold green ">x</span><span class="yellow ">r</span><span class="bold highlighted dimgray ">-</span><span class="green ">x</span><span class="yellow ">r</span><span class="bold highlighted dimgray ">-</span><span class="green ">x</span>@ <span class="bold highlighted dimgray ">-</span> <span class="bold yellow ">remo</span> <span class="blue "> 8 Mar 23:24</span> <span class="bold blue ">working_copy</span>
</pre>
</body>
</html>
Great!
What I recomment is you copy these styles to a separate CSS file.
You can customize it to your heart's content, and with the --no-header
flag, aha won't overwrite them.
Here is my current stylesheet. I took the colors from tailwind's color palette, which I really like. I haven't customized everything yet, I will probably tweak this when I use aha with the output of other terminal programs.
.reset { color: black; }
.bg-reset { background-color: white; }
.inverted { color: white; }
.bg-inverted { background-color: black; }
.dimgray { color: dimgray; }
.red { color: oklch(0.645 0.246 16.439); }
.green { color: oklch(0.723 0.219 149.579); }
.yellow { color: oklch(0.795 0.184 86.047); }
.blue { color: oklch(0.673 0.182 276.935); }
.purple { color: oklch(0.714 0.203 305.504); }
.cyan { color: oklch(0.789 0.154 211.53); }
.white { color: gray; }
.bg-black { background-color: black; }
.bg-red { background-color: oklch(0.645 0.246 16.439); }
.bg-green { background-color: oklch(0.627 0.194 149.214); }
.bg-yellow { background-color: oklch(0.852 0.199 91.936); }
.bg-blue { background-color: oklch(0.707 0.165 254.624); }
.bg-purple { background-color: oklch(0.714 0.203 305.504); }
.bg-cyan { background-color: oklch(0.789 0.154 211.53); }
.bg-white { background-color: gray; }
.underline { text-decoration: underline; }
.bold { font-weight: bold; }
.italic { font-style: italic; }
.blink { text-decoration: blink; }
.crossed-out { text-decoration: line-through; }
.highlighted { filter: contrast(70%) brightness(190%); }
We're almost done, we just need to make this more usable.
The command has gotten pretty long.
I created a script at ~/.local/bin/aha
with the following content:
#!/usr/bin/env bash
set -euo pipefail
# wrapper around `aha`
# --no-header for embedding as snippets in websites, e.g. blog.buenzli.dev
# --stylesheet to make the colors overridable by an external stylesheet
echo "<!-- generated by aha script -->"
echo "<pre>"
/usr/bin/aha --no-header --stylesheet
echo "</pre>"
You can also name the script something different to make sure there is no name conflict with the actual program called aha.
In my case, it works because ~/.local/bin
appears before /usr/bin
in my $PATH
.
Now you can simply run eza -la .jj --color always | aha
straight into your markdown file and everything should be pretty!
You may have an aversion to adding little scripts like this to your setup. I certainly did in the past, because I worried about forgetting what's where and then having my setup break when I work on another machine. If that's you, I recommend keeping your configuration files (aka dotfiles) in version control. There are many tools out there to help with that, make sure to shop around. I am currently happy with chezmoi.
TL;DR
-
Include the following stylesheet in your website, tweak the colors according to your preference:
.reset { color: black; } .bg-reset { background-color: white; } .inverted { color: white; } .bg-inverted { background-color: black; } .dimgray { color: dimgray; } .red { color: oklch(0.645 0.246 16.439); } .green { color: oklch(0.723 0.219 149.579); } .yellow { color: oklch(0.795 0.184 86.047); } .blue { color: oklch(0.673 0.182 276.935); } .purple { color: oklch(0.714 0.203 305.504); } .cyan { color: oklch(0.789 0.154 211.53); } .white { color: gray; } .bg-black { background-color: black; } .bg-red { background-color: oklch(0.645 0.246 16.439); } .bg-green { background-color: oklch(0.627 0.194 149.214); } .bg-yellow { background-color: oklch(0.852 0.199 91.936); } .bg-blue { background-color: oklch(0.707 0.165 254.624); } .bg-purple { background-color: oklch(0.714 0.203 305.504); } .bg-cyan { background-color: oklch(0.789 0.154 211.53); } .bg-white { background-color: gray; } .underline { text-decoration: underline; } .bold { font-weight: bold; } .italic { font-style: italic; } .blink { text-decoration: blink; } .crossed-out { text-decoration: line-through; } .highlighted { filter: contrast(70%) brightness(190%); }
-
Add the follwing script to your
$PATH
:#!/usr/bin/env bash set -euo pipefail # wrapper around `aha` # --no-header for embedding as snippets in websites, e.g. blog.buenzli.dev # --stylesheet to make the colors overridable by an external stylesheet echo "<!-- generated by aha script -->" echo "<pre>" /usr/bin/aha --no-header --stylesheet echo "</pre>"
-
Run
any-cli-program --color always | aha >> some-file.md
Conclusion
Now we're ready to flex on our fellow nerds with fancy colorful letters! 🤩 I'll make the first move, here's me working on the Linux kernel with jj:
@ qkkyy remo (empty) (no description set) ○ qzsuz remo devel use PropertyGuard ○ ukrqy remo add justfile ○ yooow remo add devicetree overlay for raspberry pi setup ○ nwytk remo squash-merge rpi-6.14.y ○ xvnmz remo ds90ub954 add DS90UB954 FPD-link driver ○ monkp remo abstractions Merge abstractions ├─┬─┬─┬─╮ │ │ │ │ ○ smqrp remo i2c-new-client WIP: rust: i2c: add new_client_device │ │ │ ○ │ nvsyp remo property-guard rust: fwnode: add PropertyGuard │ │ │ ○ │ wtuzo remo property-reference-args rust: fwnode: add property_get_reference_args │ │ │ ○ │ xnquk remo Merge property-child-iterator and arrayvec │ │ │ ├───╮ │ │ │ │ │ ○ utqyp remo arrayvec rust: add ArrayVec │ │ │ ○ │ │ qwwlo remo property-child-iterator rust: fwnode: add child accessor and iterator │ │ │ ○ │ │ ypnnt remo dev-as-fwnode rust: device: read properties via fwnode_handle │ │ │ ○ │ │ kmnkw remo const-i8-to-char-ptr fix usage of *const i8 │ │ │ ○ │ │ pqwvp robh rebase/robh/rust/device-properties samples: rust: platform: Add property read examples │ │ │ : │ │ (elided revisions) │ │ │ ├─╯ │ │ │ ○ │ │ ssnws remo regmap-read-write rust: regmap: add read & write methods │ │ ○ │ │ xkrlp remo regmap-without-fields rust: regmap: remove requirement of using fields │ │ ├─╯ │ │ │ ○ │ vtunu fabi rebase/fabo/b4/ncv6336 fixup! rust: i2c: add basic I2C client abstraction │ │ : │ (elided revisions) │ │ ├─────╯ │ ○ │ nnrto remo delay-msleep rust: delay: add msleep function │ ├─╯ ○ │ lwzop remo gpio-cansleep rust: gpio: add set_value_cansleep method ○ │ nouww remo gpio-consumer (empty) Cherry-pick rust gpio consumer ○ │ wmsnl fabi rust: gpio: add GPIOD consumer abstractions ├─╯ • ovoup ojed rust-next MAINTAINERS: add Danilo Krummrich as Rust reviewer │ ~
Now it's your turn! 😃