Compare commits

37 Commits

Author SHA1 Message Date
0695ad26bd migrate to docker compose
Some checks failed
/ Deploy website to server (push) Failing after 1m37s
2025-05-27 15:48:18 +01:00
a843e7b902 new poast and new font
Some checks failed
/ Deploy website to server (push) Has been cancelled
2025-05-14 00:05:11 +01:00
76fbc4f439 remove title from post markdown
Some checks failed
/ Deploy website to server (push) Has been cancelled
2025-05-05 20:52:50 +01:00
f190d3fb33 new poast
Some checks failed
/ Deploy website to server (push) Has been cancelled
2025-05-05 20:51:17 +01:00
2d04a16f10 god im so stupid
Some checks failed
/ Deploy website to server (push) Failing after 14s
2025-05-02 00:51:32 +01:00
dff5a292d1 idk
Some checks failed
/ Deploy website to server (push) Failing after 14s
2025-05-02 00:49:35 +01:00
a68328527e need to add newline to end of private key file
Some checks failed
/ Deploy website to server (push) Failing after 14s
2025-05-02 00:42:50 +01:00
c3d884ac76 try ipv4 address
Some checks failed
/ Deploy website to server (push) Failing after 14s
2025-05-02 00:35:58 +01:00
b56b57e85e whoops
Some checks failed
/ Deploy website to server (push) Failing after 14s
2025-05-02 00:26:02 +01:00
a32deff9c8 use oauth to auth with tailscale
Some checks failed
/ Deploy website to server (push) Failing after 14s
2025-05-02 00:18:16 +01:00
88af49844e fix ci action
Some checks failed
/ Deploy website to server (push) Failing after 14s
2025-05-01 23:56:13 +01:00
faa4f4c677 test ci action
Some checks failed
/ Deploy website to server (push) Failing after 2m41s
2025-05-01 23:44:30 +01:00
d3cddbea97 new poasts 2025-04-10 15:23:38 +01:00
8ea52962bf new poast: selection detection 2025-01-05 00:41:05 +00:00
8850366898 fix typo and rename file 2024-12-05 00:03:21 +00:00
94dcdbc5d9 slight grammar change 2024-12-04 23:39:34 +00:00
0b799321e9 new poast: importance of environment 2024-12-04 23:38:18 +00:00
8229c47104 new poast: progressive blur in css 2024-10-26 15:53:34 +01:00
920e1119b3 update email 2024-10-16 00:50:22 +01:00
b03053e795 fix year 2024-10-09 18:58:21 +01:00
97b7dc06e9 chore: add screenshot to README 2024-09-28 20:19:00 +01:00
243cf9608a fix js examples 2024-09-07 17:56:27 +01:00
edea36bb98 footer should be monospace as well 2024-09-07 17:42:59 +01:00
44b52d8751 add tweet reference 2024-09-07 17:39:15 +01:00
ddbdff8c26 rollback pnpm lock file version 2024-09-07 17:39:04 +01:00
5b201bf797 use georgia font for blog poasts 2024-09-07 17:26:51 +01:00
ec96770e11 new poast: packaging go project as nix flake 2024-09-07 17:10:47 +01:00
60fb7b50d1 add flake.nix 2024-09-07 17:10:33 +01:00
da07a8ba8f add theprimeagen twt 2024-08-25 22:06:22 +01:00
51b06bd1f3 fix grammar 2024-08-25 02:24:00 +01:00
8205db685f fix slight typo 2024-08-25 02:23:35 +01:00
d7225bb1b1 new poast 2024-08-25 02:23:03 +01:00
3fa75df0d8 update project lists 2024-07-22 01:01:00 +01:00
c45ee5c2ea fix version calculation 2024-07-15 01:14:10 +01:00
d8b5519609 hack to fix type error 2024-07-15 01:05:56 +01:00
5238e13983 add :w command 2024-07-15 01:03:20 +01:00
9ca9a5c9e7 Merge pull request #1 from kennethnym/nvim-redesign
Nvim redesign
2024-07-15 00:51:52 +01:00
52 changed files with 5195 additions and 3061 deletions

41
.github/workflows/deploy.yml vendored Normal file
View File

@@ -0,0 +1,41 @@
on:
push:
branches:
- main
workflow_dispatch:
jobs:
deploy:
runs-on: ubuntu-latest
name: Deploy website to server
env:
MACHINE_USER_NAME: kenneth
MACHINE_NAME: helian
steps:
- name: Setup Tailscale
uses: tailscale/github-action@v3
with:
oauth-client-id: ${{ secrets.TS_OAUTH_CLIENT_ID }}
oauth-secret: ${{ secrets.TS_OAUTH_CLIENT_SECRET }}
tags: tag:ci
- name: Add SSH key
env:
SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
run: |
mkdir -p ~/.ssh
MACHINE_IP="$(tailscale ip -4 $MACHINE_NAME)"
ssh-keyscan $MACHINE_IP >> ~/.ssh/known_hosts
printf "%s" "$SSH_PRIVATE_KEY" > ~/.ssh/key
# add a new line to the end of the private key file
# otherwise it won't be loaded properly
echo >> ~/.ssh/key
chmod 600 ~/.ssh/key
- name: Deploy website
run: |
MACHINE_IP="$(tailscale ip -4 $MACHINE_NAME)"
ssh -i ~/.ssh/key "$MACHINE_USER_NAME:@$MACHINE_IP" /bin/bash << EOF
cd /opt/website
git pull
cd ../
docker compose up --build --detach website
EOF

View File

@@ -1,7 +1,7 @@
# syntax = docker/dockerfile:1
# Adjust NODE_VERSION as desired
ARG NODE_VERSION=18.17.0
ARG NODE_VERSION=20.11.1
FROM node:${NODE_VERSION}-slim as base
LABEL fly_launch_runtime="Astro"
@@ -13,7 +13,7 @@ WORKDIR /app
ENV NODE_ENV="production"
# Install pnpm
ARG PNPM_VERSION=8.9.2
ARG PNPM_VERSION=9.15.2
RUN npm install -g pnpm@$PNPM_VERSION

View File

@@ -1 +1,3 @@
my website built with [astro](https://astro.build/). do not copy or re-distribute without my prior permission. all rights reserved.
![homepage of my website](./assets/screenshot.png)

BIN
assets/screenshot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 344 KiB

View File

@@ -1,6 +1,8 @@
import { defineConfig } from "astro/config";
import mdx from "@astrojs/mdx";
import sitemap from "@astrojs/sitemap";
import remarkMath from "remark-math";
import rehypeKatex from "rehype-katex";
import tailwind from "@astrojs/tailwind";
@@ -14,5 +16,7 @@ export default defineConfig({
// https://shiki.style/themes
theme: "catppuccin-mocha",
},
remarkPlugins: [remarkMath],
rehypePlugins: [rehypeKatex],
},
});

26
flake.lock generated Normal file
View File

@@ -0,0 +1,26 @@
{
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1725704419,
"narHash": "sha256-35DCc49kiHAmHjAHrx/Bt//+I76aYe9HVNh7pxqjvko=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "6ecb754c326510106873747aa6233d90c463e966",
"type": "github"
},
"original": {
"owner": "NixOS",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"nixpkgs": "nixpkgs"
}
}
},
"root": "root",
"version": 7
}

32
flake.nix Normal file
View File

@@ -0,0 +1,32 @@
{
description = "my website";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs?tag=24.05";
};
outputs = { nixpkgs, ... }:
let
supportedSystems = [ "x86_64-linux" "x86_64-darwin" "aarch64-linux" "aarch64-darwin" ];
forAllSystems = nixpkgs.lib.genAttrs supportedSystems;
nixpkgsFor = forAllSystems (system: import nixpkgs { inherit system; });
in
{
devShells = forAllSystems (system:
let
pkgs = nixpkgsFor.${system};
in
{
default = pkgs.mkShell {
packages = [
pkgs.pnpm_8
pkgs.flyctl
pkgs.nodejs_20
];
};
}
);
};
}

View File

@@ -24,6 +24,8 @@
"@flydotio/dockerfile": "latest",
"@tailwindcss/typography": "^0.5.10",
"prettier": "^3.2.5",
"prettier-plugin-astro": "^0.13.0"
"prettier-plugin-astro": "^0.13.0",
"rehype-katex": "^7.0.1",
"remark-math": "^6.0.0"
}
}

6924
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 163 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 748 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 799 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 180 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 293 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 301 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 168 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

View File

