commit 23f714f570aebe075ff878d781a168b017de2892 Author: Lilac Date: Sat Jul 11 14:54:56 2026 -0400 initial commit diff --git a/assets/style.css b/assets/style.css new file mode 100644 index 0000000..08dbfec --- /dev/null +++ b/assets/style.css @@ -0,0 +1,348 @@ +:root { + --color-background: rgba( 18, 15, 18, 1); + --color-dark: rgba( 9, 7, 9, 1); + --color-text: rgba(255, 255, 255, 1); + --color-purple: rgba(171, 90, 237, 1); + --color-purple-translucent: rgba(171, 90, 237, 0.8); + --color-purple-transparent: rgba(171, 90, 237, 0.3); + --color-purple-hover: rgba(194, 135, 242, 1); + + --content-width: 800px; + --extra-width: 4em; +} + +@font-face { + font-family: "linja-pona-4"; + src: url('https://ajlee2006.github.io/linjaponasandbox/linja-pona-4.2.otf') format('opentype'); +} + +html { + font-family: 'Inter', sans-serif; + text-rendering: optimizeLegibility; + font-size: 1.125rem; + line-height: 1.25; + + color: var(--color-text); + background: var(--color-background) url('data:image/svg+xml;utf8,'); + background-position: 1.25em 0.5em; + margin: 0; + padding: 0; + min-height: 100vh; + overflow-x: hidden; + + & *::selection { + background-color: var(--color-purple-transparent); + } +} + +body { + background-color: var(--color-background); + border: 1px solid var(--color-purple-translucent); + border-radius: 8px; + margin: 1em auto; + padding: 0; + max-width: var(--content-width); + + & > * { + padding: 2em; + + & > :first-child { + margin-top: 0; + } + & > :last-child { + margin-bottom: 0; + } + } + & > * + * { + border-top: 1px solid var(--color-purple-translucent); + } +} + +/* Typography */ + +h1, h2, h3, h4, h5, h6 { + margin: 0 0 0.5em 0; + padding: 0; +} +h2 { font-size: 1.25rem; } +h3 { font-size: 1rem; } + +sup { + vertical-align: baseline; + position: relative; + top: -0.4em; + font-size: 0.75em; +} + +a { + color: var(--color-purple); + transition-property: color; + transition-duration: 0.5s; + text-underline-offset: 2.5px; + text-underline-thickness: 1px; + + &:hover, &:focus, &:active { + color: var(--color-purple-hover); + } +} + +span.t { + font-family: linja-pona-4; + white-space: nowrap; +} + +code { + font-family: "Monaspace Neon", ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace !important; + font-size: 0.9em; + font-size-adjust: from-font; + font-weight: 400; + line-height: 1; + + display: inline-block; + color: var(--color-text); + background: var(--color-dark); + border-radius: 4px; + padding: 0.05em 0.35em; +} + +pre { + background-color: transparent !important; + + & > code { + display: block; + overflow-x: scroll; + + background-color: transparent !important; + border: 1px solid; + border-color: var(--color-purple-translucent); + border-radius: 0; + padding: 1em 0.5em; + line-height: 1.4; + } +} + +ul { + position: relative; + list-style: none; + margin-left: 0; + padding-left: 1em; + + & li:before { + content: "–"; + position: absolute; + left: 0; + } +} + +dl { + display: grid; + grid-template-columns: 6em 1fr; + gap: 0.25em 2em; + padding: 0; + margin: 0; + + & > dd { + margin: 0; + } +} + +img, video { + max-width: 100%; + overflow-x: auto; + display: block; +} + +header { + display: flex; + flex-direction: row; + align-items: center; + + padding-top: 0.5em; + padding-bottom: 0.5em; + + & > div { + flex: 1; + + & > h1 { + font-size: 1.5rem; + margin-top: 1rem; + line-height: 1.5rem; + } + & > p:last-child { + margin-bottom: 1em; + } + } + + & > img { + border-radius: 8px; + aspect-ratio: 1/1; + height: 5em; + } +} + +nav { + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + + gap: 0.7ch; + padding-top: 1rem; + padding-bottom: 1rem; + + & a { + text-decoration: none; + } + + & > ul { + display: inline-flex; + flex-direction: row; + + font-size: smaller; + gap: 0.25em; + margin: 0; + padding: 0.25em; + + list-style: none; + + background-color: var(--color-dark); + border-radius: 2em; + + & > li { + margin: 0; + + &::before { + content: none; + } + + & > a { + display: inline-block; + padding: 0.5em 1em; + + border-radius: 1.25em; + text-decoration: none; + + color: var(--color-text); + + &.active { + background-color: var(--color-purple); + color: var(--color-dark); + } + } + } + } +} + +footer { + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + + padding-top: 1.5em; + padding-bottom: 1.5em; + + & * { + padding: 0; + margin: 0; + } +} + +@media (min-width: 881px) { + section { + & > div { + margin-left: calc(var(--extra-width) * -1); + width: calc(var(--content-width) + var(--extra-width)); + + position: relative; + z-index: 2; + + &::before { + content: ""; + position: absolute; + border-radius: 6px; + top: 0; + left: 0; + height: 100%; + width: 100%; + background-color: var(--color-background); + border: 1px solid var(--color-purple-translucent); + z-index: -1; + + clip-path: polygon( + -1px -1px, + calc(var(--extra-width) / 2) -1px, + calc(var(--extra-width) / 2) 3px, + calc(100% - 2px - var(--extra-width) / 2) 3px, + calc(100% - 2px - var(--extra-width) / 2) -1px, + calc(100% + 1px) -1px, + calc(100% + 1px) calc(100% + 1px), + calc(100% - 2px - var(--extra-width) / 2) calc(100% + 1px), + calc(100% - 2px - var(--extra-width) / 2) calc(100% - 3px), + calc(var(--extra-width) / 2) calc(100% - 3px), + calc(var(--extra-width) / 2) calc(100% + 1px), + -1px calc(100% + 1px) + ); + } + + & > pre > code, & > img, & > video { + padding: 1em 1.5em; + padding-bottom: calc(0.85em); + border: none; + } + + & > img, & > video { + width: calc(100% - 2.85em); + } + } + } +} + +@media (max-width: 880px) { + pre > code, div > img, div > video { + margin: 0 -2em; + padding: 1.5em 2em; + border-left: none; + border-right: none; + } +} +@media (max-width: 800px) { + body { + margin: 0; + border: none; + overflow-x: hidden; + + & > * { + padding: 1em; + } + } + + pre > code, div > img, div > video { + margin: 0 -1em; + padding: 1.5em 1em; + } +} + +hr { display: none; } + +ol { + list-style: none; + counter-reset: counter; + padding-left: 2em; + + & > li { + counter-increment: counter; + position: relative; + margin-top: 0.25em; + + &::before { + content: "[" counter(counter) "]"; + position: absolute; + left: -2em; + } + + & > p { + display: inline; + } + } +} diff --git a/content/_index.md b/content/_index.md new file mode 100644 index 0000000..36c324d --- /dev/null +++ b/content/_index.md @@ -0,0 +1,33 @@ +--- +title: "Lilac's Website" +description: "Computer Engineer & [Certified Punk Rocker](https://youtu.be/z9LiPuVRyU8&t=261)" +--- +# About Me + +Howdy! My name is Lilac, and this is my website. I am interested in low-level +computing, networking, and Linux. When not tinkering with 'puters, I birdwatch, +crochet, and garden. + +# Technology + +I write embedded firmware and POSIX/Linux applications using C and several +dialects of assembly. I do the server administration for all my own web services +using Linux and Nix. + +Here is some of the software I've written: + +[Tanager](https://code.grasswren.net/Lilac/Tanager) +: QMK keyboard firmware with flashy backlighting + +[grasswren.net](https://code.grasswren.net/Lilac/grasswren.net) +: Content and Hugo theme for this website + +# Music + +My favourite genres of music are grunge, punk, hyperpop, breakcore, and digital +hardcore. I really enjoy the music scene where I live; I think everybody should +get out there and interact with their local artists. + +I self-host my music library, so I don't have opportunities to discover new +music very often. If you are visiting this site for the first time, [shoot me an +email](mailto:lilac@grasswren.net) telling me what I should listen to next! diff --git a/content/birds/_index.md b/content/birds/_index.md new file mode 100644 index 0000000..11b32e8 --- /dev/null +++ b/content/birds/_index.md @@ -0,0 +1,4 @@ +--- +title: Lilac's Birds +description: They're cute little guys, aren't they? +--- diff --git a/content/blog/_index.md b/content/blog/_index.md new file mode 100644 index 0000000..52b1b9e --- /dev/null +++ b/content/blog/_index.md @@ -0,0 +1,4 @@ +--- +title: "Lilac's Blog" +description: "I love pretending I know what's going on!" +--- diff --git a/content/blog/qmk/index.md b/content/blog/qmk/index.md new file mode 100644 index 0000000..30035e5 --- /dev/null +++ b/content/blog/qmk/index.md @@ -0,0 +1,154 @@ +--- +title: "Building Custom Keyboard Backlighting" +description: "Upgrading my [Ergodox EZ](https://ergodox-ez.com/) using [QMK](https://github.com/qmk/qmk_firmware)" +date: 2026-07-09 +--- +# Intro + +I own an Ergodox EZ, a very comfortable, split, columnar keyboard. I bought it +because I gave myself carpal tunnel typing on my last keyboard, and I wanted to +see if I could type any faster on a split board.[^1] Ergodox keyboards run +open-source QMK firmware, meaning they are extensively configurable for a +programmer like me! I love my keyboard, and I would recommend a split keyboard +to anybody looking for something more comfortable than a standard keyboard. + +# Key Layout + +My key layout isn't the main focus of this post, but I wanted to highlight it +because it uses some cool features QMK offers. + +![Keyboard Layout](/qmk/layout.png) + +The first thing to notice is that I have plenty of thumb keys for common +keycodes. I never really liked that common keys like Enter, Backspace, and +Escape[^2] were off to the side, so I moved them to be more easily accessible. + +On the bottom left side, there is a key labelled Layer. QMK has a concept called +[layers](https://docs.qmk.fm/feature_layers), not unlike how a phone keyboard +works: tap or hold a layer key, and access different keys. I use this for my +function keys because I use them so infrequently, and I don't want them wasting +space normally. + +Finally, I have a key labelled Macro. I commonly type `->` when programming, so +I made this key type both characters in a single stroke. I also modified the +handling of the Backspace key so it deletes both characters if the previous +keycode was Macro. + +# Backlighting + +All the default backlighting options provided by QMK are too boring for me, so I +rolled my own. It works like this, with each keyboard half operating +independently: + +- Every keystroke changes the backlight colour +- When all keys are released, the backlight LEDs fade out nicely +- Backlight LEDs closer to the most recently pressed key are brighter than + those further from it + +Every time a keystroke is detected, the following function runs, tracking which +key was pressed and updating the colours: + +```c +void post_process_record_user(uint16_t keycode, keyrecord_t *record) +{ + if (!backlight || !record->event.pressed) { + return; + } + + if (record->event.key.row < 7) { + hue_left = rand(); + brightness_left = 255; + + if (record->event.key.col != 5) { + position_left = 29 - 2 * record->event.key.row; + } else { + position_left = 15; + } + + color(hue_left, brightness_left, position_left, 15); + } else { + hue_right = rand(); + brightness_right = 255; + + if (record->event.key.col != 5) { + position_right = 26 - 2 * record->event.key.row; + } else { + position_right = 14; + } + + color(hue_right, brightness_right, position_right, 0); + } +} +``` + +This function is rather long because I have to control the keyboard halves +independently, but the main idea is simple. Every keystroke: + +- Randomize the colour hue +- Maximize the brightness +- Record the column the key is in +- Turn on the LEDs using those values + +In another function, I keep track of how many keys are pressed at any given +time, so I can dim the LEDs once no more are pressed: + +```c +if (record->event.key.row < 7) { + count_left += record->event.pressed ? 1 : -1; +} else { + count_right += record->event.pressed ? 1 : -1; +} +``` + +## Brightness Calculation + +Now that I track these values, I need to use them to calculate the brightness of +each LED: + +```c +static void color(uint8_t hue, uint8_t brightness, uint8_t position, + uint8_t index) +{ + for (int i = index; i < (index + 15); ++i) { + const uint32_t cube = (uint32_t)brightness * brightness * brightness; + const uint8_t distance = position > i ? position - i : i - position; + sethsv(hue, 255, cube / 65025 * 5 / (5 + distance), i); + } + rgblight_set(); +} +``` + +This function takes in a hue, brightness, position, and starting index. There +are 30 LEDs on an Ergodox EZ, meaning that I can pass either `0` or `15` as the +starting index to have the function loop over the right or left LEDs +respectively. + +The `brightness` and `position` parameters contribute to the brightness of the +LED. First, the brightness is passed through a cubic ease-out function. This +produces a more pleasing dimming as opposed to linearly dimming the LED, as the +human eye senses brightness on a logarithmic scale. Second, the brightness is +scaled down by an amount proportional to its distance from the last key pressed. +The scaling function being used is `5 / (5 + n)` where `n` is the distance in +LED index from the brightest LED, which is not dimmed because its scaling +fraction evaluates to `1`. + +The `sethsv` function can then be passed a hue (given as a parameter and +unchanging as the LED is dimmed), a saturation (always the maximum), the value +(or brightness, as just calculated), and the index of the LED. Finally, I flush +my LED settings out to the LED peripheral with the `rgblight_set` function. + +# Result + +With a little logic, the backlights on each keyboard half act just as I wanted, +lighting up reactively and fading when no more keys are pressed. + +Take a look! + +![Typing on the Keyboard](/qmk/keyboard.mp4) + +# Footnotes + +[^1]: Quite a bit! + +[^2]: I use the [Helix](https://helix-editor.com/) editor, meaning I type + Escape almost as much as I type Space. diff --git a/content/blog/qmk/keyboard.mp4 b/content/blog/qmk/keyboard.mp4 new file mode 100644 index 0000000..a10fa49 Binary files /dev/null and b/content/blog/qmk/keyboard.mp4 differ diff --git a/content/blog/qmk/layout.png b/content/blog/qmk/layout.png new file mode 100644 index 0000000..060c648 Binary files /dev/null and b/content/blog/qmk/layout.png differ diff --git a/hugo.yaml b/hugo.yaml new file mode 100644 index 0000000..60897dc --- /dev/null +++ b/hugo.yaml @@ -0,0 +1,12 @@ +title: 'grasswren.net' +baseURL: 'https://grasswren.net' + +uglyURLs: true +disableKinds: + - RSS + - sitemap + - taxonomy + - term + +markup: { goldmark: { parser: { wrapStandAloneImageWithinParagraph: false } } } +minify: { minifyOutput: true } diff --git a/layouts/_default/_markup/render-heading.html b/layouts/_default/_markup/render-heading.html new file mode 100644 index 0000000..39bfeae --- /dev/null +++ b/layouts/_default/_markup/render-heading.html @@ -0,0 +1 @@ +{{ .Text | safeHTML }} diff --git a/layouts/_default/_markup/render-image.html b/layouts/_default/_markup/render-image.html new file mode 100644 index 0000000..a61755d --- /dev/null +++ b/layouts/_default/_markup/render-image.html @@ -0,0 +1,5 @@ +{{- if eq (path.Ext .Destination) ".mp4" -}} +
+{{- else -}} +
{{ .Text }}
+{{- end -}} diff --git a/layouts/_default/baseof.html b/layouts/_default/baseof.html new file mode 100644 index 0000000..4039e8b --- /dev/null +++ b/layouts/_default/baseof.html @@ -0,0 +1,42 @@ + + + +{{ .Title }} + + + + + + + + + + +{{ $css := resources.Get "style.css" | resources.Minify | resources.Fingerprint }} + + + + + +
+
+

{{ .Title }} {{ if eq .Page.BundleType "branch" }}[_lipu_pi_soweli_kili]{{ end }}

+

{{ .Description | markdownify }}

+
+Espurr +
+{{ block "main" . }} +{{ end }} + + + diff --git a/layouts/_default/index.html b/layouts/_default/index.html new file mode 100644 index 0000000..3d67e24 --- /dev/null +++ b/layouts/_default/index.html @@ -0,0 +1,8 @@ +{{ define "main" }} +{{ $sections := split .Content " +{{ print " +{{ end }} +{{ end }} diff --git a/layouts/_default/list.html b/layouts/_default/list.html new file mode 100644 index 0000000..f1e93a9 --- /dev/null +++ b/layouts/_default/list.html @@ -0,0 +1,10 @@ +{{ define "main" }} +
+
+{{ range .Pages }} + +{{ .Title }} +{{ end }} +
+
+{{ end }} diff --git a/layouts/_default/single.html b/layouts/_default/single.html new file mode 100644 index 0000000..909af3a --- /dev/null +++ b/layouts/_default/single.html @@ -0,0 +1,12 @@ +{{ define "main" }} +{{- $content := .Content -}} +{{- $content = replaceRE `
` "" $content -}} +{{- $content = replaceRE `
\s*$` "" $content -}} +{{- $tmp := $content | safeHTML -}} +{{ $sections := split $tmp " +{{ print " +{{ end }} +{{ end }} diff --git a/static/espurr.png b/static/espurr.png new file mode 100644 index 0000000..704aa86 Binary files /dev/null and b/static/espurr.png differ