@@ -25,6 +25,10 @@ const { title, description, image = '/blog-placeholder-1.jpg' } = Astro.props;
<!-- RSS autodiscovery -->
<link rel="alternate" type="application/rss+xml" title={title} href={`${Astro.site}rss.xml`} />
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Source+Serif+4:ital,opsz,wght@0,8..60,200..900;1,8..60,200..900&display=swap" rel="stylesheet">
<!-- Primary Meta Tags -->
<title>{title}</title>
<meta name="title" content={title} />

View File

@@ -20,6 +20,10 @@
const cmdEvent = new CustomEvent("closebuffer");
window.dispatchEvent(cmdEvent);
},
w: () => {
const cmdEvent = new CustomEvent("savebuffer");
window.dispatchEvent(cmdEvent);
},
help: () => {
const cmdEvent = new CustomEvent("openhelp");
window.dispatchEvent(cmdEvent);

View File

@@ -14,7 +14,7 @@ import CommandLine from "./CommandLine.astro";
<span>&nbsp;</span>
<Link href="https://polygui.org">poly gui</Link>
<span>&nbsp;</span>
<Link href="https://polygui.org/nanopack/introduction/">nanopack</Link>
<Link href="https://github.com/kennethnym/infinifi">infinifi</Link>
<span>&nbsp;</span>
<Link href="https://github.com/kennethnym/mai">mai</Link>
<span>&nbsp;</span>
@@ -25,7 +25,7 @@ import CommandLine from "./CommandLine.astro";
<span>&nbsp;</span>
<Link href="https://x.com/kennethnym">x.com</Link>
<span>&nbsp;</span>
<Link href="mailto:kennethnym@outlook.com">email</Link>
<Link href="mailto:kennethnym@hey.com">email</Link>
<span>&nbsp;</span>
</div>
</footer>
@@ -57,4 +57,10 @@ import CommandLine from "./CommandLine.astro";
window.addEventListener("openiccf", () => {
window.location.href = "https://vimhelp.org/uganda.txt.html#iccf";
});
window.addEventListener("savebuffer", () => {
if ((window as any).showSaveFilePicker) {
(window as any).showSaveFilePicker();
}
});
</script>

View File

@@ -0,0 +1,23 @@
---
title: autocomplete considered harmful
description: my experience of no autocompletion in editors
pubDate: May 5 2025
---
it is taken for granted that an editor provides autocompletion, even at a basic level. with a few exceptions, modern editors have autocompletion configured and enabled out of the box. because it is a default experience for editors, few has ever questioned whether it is an ideal coding environment for a developer. i used to take autocompletion for granted, and never even considered the possibility of turning it off. that changed 2 weeks ago, and having written code without it for an extended period, i think it provides a more pleasant coding experience, and i think more developers should give it a try.
## micro-stutters
autocompletion popup causes micro-stutters. when you try to complete on every keystroke, you have to stop and check that the entry selected is what you want, and then either select it, or find the entry that you want. when it is turned off, code can flow naturally from my mind to the computer without interruption. that "stop-and-check" may be minor, but it compounds when it occurs on every keystroke. removing that mental barrier has turned out to be crucial in helping me maintain my flow, making my coding experience far more enjoyable as a result.
## better code consolidation
having to type out all your code manually also helped me dramatically in consolidating the codebase in my head. instead of relying on autocompletion to index my codebase, i have to do that myself, especially for things that i access regularly so that i can type their names out correctly. this index has proven to provide a more solid mental model of my codebase, thus improving my understanding of it. this index also makes navigating around the codebase a lot more fluid, which, again, helps maintain my flow.
## a good middle ground
instead of turning autocompletion in my editors altogether, i chose to have the popup visible on demand (ctrl + space in my case.) this way, if there is a "cache miss" when accessing my mental index of the codebase, i can still lookup what is available, and update my index accordingly.
## let me know your experience!
i would love to hear from my fellow software developers about their experience of turning off autocompletion in their code editor of choice. feel free to dm me on [x dot com](https://x.com/kennethnym) or [email me](mailto:kennethnym@hey.com) about your thoughts on this!

View File

@@ -0,0 +1,184 @@
---
title: continuous deployment with SSH and tailscale
description: A guide in implementing continuous deployment with SSH and Tailscale
pubDate: May 13 2025
---
*Skip to [high level overview](#high-level-overview) if you are not interested in the back story.*
I recently purchased a cheap Dell OptiPlex to act as my own server. I have since successfully moved all my services to the machine, including my [git server](https://code.nym.sh) and my website which was previously hosted on [Fly](https://fly.io). Without the awesome `fly deploy` command to deploy my website, I had to find another way to automate the deployment of my website.
Here comes the problem: my website is exposed to the public via [CloudFlare Tunnel](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/), but the host running my website isn't publicly accessible. This means that, for instance, I cannot SSH into the host in a CI/CD environment, such as GitHub Actions.
Thankfully, using [Tailscale](https://tailscale.com/), I can securely access any of my devices, even outside my home network.
## Why Tailscale?
Based on [WireGuard](https://www.wireguard.com/), Tailscale lets you set up your own VPN with ease for your own devices, which forms a "tailnet". Any device connected to the tailnet can securely access other devices within it using the corresponding assigned address.
Therefore, if I can manage to connect a machine to my tailnet, I will be able to access my host anywhere, in any environment, including CI/CD pipelines. Fortunately, Tailscale maintains a [GitHub Action](https://tailscale.com/kb/1276/tailscale-github-action) that lets you connect to your tailnet in a GitHub Action runner.
## High Level Overview
1. Trigger continuous deployment when commits are made to the main branch
2. Connect the runner of the continuous deployment pipeline to the tailnet to which your host is connected
3. SSH into the host using the assigned IP
4. Pull the new commits on the host, and re-deploy the website
I will be using GitHub Action syntax for the CI/CD config file, but the overarching concept applies to any CI/CD environment.
## The Basics
Start with the following code:
```yml
on:
push:
branches:
- main
workflow_dispatch:
jobs:
deploy:
runs-on: ubuntu-latest
name: Deploy website to server
env:
MACHINE_USER_NAME: user
MACHINE_NAME: my-host
```
This allows the pipeline to be triggered when commits are made to the `main` branch, and manually in the GitHub UI.
Two environment variables are also defined here:
- `MACHINE_USER_NAME`: the name of the user used for SSH-ing into the machine; and
- `MACHINE_NAME`: the name of the machine assigned by Tailscale. You can find this out in the admin console of Tailscale.
## Setting Up Tailscale GitHub Action
### Defining a Tag
In Tailscale, you can assign and group machines with *tags*. One of their primary purposes is to allow you to apply access control to machines based on tags.
In this case, it is useful to assign the runner running the CI/CD pipeline a tag. Then, using Tailscale's [Access Control Lists (ACLs)](https://tailscale.com/kb/1018/acls), we can limit access the tag has to the host to which we wish to deploy. We will name it `tag:ci`.
To define a tag, navigate to the "Access Controls" page in the admin console. You will then be presented with an editor for the ACL file. Within the `"tagOwners"` key, add the following:
```json
{
"tagOwners": {
"tag:ci": ["autogroup:owner"]
}
}
```
`"autogroup:owner"` specifies that the owner of the tailnet can apply this tag. Before we move on, make a note of the IP address of the machine that will be hosting the service.
Now, let's define an ACL for the tag. We want machines that are tagged with `tag:ci` to be able to SSH into the host machine and nothing else. As an example, we will use `1.2.3.4` as the IP address of the machine.
```json
{
"acls": [
// other lists
{"action": "accept", "src": ["tag:ci"], "dst": ["1.2.3.4:22"]}
]
}
```
This specifies that machines tagged with `tag:ci` can only access machine with IP `1.2.3.4` on port `22`, which is the default SSH port. If your machine has a non-standard SSH port, change `22` to the correct port.
### Creating an OAuth Client
Tailscale's GitHub Action relies on OAuth2 for authentication. To get started, go to the admin console, then navigate to the "OAuth Clients" section in settings. Give the client a descriptive name, and make sure the [`auth_keys` scope](https://tailscale.com/kb/1215/oauth-clients#scopes) is enabled. Take a note of the client ID and client secret, and store them as GitHub Action secrets.
Now, add a step in the workflow file to set up Tailscale:
```yml
on:
push:
branches:
- main
workflow_dispatch:
jobs:
deploy:
runs-on: ubuntu-latest
name: Deploy website to server
env:
MACHINE_USER_NAME: user
MACHINE_NAME: my-host
steps:
- name: Setup Tailscale
uses: tailscale/github-action@v3
with:
oauth-client-id: ${{ secrets.TS_OAUTH_CLIENT_ID }}
oauth-secret: ${{ secrets.TS_OAUTH_CLIENT_SECRET }}
tags: 'tag:ci'
```
This step installs Tailscale on the runner machine, and authenticates it into our tailnet using the OAuth credentials we created. The `tag:ci` is also applied to the runner, which means it can access the machine at `1.2.3.4` on port 22 and nothing else.
## Setting Up SSH Access
To let the CI/CD runner authenticate over SSH, we will need to:
1. Generate an SSH key pair
2. Install the public key to the host machine, and
3. Save the private key as a GitHub secret to be used in the workflow.
Once you have completed the steps above, add the following workflow step after the Tailscale setup step:
```yml
- name: Add SSH key
env:
SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
run: |
mkdir -p ~/.ssh
MACHINE_IP="$(tailscale ip -4 $MACHINE_NAME)"
ssh-keyscan $MACHINE_IP >> ~/.ssh/known_hosts
printf "%s" "$SSH_PRIVATE_KEY" > ~/.ssh/key
# add a new line to the end of the private key file
# otherwise it won't be loaded properly
echo >> ~/.ssh/key
chmod 600 ~/.ssh/key
```
Let's break down the script:
1. Create the default SSH directory if it does not exist
2. Using Tailscale's CLI to find the IPv4 of the host machine by its assigned name, and store it in `MACHINE_IP` variable
3. Perform a key scan on the machine, then add it to the list of known hosts
4. Save the private key in GitHub secret (made available as an environment variable via the `env` block) to `~/.ssh/key` file
5. GitHub trims any trailing newline character in a secret value. Because SSH expects a trailing newline character at the end of a key file, a new line needs to be appended to the end of the key file created in the previous step
6. Correct the permission of the key file
## Deploying the Website
With everything in place, the runner is now able to SSH into the host machine via Tailscale! How you deploy your services may vary, so I will use [my website](https://github.com/kennethnym/website/blob/main/.github/workflows/deploy.yml) as an example, which is stored as a git repo on the host machine and is run as a Docker container.
The important part here is obtaining the machine IP using its assigned name, which we did in the previous step; passing the correct key file to SSH; and finally providing the correct username and IP.
```yml
- name: Deploy website
env:
SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
run: |
MACHINE_IP="$(tailscale ip -4 $MACHINE_NAME)"
ssh -i ~/.ssh/key "$MACHINE_USER_NAME@$MACHINE_IP" /bin/bash << EOF
cd /opt/website
git pull
docker build -t website .
docker stop website-container
docker rm website-container
docker run --name website-container --restart=always --publish 5432:80 --detach website
EOF
```
## References
- [Using GitHub Actions and Tailscale to build and deploy applications securely](https://tailscale.com/blog/2021-05-github-actions-and-tailscale)
## Final Notes
We have successfully created a GitHub Action workflow that automatically deploys a service over SSH using Tailscale, without public access to our host machine. I hope you find this guide helpful, and thank you for reading.
Please don't hesitate to reach out should you spot any mistake.

View File

@@ -0,0 +1,45 @@
---
title: emotional intelligence and connections
description: on the importance of emotional intelligence
pubDate: Apr 10 2025
---
Over the course of the past month, I have come to the realization that human connection is the single most important thing one should spend the effort to foster. I will go as far as to say that it is far more difficult to develop **deep emotional connections** with people than honing technical excellence. In my opinion, the determining factor of being able to develop emotional connections with people is one's emotional intelligence.
## being technical is easy; the ability to feel is not
Being technically excellent is easy. If you have the ability to read this article, you have the ability to become really good at something. with the internet and an llm, you have easy access to all of human knowledge at your fingertip. All you need to do is to digest them and apply them. It takes time and effort, but the path to technical excellence is predictable and linear.
The ability to *feel* is the ability to understand and feel your emotions deeply. Try to answer the following questions:
- How are you feeling at this very moment?
- Why are you feeling the way you are?
- What is the "stacktrace" of your emotion? (what effects your emotion? what effects that? so on and so forth)
As an additional challenge, try to verbalize your answer rather than merely answering in your head. You will realize it is a lot harder than it seems!
With the ability to understand your internal emotions comes sympathy and ultimately empathy, which is the key to foster emotional connection with someone. You will be able to put yourself in their shoes and truly understand and, most importantly, **feel** what they are feeling.
## the cultural bias towards outward intelligence
I coin the term "outward intelligence", which is what people think of when they think of "intelligence": outward behaviors of a person that is perceived as "being intelligent", for instace, being good at problem solving or being well spoken. Inversely, I define "inward intelligence" as the ability to understand one's inner self.
This distinction is crucial, for a person cannot be "intelligent" or "smart" if they lack either. Unfortunately, the current culture seems to be significantly biased towards outward intelligence, when in reality emotional intelligence is **equally important**. This is easily verified by noticing that, for instance, that the adjectives "intelligent" or "smart" is dominantly defined by a person's outward behavior (hence "outward intelligence") and that IQ always dominates online discourse, at least on twitter.
## developing emotional intelligence
This naturally leads to the question - how do i develop emotional intelligence? While i am not able to offer professional advice, I can offer some pointers, drawn from anecdotal experiences:
### feel your emotions
The worst thing to do to yourself is to surpress and ignore your emotions. "It is fine" or "I don't care anyways" are common phrases when your brain is trying to surpress your emotions. Instead, let your emotions out - feel it, understand it, trace it. Emotions are like problems - they don't just go away if you ignore them; instead they pile up until they become too much to deal with.
### find your connections
Emotional intelligence and human connections has a symbiotic relationship. To develop emotional intelligence, you need to develop connections with other people. Try to move your conversations beyond the surface level. Then, you will have the opportunity to verbalize your emotions. You *will* be bad at it at first, but this forces you to understand your emotions better, which makes you better at verbalizing them. As your understanding of your own emotions grow, your empathy also develops, which strengthens your connections with others.
## final thank you note
If you are reading this and we have met before - from the bottom of my heart, I will forever be grateful to you for the opportunity to have been able to meet and connect, and I hope we will have the chance to meet again soon.
If you are reading this and we spend time together on a regular basis - know that mere words can't begin to express my unwavering gratitude towards you, that I will forever be in debt to you, and that I wish for us to be a never ending chapter of our lives.

View File

@@ -0,0 +1,373 @@
---
title: packaging a go cli as a nix flake
description: a guide in how to package a go cli as a nix flake
pubDate: '7 Sept 2024'
---
after being psyop-ed into using neovim, i have now also been manipulated into using [nix](https://nixos.org/), an awesome package manager that deserves more attention.
<blockquote class="twitter-tweet" data-theme="dark"><p lang="en" dir="ltr">now i just need u to fall for nix thirst traps</p>&mdash; juwee (@juweeism) <a href="https://twitter.com/juweeism/status/1774069826241352146?ref_src=twsrc%5Etfw">March 30, 2024</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>
this is an article on how to package a go cli as a nix flake. however, instead of using a new go project, i will use [`nanoc`](https://github.com/nanopack-buffer/nanoc), the [NanoPack](https://nanopack.dev) codegen tool written in go as an example, because i think it more practically reflects how a flake is packaged.
this guide assumes that you have some basic knowledge of the nix language. you can [learn about the language basics here](https://nix.dev/tutorials/nix-language.html). regardless, i will try to map nix's constructs to more conventional JavaScript to hopefully make it easier to understand.
## a brief introduction
when you develop a project, you inevitably have to install some tools or libraries, such as cmake, node.js, or in this case, go, onto your system. normally, they are installed globally onto your system. over time, your system will be littered with unrelated tools, all available globally.
furthermore, when two projects require different versions of the same tool, installing it globally is not an option. this happens quite often, for example in node.js or python, when two projects require different versions of node.js. without nix, one might use [nvm](https://github.com/nvm-sh/nvm) and [pyenv](https://github.com/pyenv/pyenv) to solve this problem. with nix, however, you will no longer need separate tools to manage it.
## the structure of a flake file
a nix flake starts with a file called `flake.nix` at the root of your project. every `flake.nix` file contains an [attribute set](https://nix.dev/tutorials/nix-language.html#attribute-set) (think records, or objects in JavaScript) that contains the following attributes:
- `description`: a description of your flake.
- `inputs`: an attribute set of other nix packages you need for this flake. you can think of this as *importing* other nix packages.
- `outputs`: a function that receives the above inputs as parameters and produces an attribute set of *outputs* that your flake produce. an output can be an executable, a static library, or even a shell environment which i will get into.
this is what the skeleton of `flake.nix` looks like:
```nix
{
description = "my awesome flake";
inputs = { };
outputs = { ... }: { };
}
```
so far, the inputs and outputs are empty, so let's go through each of them.
## flake inputs
flake inputs are nix packages that you are importing to your own flake in order to use their outputs. an input can be another git repo, a git submodule, or even a local directory. [this page](https://nixos-and-flakes.thiscute.world/other-usage-of-flakes/inputs) gives an awesome overview of how to include an input in different ways.
nix maintains an official repository of more than 100,000 nix packages that you can import easily (the repository lives on github [here](https://github.com/NixOS/nixpkgs).) since `nanoc` requires some packages from the repository, we will need to include it in our flake file:
```nix
{
# ...
inputs = {
# include NixOS/nixpkgs github repo on tag "24.05"
nixpkgs.url = "github:NixOS/nixpkgs?tag=24.05";
};
# ...
}
```
in javascript, this will look like:
```js
const flake = {
inputs: {
nixpkgs: {
url: "github:NixOS/nixpkgs?tag=24.05"
}
}
}
```
some things to note here:
- nix has a special `github` protocol for importing a github repository.
- `nixpkgs` is just a variable and doesn't have to be `nixpkgs`, but the convention is to use `nixpkgs` when including the official repo.
i have some other flakes that require other inputs, but `nanoc` is not one of them. don't worry, i will publish a guide for them as well.
## flake outputs
flake outputs are things that a flake produces, be it a static library or an executable. it is defined as a function that receives the specified inputs as parameters and that produces an attribute set that describes the outputs of this flake. to use an input, you can specify the name of the input as a parameter. in `nanoc`'s case, it will be a parameter called `nixpkg`, corresponding to what is specified in `inputs`:
```nix
{
# ...
outputs = { nixpkgs, ... }: { };
}
```
translating it to javascript will look like this:
```js
const flake = {
outputs: ({ nixpkgs }) => ({ })
}
```
here comes the tricky part - the returned attribute set can contain many different attributes, and many of them require an explicit definition for each *system* this flake will be on. *system* here refers to a string that specifies the operating system as well as the processor architecture. the following will be used for `nanoc`:
- `"x86_64-linux"`
- `"x86_64-darwin"`
- `"aarch64-linux"`
- `"aarch64-darwin"`
unfortunately, i was not able to find official documentation on these magic strings, but they have special meanings in a flake and therefore must be exact. anyways, whenever you come across `${system}`, please substitute it with the four strings above (without the quotes). for example `a.${system}` will expand to `a.x86_64-linux`, `a.x86_64-darwin`, so on and so forth.
back to `outputs` - the attribute set returned by `outputs` can contain many attributes. for brevity, i will only list the ones that are used by `nanoc`, but you can refer to [here](https://nixos-and-flakes.thiscute.world/other-usage-of-flakes/outputs) for a full list of attributes. the ones that `nanoc` will be using are:
- `packages.${system}`: defines an attribute set containing all the packages this flake produces, including executables and libraries.
- `devShell`: defines an attribute set that describes the shell environment for development which can be triggered by running `nix develop`.
### exporting an executable
`nanoc` produces one output - the `nanoc` executable that is run to trigger codegen on nanopack schemas. to specify it manually, we would have had to repeat the definition *for each* `packages.${system}`, but thankfully `nixpkgs` provide a handy tool under `nixpkgs.lib` which is a function called `genAttrs`. it takes in an array of system strings, and produces a function that accepts a callback which is called on each system string and that produces an attribute set. after the callback is called on each system string, a final attribute set containing all system strings as attributes and the returned attribute set as the corresponding value. this may sound confusing, so here is what it would look like in javascript:
```js
function genAttrs(systems) {
return (callback) => {
const attrs = {}
for (const system of systems) {
attrs[system] = callback(system)
}
return attrs
}
}
```
the attribute set returned by genAttrs will look something like this:
```nix
{
# the empty attribute sets below are returned by calling the callback on each system string.
x86_64-linux: { };
x86_64-darwin: { };
aarch64-linux: { };
aarch64-darwin: { };
}
```
i will refer to this attribute set as a *system matrix*.
now, let's define the systems we support and use the `genAttrs` function to get a function that generates the correct system matrix set for us:
```nix
{
outputs = { nixpkgs, ... }:
let
supportedSystems = [ "x86_64-linux" "x86_64-darwin" "aarch64-linux" "aarch64-darwin" ];
forAllSystems = nixpkgs.lib.genAttrs supportedSystems;
nixpkgsFor = forAllSystems (system: import nixpkgs { inherit system; });
in
{ };
}
```
here, `forAllSystems` stores the function that generates a system matrix, and `nixpkgsFor` is a system matrix that maps a system to the correct attribute set that contains all usable packages for the system. the reason why we need to call import on `nixpkgs` is that `nixpkgs` contain an `outPath` attribute that `import` recognizes. `import` will then evaluates the nix file at `outPath` and returning the value - a function that returns an attribute set containing all nix packages in the repository. finally, the function is called by passing in the `system` parameter of the callback in order to obtain the correct attribute set of packages for the system.
with all that out of the way, let's specify the `nanoc` executable. `nixpkgs` provide a function called `buildGoModule` for building a go module ([docs here](https://nixos.org/manual/nixpkgs/stable/#sec-language-go)). it is built on top of `mkDerivation` with some go-specific options. both create the same *derivation* (a build task).
```nix
{
outputs = { nixpkgs, ... }:
let
supportedSystems = [ "x86_64-linux" "x86_64-darwin" "aarch64-linux" "aarch64-darwin" ];
forAllSystems = nixpkgs.lib.genAttrs supportedSystems;
nixpkgsFor = forAllSystems (system: import nixpkgs { inherit system; });
in
{
packages = forAllSystem(system:
let
pkgs = nixpkgsFor.${system};
in
{
nanoc = pkgs.buildGoModule {
pname = "nanoc";
version = "0.1.0";
src = ./.;
vendorHash = nixpkgs.lib.fakeHash;
buildInputs = [
# to be filled later
];
}
}
);
};
}
```
this is a lot, so let's break it down:
- `nanoc`, an attribute, defines the executable that we want to export. although it doesn't *have* to be named `nanoc`, it only makes sense to name it `nanoc` because it corresponds with the name of the executable.
- `nanoc` stores the value returned by `pkgs.buildGoModule` which is a derivation that describes how to build the go module.
- an attribute set was passed to `pkgs.buildGoModule` to specify options for the derivation:
- `pname` is a name for the package
- `version` is the version of the package
- `src` is the relative path to the directory where source code lives. in nix, a path must contain at least one slash (`/`) to be considered one, so simply using `.` to denote the current directory is not sufficient, which is why `./.` is used instead.
- `vendorHash` is the hash of the dependencies of this go module. because we can't know the hash when authoring the flake, `nixpkgs.lib.fakeHash` is used as a placeholder. the true hash can be obtained after running `nix build` - it will fail because it sees `fakeHash`, and nix will print out the actual hash which needs to replace `fakeHash`.
- `buildInputs` specifies an array of inputs that are needed at *runtime*. this is usually shared libraries or external executables that are executed during runtime.
#### specifying runtime dependencies
`nanoc` has these dependencies:
- clang-format, to format generated c++ code.
- biome, to format generated typescript code.
- swift-format, to format generated swift code.
luckily, they are all packaged as nix packages, so using them is as simple as:
```nix
buildInputs = [
pkgs.clang-tools
pkgs.biome
pkgs.swift-format
]
```
now, they will be automatically included in PATH when `nanoc` is run. the final `outputs` looks like this:
```nix
{
outputs = { nixpkgs, ... }:
let
supportedSystems = [ "x86_64-linux" "x86_64-darwin" "aarch64-linux" "aarch64-darwin" ];
forAllSystems = nixpkgs.lib.genAttrs supportedSystems;
nixpkgsFor = forAllSystems (system: import nixpkgs { inherit system; });
in
{
packages = forAllSystem(system:
let
pkgs = nixpkgsFor.${system};
in
{
nanoc = pkgs.buildGoModule {
pname = "nanoc";
version = "0.1.0";
src = ./.;
vendorHash = nixpkgs.lib.fakeHash;
buildInputs = [
pkgs.clang-tools
pkgs.biome
pkgs.swift-format
];
}
}
);
};
}
```
### declaring a development shell environment
this is my favorite feature about nix - being able to define and use development environments that are isolated from other projects. no longer is my global shell littered with unrelated tools that might conflict with each other!
a development shell environment is defined similarly to packages, but instead of `buildGoModule`, nixpkgs provide a function called `mkShell` that is used to create a derivation for a shell environment. before we dive into the code, let's have a look at what we need inside the development shell:
- go obviously.
- [go tools](https://pkg.go.dev/golang.org/x/tools), which contains various tools that aid in go development.
- all the `buildInputs` of nanoc because we will be running `nanoc` inside the shell, which means we will need the runtime dependencies of `nanoc` in the shell.
- cmake, to build the c++ examples (this will change soon as i will migrate away from cmake to build scripts.)
luckily for us, all of the above are available as nix packages under `nixpkgs`!
on to the code:
```nix
{
outputs = { nixpkgs, ... }:
let
supportedSystems = [ "x86_64-linux" "x86_64-darwin" "aarch64-linux" "aarch64-darwin" ];
forAllSystems = nixpkgs.lib.genAttrs supportedSystems;
nixpkgsFor = forAllSystems (system: import nixpkgs { inherit system; });
in
{
# ...
devShells = forAllSystems(system:
let
pkgs = nixpkgsFor.${system}
in
{
default = pkgs.mkShell {
packages = [
pkgs.go
pkgs.gotools
# nanoc requires clang-format in clang-tools
pkgs.clang-tools
# nanoc uses biome to format typescript code
pkgs.biome
# nanoc uses swift-format to format swift code
pkgs.swift-format
# used to build c++ examples
pkgs.cmake
]
};
};
);
};
}
```
instead of giving the shell a name, the shell derivation is defined under the special `default` attribute. the `default` shell is the shell that will be activated when running `nix develop` without specifying a name. we only need one development shell, so we can use it as the default one. if you use a different attribute name, you can activate the shell by running `nix develop .#{name}`, where `{name}` is the attribute name.
notice that we have specified all the tools we need in the shell as an array stored under the `packages` attribute. now, when we run `nix develop` inside our project, nix will create and activate a shell that contains all the tools that we have specified.
## using the flake
we have successfully packaged `nanoc` as a nix flake. if you wish to see the full flake file, head over [here](https://github.com/nanopack-buffer/nanoc/blob/main/flake.nix).
since `nanoc` is now a flake, we can use it in another flake as an input:
```nix
{
description: "a flake that uses nanoc";
inputs = {
nanoc.url = "github:nanopack-buffer/nanoc/main";
};
outputs = { nanoc, ... }: {
let
supportedSystems = [ "x86_64-linux" "x86_64-darwin" "aarch64-linux" "aarch64-darwin" ];
forAllSystems = nixpkgs.lib.genAttrs supportedSystems;
nixpkgsFor = forAllSystems (system: import nixpkgs { inherit system; });
in
{
packages = forAllSystems(system:
let
pkgs = nixpkgsFor.${system}
in
{
myPkg = pkgs.mkDerivation {
# ...
buildInputs = [
# notice how this corresponds to the structure of the attribute set
# defined in the flake file of nanoc!
nanoc.packages.${system}.nanoc
];
# ...
};
};
);
};
};
}
```
## searching through the nixpkgs repository
you can check whether a nix package is available by going to nixos's [official search page](https://search.nixos.org/packages). for example, you can verify that clang-format is provided under the clang-tools package:
![search results for "clang-format" showing the expanded entry for the `clang-tools` package](/nixpkg-search-cmake.png)
## references
- [nixos wiki - "Development environment with nix-shell"](https://nixos.wiki/wiki/Development_environment_with_nix-shell)
- [search.nixos.org](https://search.nixos.org)
- [pkgs.mkShell](https://ryantm.github.io/nixpkgs/builders/special/mkshell/)
- [nixos wiki - "Go"](https://nixos.wiki/wiki/Go)

View File

@@ -0,0 +1,302 @@
---
title: 'progressive blur in css'
description: 'a guide in how to implement progressive blur in css'
pubDate: '26 Oct 2024'
heroImage: '/progressive-blur-screenshot.png'
image: '/progressive-blur-screenshot.png'
---
progressive blur is a type of blur effect that blurs an object or background with gradually increasing strength. it is heavily used in apple's design, such as the status bar when the maps app is opened:
![progressive blur under the status bar when the maps app is opened in iOS](/progressive-blur-example.jpeg)
in this guide, i will show you how to achieve the same effect in css!
## how it works
progressive blur can be broken down into segments of blurs that increases in strength. here is an illustration that roughly breaks down the progressive blur found in the example above:
<picture>
<source srcset="/progressive-blur-illustration.png" media="(prefers-color-scheme: dark)" />
<img alt="an illustration of progressive blur" srcset="/progressive-blur-illustration-light.png" />
</picture>
each colored rectangle represents an area with a specific blur value, ordered from the strongest blur value to the weakest, top to bottom. each rectangle slightly overlaps each other so that the blur can transition from one value to another seamlessly.
while i used 5 rectangles in the illustration, in practice, the number can vary, depending on the exact effect you wish to achieve. 5 is a good number to start, but you can tweak the number to your liking. in fact, this goes for every number used in this guide - they are the result of countless hours spent on tweaking to my liking. please feel free to tweak those numbers to suit your preference.
all that's left to do is to map that illustration into html and css!
## laying out the structure
the html structure is straightforward. we want a `div` that contains 5 `div`s on top of each other. you will find out the reason why the `div`s aren't stacked in a column soon, but for now, bear with me:
```html
<div class="progressive-blur-container">
<div class="blur-filter"></div>
<div class="blur-filter"></div>
<div class="blur-filter"></div>
<div class="blur-filter"></div>
<div class="blur-filter"></div>
<div class="blur-filter"></div>
<div class="blur-filter"></div>
</div>
```
this is the css to lay them out correctly:
```css
.progressive-blur-container {
position: absolute;
left: 0;
bottom: 0;
right: 0;
width: 100%;
height: 50%;
pointer-event: none;
}
.progressive-blur-container > .blur-filter {
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
}
```
since `progressive-blur-container` will sit on top of the content, `pointer-event: none;` needs to be applied so that it doesn't block any pointer event to the content.
## onto the blur effect
as illustrated above, there needs to be different sections of blurs, each with a specific blur value, slightly overlapping each other. concretely, each section is a `div` that takes up the whole progressive blur area. to only blur a specific area within a section (and transparent for the rest of the section), we can use a combination of `mask` and `linear-gradient`. using this combo, `rgba(0,0,0,0)` maps to transparent, and `rgba(0,0,0,1)` maps to the blur effect. we can then control where the blur effect starts and ends by specifying the position of `rgba(0,0,0,1)` within the `linear-gradient`.
let's start with the first section which has the weakest blur:
```css
.progressive-blur-container > .blur-filter:nth-child(1) {
backdrop-filter: blur(1px);
mask: linear-gradient(rgba(0, 0, 0, 0), rgba(0, 0, 0, 1) 10%, rgba(0, 0, 0, 1) 30%, rgba(0, 0, 0, 0) 40%);
}
```
here, we are specifying that the blur area should start at 10% of the `div` and end at 30%, and we want the effect to end at 40%. here is an illustration, where the pink color represents where the blur effect is:
![Illustration of the first blur layer](/first-blur-section-demo.png)
then, for subsequent sections, we just need to adjust the percentages so that each subsequent section stacks like what's shown in the illustration:
```css
.progressive-blur-container > .blur-filter:nth-child(2) {
backdrop-filter: blur(2px);
mask: linear-gradient(rgba(0, 0, 0, 0) 10%, rgba(0, 0, 0, 1) 20%, rgba(0, 0, 0, 1) 40%, rgba(0, 0, 0, 0) 50%);
}
.progressive-blur-container > .blur-filter:nth-child(3) {
backdrop-filter: blur(4px);
mask: linear-gradient(rgba(0, 0, 0, 0) 15%, rgba(0, 0, 0, 1) 30%, rgba(0, 0, 0, 1) 50%, rgba(0, 0, 0, 0) 60%);
}
.progressive-blur-container > .blur-filter:nth-child(4) {
backdrop-filter: blur(8px);
mask: linear-gradient(rgba(0, 0, 0, 0) 20%, rgba(0, 0, 0, 1) 40%, rgba(0, 0, 0, 1) 60%, rgba(0, 0, 0, 0) 70%);
}
.progressive-blur-container > .blur-filter:nth-child(5) {
backdrop-filter: blur(16px);
mask: linear-gradient(rgba(0, 0, 0, 0) 40%, rgba(0, 0, 0, 1) 60%, rgba(0, 0, 0, 1) 80%, rgba(0, 0, 0, 0) 90%);
}
.progressive-blur-container > .blur-filter:nth-child(6) {
backdrop-filter: blur(32px);
mask: linear-gradient(rgba(0, 0, 0, 0) 60%, rgba(0, 0, 0, 1) 80%);
}
.progressive-blur-container > .blur-filter:nth-child(7) {
z-index: 10;
background-filter: blur(64px);
mask: linear-gradient(rgba(0, 0, 0, 0) 70%, rgba(0, 0, 0, 1) 100%)
}
```
each blur section overlaps the previous section to allow the blur to seamlessly transition. if we represent each blur value as different colors, it would look like the following:
<center>
<img alt="illustration of all the blur layers combined" src="/progressive-blur-gradient.png" />
</center>
pretty, isn't it? now imagine the blur getting progressively stronger as the color goes from pink to blue, with each intermediate color representing different blur strengths. what does it look like in action?
## testing what we have
the following html code displays a long, scrollable text to demonstrate the progressive blur:
```html
<div class="container">
<p>replace me with really long text</p>
<div class="progressive-blur-container">
<div class="blur-filter"></div>
<div class="blur-filter"></div>
<div class="blur-filter"></div>
<div class="blur-filter"></div>
<div class="blur-filter"></div>
<div class="blur-filter"></div>
<div class="blur-filter"></div>
</div>
</div>
```
some additional css:
```css
.container {
position: relative;
width: 500px;
}
p {
height: 300px;
overflow: scroll;
}
```
if you have followed each step correctly, you should see a really nice blur effect at the bottom of the paragraph.
## hiding the blur "glitches"
you may notice that when you scroll, the bottom of the blur area starts "glitching" and becomes really distracting:
<video controls src="/progressive-blur-glitching.mov"></video>
to solve this, we can add a background gradient that goes from transparent to the color of the page, which is `#ffffff` in this case:
```html
<div class="progressive-blur-container">
<div class="blur-filter"></div>
<div class="blur-filter"></div>
<div class="blur-filter"></div>
<div class="blur-filter"></div>
<div class="blur-filter"></div>
<div class="blur-filter"></div>
<div class="blur-filter"></div>
<!-- this gradient hides the glitching -->
<div class="gradient"></div>
</div>
```
the css is straightforward:
```css
.progressive-blur-container > .gradient {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(transparent, #ffffff);
}
```
now, the glitch should be gone:
<video controls src="/progressive-blur-demo.mov"></video>
we are done! we have successfully implemented progressive blur in css.
## final code
```html
<div class="container">
<p>really long text...</p>
<div class="progressive-blur-container">
<div class="blur-filter"></div>
<div class="blur-filter"></div>
<div class="blur-filter"></div>
<div class="blur-filter"></div>
<div class="blur-filter"></div>
<div class="blur-filter"></div>
<div class="blur-filter"></div>
<div class="gradient"></div>
</div>
</div>
```
```css
.container {
position: relative;
width: 500px;
}
.progressive-blur-container {
position: absolute;
left: 0;
bottom: 0;
right: 0;
width: 100%;
height: 50%;
pointer-event: none;
}
.progressive-blur-container > .blur-filter {
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
}
.progressive-blur-container > .blur-filter:nth-child(1) {
backdrop-filter: blur(1px);
mask: linear-gradient(rgba(0, 0, 0, 0), rgba(0, 0, 0, 1) 10%, rgba(0, 0, 0, 1) 30%, rgba(0, 0, 0, 0) 40%);
}
.progressive-blur-container > .blur-filter:nth-child(2) {
backdrop-filter: blur(2px);
mask: linear-gradient(rgba(0, 0, 0, 0) 10%, rgba(0, 0, 0, 1) 20%, rgba(0, 0, 0, 1) 40%, rgba(0, 0, 0, 0) 50%);
}
.progressive-blur-container > .blur-filter:nth-child(3) {
backdrop-filter: blur(4px);
mask: linear-gradient(rgba(0, 0, 0, 0) 15%, rgba(0, 0, 0, 1) 30%, rgba(0, 0, 0, 1) 50%, rgba(0, 0, 0, 0) 60%);
}
.progressive-blur-container > .blur-filter:nth-child(4) {
backdrop-filter: blur(8px);
mask: linear-gradient(rgba(0, 0, 0, 0) 20%, rgba(0, 0, 0, 1) 40%, rgba(0, 0, 0, 1) 60%, rgba(0, 0, 0, 0) 70%);
}
.progressive-blur-container > .blur-filter:nth-child(5) {
backdrop-filter: blur(16px);
mask: linear-gradient(rgba(0, 0, 0, 0) 40%, rgba(0, 0, 0, 1) 60%, rgba(0, 0, 0, 1) 80%, rgba(0, 0, 0, 0) 90%);
}
.progressive-blur-container > .blur-filter:nth-child(6) {
backdrop-filter: blur(32px);
mask: linear-gradient(rgba(0, 0, 0, 0) 60%, rgba(0, 0, 0, 1) 80%);
}
.progressive-blur-container > .blur-filter:nth-child(7) {
z-index: 10;
background-filter: blur(64px);
mask: linear-gradient(rgba(0, 0, 0, 0) 70%, rgba(0, 0, 0, 1) 100%)
}
.progressive-blur-container > .gradient {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(transparent, #ffffff);
}
p {
height: 300px;
overflow: scroll;
}
```
## codepen link
[link to codepen demo](https://codepen.io/kennethnym/pen/eYqyRjQ)

View File

@@ -0,0 +1,134 @@
---
title: Selection Detection with Vector Math
description: 'a guide in how to implement object selection detection in canvas with vector math'
pubDate: '5 Jan 2025'
useKatex: true
heroImage: /selection-projection.png
image: /selection-projection.png
---
I am currently working on making my own whiteboard program using [Dear ImGui](https://github.com/ocornut/imgui), and one of the features I had to implement was the ability to select objects on the whiteboard. Since ImGui does not offer click detections for custom shapes, I had to implement my own selection detection algorithm. With the help of some vector math and the Internet, it was actually quite straightforward.
## Framing the Problem
Whether an object should be selected can be framed as asking how close a mouse click is to an object. If the distance is below a threshold, then we can consider the object to be selected.
Let's reframe this question again in math. Consider a line (segment) $\textbf{b}$ with two points, $v_1$ and $v_2$, and point $P$, where the mouse click happened. The problem is to find a point on $\textbf{b}$ that is closest to $P$. Intuitively, that point can be found by drawing a perpendicular line from $P$ to $\textbf{b}$, and the intersection point will be the closest point on $\textbf{b}$ to $P$. The distance between the two points then becomes the closest distance between $P$ and $\textbf{b}$. Below is an illustration of the problem:
<picture>
<source srcset="/selection-detection-problem.png" media="(prefers-color-scheme: dark)" />
<img alt="an illustration of the selection detection problem" src="/selection-detection-problem-light.png" />
</picture>
where:
- $P$ is the point where the mouse click occurred;
- $\textbf{b}$ is the line that can be selected; and
- $d$ is the shortest distance between $P$ and $\textbf{b}$.
## Applying Vector Math
Notice how the letter $\textbf{b}$ is bolded. This means that the line is treated as a vector. Now, we introduce another vector $\textbf{a}$ from the beginning of $\textbf{b}$ to $P$. Observe that the projection of $\textbf{a}$ onto $\textbf{b}$ $proj_{\textbf{b}}\textbf{a}$ points to the shortest point on $b$ from $P$.
<picture>
<source srcset="/selection-projection.png" media="(prefers-color-scheme: dark)" />
<img alt="vector projection illustration" src="/selection-projection-light.png" />
</picture>
The projection is written as:
$$
proj_{\textbf{b}}\textbf{a} = \frac{\textbf{a}\cdot\textbf{b}}{||\textbf{b}||} \hat{\textbf{b}}
$$
where
- $||\textbf{b}||$ is the magnitude of $\textbf{b}$
- $\hat{\textbf{b}}$ is the unit vector of $\textbf{b}$
Recall that the magnitude of a vector $\textbf{a}$ is defined as:
$$
||\textbf{a}|| = \sqrt{\sum_{i=1}^{n}x_i^2}
$$
and its unit vector as:
$$
\hat{\textbf{a}} = \frac{\textbf{a}}{||\textbf{a}||}
$$
for any finite n-dimensional vector $\textbf{a} \in \mathbb{R}^n$.
Substituting into the projection formula, we get
$$
proj_{\textbf{b}}\textbf{a} = \frac{\textbf{a}\cdot\textbf{b}}{\textbf{b}\cdot\textbf{b}}\textbf{b}
$$
### Finding $\textbf{a}$ and $\textbf{b}$
To find $\textbf{a}$, we treat $v_1$ and $P$ as two vectors:
<picture>
<source srcset="/selection-detection-find-a.png" media="(prefers-color-scheme: dark)" />
<img alt="vector from starting point of b to P" src="/selection-detection-find-a-light.png" />
</picture>
Then, we subtract $\textbf{v}_1$ from $\textbf{P}$ using vector subtraction:
$$
\textbf{a} = \textbf{P} - \textbf{v}_1
$$
$\textbf{b}$ can be found in a similar fashion.
### Finding the Intersection Point
Once the projection vector is found, we add it to $\textbf{v}_1$ to obtain the coordinates of the intersection point:
<picture>
<source srcset="/selection-detection-find-intersection.png" media="(prefers-color-scheme: dark)" />
<img alt="find intersection point" src="/selection-detection-find-intersection-light.png" />
</picture>
### Computing the Shortest Distance
Finally, to find the shortest distance between $P$ and $\textbf{b}$, we now have two choices:
- Find the euclidean distance between $P$ and the intersection point
- Perform vector subtraction between the two points, then find its magnitude.
In either case, we have successfully found the shortest distance between a point and a line segment!
## Special Cases
There are two special cases that we have not considered:
1. The projection of the point falls outside of $\textbf{b}$ to the left of it
2. The projection of the point falls outside of $\textbf{b}$ to the right of it
<picture>
<source srcset="/selection-detection-special-cases.png" media="(prefers-color-scheme: dark)" />
<img alt="special cases of the selection detection problem" src="/selection-detection-special-cases-light.png" />
</picture>
In the first case, the dot product $\textbf{a} \cdot \textbf{b}$ is less than zero. Therefore, if we find that the dot product is less than zero, we know that the closest point from $P$ to $\textbf{b}$ is the starting point of $\textbf{b}$.
In the second case, the projection of $\textbf{a}$ onto $\textbf{b}$ is longer than $\textbf{b}$. We can write this relation as:
$$
\begin{align}
||proj_{\textbf{b}}\textbf{a}|| &> ||\textbf{b}|| \nonumber \\
\frac{\textbf{a}\cdot\textbf{b}}{||\textbf{b}||} &> ||\textbf{b}|| \nonumber \\
\textbf{a}\cdot\textbf{b} &> ||\textbf{b}||^2 \nonumber\\
\textbf{a}\cdot\textbf{b} &> \textbf{b}\cdot\textbf{b} \nonumber\\
\end{align}
$$
Therefore, when $\textbf{b}\cdot\textbf{b}$ is greater than $\textbf{a}\cdot\textbf{b}$, we know that we are in the second case, and we can infer that the end point of $\textbf{b}$ is the closest to $P$.
## Implementation Note
At the final step of finding the distance between a point and the intersection point, a square root is involved using either method. Since we only care about whether the distance meets a threshold and not the actual number, we can skip the square root calculation and compare it with the square of the threshold we want. For example, if we want the threshold to be 5px for an object to be considered selected, we compare the computation with $5^2 = 25$.

View File

@@ -0,0 +1,9 @@
---
title: untitled
description: untitled
pubDate: 'Apr 3 2025'
---
<code class="break-all">
G3F4g3O4m/bsbE7vbLcopgeYggh57kLt1iEtwzlCXIMYEZpzjikx0ZJcMDOcCGFTZq+maadAeE/Kv7FI12tkNqN/QtPO0jbMDEjbJPNhmkcDeVmfATxpyLfe/qkjHQWdTU+15/JOiO8UPp1r+MUD0PPvUd88h1DseODt5jrgYKE1a+Con978gXXuqw6CIPG0DRWKwyyOYM7nqbhGoXS/ivrFZ5wQjZtz8tPImLRAvFSg0KQAXsE87e12Xr5a7mcsZMi9f9TEBVVHUf0OO/v6VG5wXLB+G4oj64djsFTp4jQ+ytRWN1gjNtVvk5Xp+aLn5EhpbuNgfwwmG9OP9JzbyLAUjrDxDOvRhrG9EEis01n2MDO30+D+gurvinZVpb928J70ZUuX2Yi3zgdW47SVJAVEH+1ZS2i+ih/fOLOjlKSJeaKvIpTqbkirv47POznUNI7FgsNfHyVNyxkwCy8l+DcPO9BfYRT3QMECr6rVlSs5bwyJ6rySdRcS7i01/yq10L9efNwulfikwPFLU+EHMBF0eP8Ku9G+gGZ3YBIgqR6jhg5R4YqK/pN8cVxw/eY0LS7NDoxgpvhQhMsJIyIfFbLDf4pKAD2ijmin5BhKdKYCrWI/CMoonngCXusF1OgKO61Y25CvhsvyGhP9rgf5TvjwbcSRna7GoQfT6Wy4P+WNWmz3bbpZlQqD2TbnZf1x6v7shNuPKZXp4WKtHh10GSOM6l7Xf0d9denVUpVrEcAS0eQgVrexkD/5b2FcGJ/2ngjJgFTwdQI5vZ55q7d0jJgWJTFgcYp8gAR4YhEwad2/b6C8ilbWrrRQ3LRW7JLiCrYpNHKBf7x0yA9+o6HJqsvQQs9M4K1D2ts7MgExg+xj6i84QXJgCdk+ieJI6uP5A/Kl6+HST/vX42ZzlUzWezZ3Ks3U4V6MRGDKKnW/F6rtMliWLW/fdSTsIdwZEFkun1FYOpMlp0yaRPP81q1chPxSTMWFSTYNktsMfJXOOGt6VIh0/kxIp/D4aiS+8aVA0eLNRCZcNhK4XSW3N3sVmVteIbcRpaPlYLkz26bDDydcZmMbTdW4pxIxUTRqFMDftfJDOW91c8mvx5a4qH4yVzPMB7klqss9/Q3MiiypfFgo98kB9e+0eiAUt4QupY09asYVqI2MpKygYAYKOyECXc0ZfbvBUe6BURl0J2/fDV5eih41BTmBaAMzccKpTJ1/8KZ0S4EZieV3HqLHOf4rU/Dx5yKItjZFdCRso17s4k8wcOZ7RGfa6D7j/Ekc4TWLZ7BTFPMCTDCs/xB/NMArRR1GuIVYps6Zz7Y30KYlU16d/nsoxMQY74ueGltYPU/dKz+wcieyqyUaFo5sK3UlzMnnCLvL1kK/Z0XarIMQUic4KAICetUhzdxDYD+gJNxm2+Eiu95AlHvQxsr25KDCRorG9rvigGpZVpoPf8Axr18R1j8eCfk6Yim0ocGLKXDol4STtDBMubMu/uu83GZL2RsMK1z9nJCCDKxoz0mIswawk/0yhzfbEQdP5ZdZh7REyo+mDQXr1JgR3UQUVZqsF/Hs7D5oCVwXBwihiEX0AMEp4kMSLkmuhHvESwADrFx7NdMuSMjiBW1Wt6Wis5MXC5qBYIGomDvuZ61cS5BmZpVfHb+IabeCNBaB/lKng69DPrlvppNqPb6hBE4Gwe8zOBUxAEKmlPie/VtZjXqlGAv+2DKOilRW8eduTGC2E0XtnqJPRTem/wAw00asvUeSVjXf9GR2UJzjB39+q/bO3YFB85HdYxG7hIW6J8yjz7ErQQKDkEqU8ta2HKlJIM1IZu+2ETCPe0k3BS7/TpuNL4iu00YA9ZFJia8+Y0qezm7kGVVfoy09CnTkbTL1iyREX5MqhgLQmZCAeTR1k/nAXU+10bAN+KSK3THniznu90i7ARs6CCnK14yeB8jCnu9suzXoS8FAQ2SJ431/pEPeK6/JDepyrECCXk3ulVgJIvFlD2jwWT/toMLahHN8M94RvGEwruMfyeY4iHz+LXIOLTDj61TiQMNP3zg=
</code>

View File

@@ -0,0 +1,30 @@
---
title: the art of programming and why i won't use llm
description: the art of programming and why i won't use llm
pubDate: '25 Aug 2024'
---
as llms get better and better at writing code, more and more people, at least on twt, have started to incorporate llms into their workflow. most people seem to agree that llms have been a game changer for coding, praising them for how much they have improved their productivity, how much easier it is to write code, and claiming that programmers who refuse to use them are "not using them correctly" and will eventually get left behind
in my opinion, the effectiveness of llms in coding at their current state is vastly overblown. even if llms were as good as what avid users of them claim, i still won't see myself using it in any meaningful capacity.
<blockquote class="twitter-tweet" data-theme="dark"><p lang="zxx" dir="ltr"><a href="https://t.co/Mq8CIQiS9A">pic.twitter.com/Mq8CIQiS9A</a></p>&mdash; ThePrimeagen (@ThePrimeagen) <a href="https://twitter.com/ThePrimeagen/status/1827740308219605272?ref_src=twsrc%5Etfw">August 25, 2024</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>
## the art of programming
programming can be broken down into two parts - solving problems algorithmically, breaking problems into steps that computers can follow within some contraints, thus forming a solution to the original problem; and expressing the solution in a way that the computer can understand.
both parts provide the programmers with an infinite canvas on which they can express their creativitiy. there are practically limitless ways to approach and solve a problem, and a practically infinite way to express a solution to the problem. hence, programming is a form of self-expression - it is an art form. what is produced through programming is a kind of art - an art few appreciate.
## i am a programming artist
in that sense, i see myself as an artist, one that expresses his creative self through programming. i enjoy creating programming art, because only through it do i find my true self, one who has a burning passion to create and build things.
## llm is not for me
using llm to write code is like asking an artist to paint for you. if you only want the end result, by all means! if you are like me who enjoy the process of painting, then why would you bother automating the fun part away? one may say, "but i am only using llm to write code. i am still doing the problem solving myself!". to me, programming isn't complete if i don't get to express the solution in code myself. it isn't my art if i don't create it myself.
## a sad reality
it is sad to me just how much people are trying to automate away programming and delegating it to a black box that can't even count letters in a word sometimes, even going as far as trying to emulate a software engineer on top of the black box. does no one not find programming fun anymore? does no one care enough about programming to go further beyond getting things working "well enough"? is this just another case of availability bias?
please don't take this as a judgemental piece to anyone that i am alluring to. it's fine to not find programming enjoyable. it's fine to just want things to work. i am just disappointed at how the ones who care appear to be an ever dying breed.

View File

@@ -0,0 +1,17 @@
---
title: your environment dictates everything
description: why your environment matters more than you think
pubDate: Dec 4, 2024
---
One thing I have learned the hard way over the past two years is that your environment, including your room, your social circle, your workplace, and even the content you consume, impacts you way more than you think. There is an old Chinese saying that goes:
> Living with nice people is like entering a room full of flowers. Over time, you stop smelling the scent of the flowers. This is because you have adapted to the room.
If there is only one advice I can give, it will be this: gatekeep your environment, from your social circle to the content you consume. Cut off people who drag you down. Stop consuming content that reinforce negative thoughts. Don't engage in meaningless social media arguments. Cultivate an environment that is positive, supportive, and inspirational.
I used to surround myself with people who played video games all day and were involved in drama all the time. During that period, I would spend most of my time playing video games with them, and I would get myself involved in the dramas as well, because I used to think as a "friend" I should be responsible for their emotions as well. Worse, I caught feelings for people I should not have. What ended up happening was that I lost motivation, lost track of my life, and became depressed. I would consume all kinds of sad content that reinforced my emotions. Working out consistently did not help at all.
The turning point was when I started interacting with people on x dot com. I was inspired by people doing cool things and pushing the boundary of technology. That inspiration became my motivation to pick myself back up - to upskill myself so that I am no longer standing on the shoulder of giant, but instead becoming part of the giant. I started cutting my video game consumption. I stopped listening to sad music. I stopped watching sad "motivational" videos. I met many cool people on x. I started building things. I even made this blog to document my journey. I have learned so much in 2024 that I felt like a completely different person.
Do I still feel down sometimes? Of course, but I see happiness as contentment. Contentment breeds complacency. I have only one short life to do great things. There is no time to be complacent. The only time I will be truly happy is when I will be on my death bed feeling satisfied of what I will have achieved.

View File

@@ -1,7 +1,7 @@
import { defineCollection, z } from 'astro:content';
import { defineCollection, z } from "astro:content";
const blog = defineCollection({
type: 'content',
type: "content",
// Type-check frontmatter using a schema
schema: z.object({
title: z.string(),
@@ -10,6 +10,7 @@ const blog = defineCollection({
pubDate: z.coerce.date(),
updatedDate: z.coerce.date().optional(),
heroImage: z.string().optional(),
useKatex: z.boolean().optional(),
}),
});

View File

@@ -5,22 +5,37 @@ import Header from "../components/Header.astro";
import Footer from "../components/Footer.astro";
import FormattedDate from "../components/FormattedDate.astro";
type Props = CollectionEntry<"blog">["data"];
type Props = CollectionEntry<"blog">["data"] & { useKatex?: boolean };
const { title, description, pubDate, updatedDate, heroImage } = Astro.props;
const {
title,
description,
pubDate,
updatedDate,
heroImage,
useKatex = false,
} = Astro.props;
---
<!doctype html>
<html lang="en" class="latte dark:mocha">
<head>
<BaseHead title={title} description={description} image={heroImage} />
{
useKatex ? (
<link
href="https://cdn.jsdelivr.net/npm/katex@0.16.19/dist/katex.min.css"
rel="stylesheet"
/>
) : null
}
</head>
<body class="bg-base text-text max-w-prose m-auto p-8">
<body class="blog bg-base text-text max-w-prose m-auto p-8">
<Header />
<main class="py-10">
<article>
<div class="prose dark:prose-invert">
<div class="prose prose-lg dark:prose-invert">
<div>
<div class="opacity-60">
<FormattedDate date={pubDate} />
@@ -33,8 +48,7 @@ const { title, description, pubDate, updatedDate, heroImage } = Astro.props;
)
}
</div>
<h1 class="m-0 mt-4">{title}</h1>
<hr />
<h1 class="m-0 my-4">{title}</h1>
</div>
<slot />
</div>

View File

@@ -10,9 +10,6 @@ import { getBlogs } from "../content/blog";
const posts = (await getBlogs()).sort(
(a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf(),
);
const current = new Date();
const currentMonth = current.getMonth();
const currentYear = `${current.getUTCFullYear() - 1}`.substring(2);
---
<!doctype html>
@@ -36,9 +33,8 @@ const currentYear = `${current.getUTCFullYear() - 1}`.substring(2);
cross-platform GUI framework for building OS-native applications.
</li>
<li>
<Link href="https://polygui.org/nanopack/introduction/"
>nanopack</Link
>: a zero-runtime, type-safe binary serialization format.
<Link href="https://github.com/kennethnym/infinifi">infinifi</Link>:
a website that plays gentle, ai-generated lo-fi music indefinitely
</li>
<li>
<Link href="https://github.com/kennethnym/mai">mai</Link>:
@@ -70,9 +66,7 @@ const currentYear = `${current.getUTCFullYear() - 1}`.substring(2);
<main
class="py-8 px-4 max-w-4xl flex flex-col items-center space-y-0 leading-tight"
>
<header class="font-bold text-center">
KENNETHNYM v{currentYear}.{currentMonth + 3}
</header>
<header class="font-bold text-center">KENNETHNYM v23.5</header>
<p class="leading-none">&nbsp;</p>
<p class="text-center">software engineer. unpaid hhkb salesman.</p>
<p>&nbsp;</p>

View File

@@ -33,11 +33,50 @@
font-style: normal;
font-display: swap;
}
@font-face {
font-family: "Georgia";
src:
url("/fonts/georgia-webfont.woff2") format("woff2"),
url("/fonts/georgia-webfont.woff") format("woff");
font-weight: normal;
font-style: normal;
}
@font-face {
font-family: "Georgia";
src:
url("/fonts/georgiab-webfont.woff2") format("woff2"),
url("/fonts/georgiab-webfont.woff") format("woff");
font-weight: bold;
font-style: normal;
}
@font-face {
font-family: "Georgia";
src:
url("/fonts/georgiai-webfont.woff2") format("woff2"),
url("/fonts/georgiai-webfont.woff") format("woff");
font-weight: normal;
font-style: italic;
}
@font-face {
font-family: "Georgia";
src:
url("/fonts/georgiaz-webfont.woff2") format("woff2"),
url("/fonts/georgiaz-webfont.woff") format("woff");
font-weight: bold;
font-style: italic;
}
body {
body,
header,
footer,
pre {
font-family: "CommitMono", monospace, monospace;
}
body.blog {
font-family: "Source Serif 4", "Georgia", serif;
}
.nf {
font-family: "NerdFont";
}