47 Commits

Author SHA1 Message Date
f696554fee Merge pull request 'Add book read section to my website' (#1) from book-reads into main
Some checks failed
/ Deploy website to server (push) Failing after 15s
Reviewed-on: #1
2025-08-06 01:21:47 +01:00
85f91aed49 fix book read layout header overlap on mobile 2025-08-06 01:18:08 +01:00
2936ab3384 add notes and web pages for them 2025-08-06 01:15:18 +01:00
5dc7db36f8 initial book read page layout 2025-08-04 00:43:27 +01:00
9d0e68f5f6 migrate to tailwindcss 4
Some checks failed
/ Deploy website to server (push) Failing after 1m50s
2025-08-03 23:02:35 +01:00
35ba12de9e new poast
Some checks failed
/ Deploy website to server (push) Failing after 1m59s
2025-07-30 22:16:48 +01:00
7a01764c0c new poast
Some checks failed
/ Deploy website to server (push) Failing after 21s
2025-07-04 01:54:56 +01:00
72525c7f5b rename poast file name
Some checks failed
/ Deploy website to server (push) Failing after 17s
2025-05-28 19:22:15 +01:00
09d869e7c5 new poast
Some checks failed
/ Deploy website to server (push) Failing after 18s
2025-05-28 19:16:30 +01:00
b8d3aba4c0 fix github action config typo 2025-05-28 19:16:21 +01:00
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
65 changed files with 4489 additions and 2319 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,18 +1,27 @@
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";
import tailwindcss from "@tailwindcss/vite";
// https://astro.build/config
export default defineConfig({
site: "https://kennethnym.com",
integrations: [mdx(), sitemap(), tailwind()],
integrations: [mdx(), sitemap()],
markdown: {
shikiConfig: {
// Choose from Shiki's built-in themes (or add your own)
// https://shiki.style/themes
theme: "catppuccin-mocha",
},
remarkPlugins: [remarkMath],
rehypePlugins: [rehypeKatex],
},
vite: {
plugins: [tailwindcss()],
},
});

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

@@ -1,29 +1,31 @@
{
"name": "website",
"type": "module",
"version": "0.0.1",
"scripts": {
"dev": "astro dev",
"start": "astro dev",
"build": "astro check && astro build",
"preview": "astro preview",
"astro": "astro"
},
"dependencies": {
"@astrojs/check": "^0.5.6",
"@astrojs/mdx": "^2.1.1",
"@astrojs/rss": "^4.0.5",
"@astrojs/sitemap": "^3.1.1",
"@astrojs/tailwind": "^5.1.0",
"astro": "^4.4.15",
"tailwindcss": "^3.4.1",
"typescript": "^5.4.2"
},
"devDependencies": {
"@catppuccin/tailwindcss": "^0.1.6",
"@flydotio/dockerfile": "latest",
"@tailwindcss/typography": "^0.5.10",
"prettier": "^3.2.5",
"prettier-plugin-astro": "^0.13.0"
}
}
"name": "website",
"type": "module",
"version": "0.0.1",
"scripts": {
"dev": "astro dev",
"start": "astro dev",
"build": "astro check && astro build",
"preview": "astro preview",
"astro": "astro"
},
"dependencies": {
"@astrojs/check": "^0.5.6",
"@astrojs/mdx": "^2.1.1",
"@astrojs/rss": "^4.0.5",
"@astrojs/sitemap": "^3.1.1",
"@tailwindcss/vite": "^4.1.11",
"astro": "^4.4.15",
"tailwindcss": "^4.1.11",
"typescript": "^5.4.2"
},
"devDependencies": {
"@catppuccin/tailwindcss": "1.0.0",
"@flydotio/dockerfile": "latest",
"@tailwindcss/typography": "^0.5.10",
"prettier": "^3.2.5",
"prettier-plugin-astro": "^0.13.0",
"rehype-katex": "^7.0.1",
"remark-math": "^6.0.0"
}
}

4973
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

@@ -1,7 +1,7 @@
---
// Import the global.css file here so that it is included on
// all pages through the use of the <BaseHead /> component.
import '../styles/global.css';
import "../styles/global.css";
interface Props {
title: string;
@@ -11,7 +11,7 @@ interface Props {
const canonicalURL = new URL(Astro.url.pathname, Astro.site);
const { title, description, image = '/blog-placeholder-1.jpg' } = Astro.props;
const { title, description, image = "/blog-placeholder-1.jpg" } = Astro.props;
---
<!-- Global Metadata -->
@@ -23,7 +23,19 @@ const { title, description, image = '/blog-placeholder-1.jpg' } = Astro.props;
<link rel="canonical" href={canonicalURL} />
<!-- RSS autodiscovery -->
<link rel="alternate" type="application/rss+xml" title={title} href={`${Astro.site}rss.xml`} />
<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>

View File

@@ -1,15 +1,15 @@
<div id="command-line" class="w-full flex flex-row bg-base">
<div id="command-line" class="w-full flex flex-row bg-ctp-base">
<input
aria-hidden="true"
id="command-line-input"
type="text"
class="bg-base focus:outline-none active:outline-none cursor-default caret-transparent m-0"
class="bg-ctp-base focus:outline-none active:outline-none cursor-default caret-transparent m-0"
/>
<div id="caret" aria-hidden="true" class="bg-text inline-block">&nbsp;</div>
<p
aria-hidden="true"
id="status-text"
class="absolute w-full h-full bg-base hidden italic"
class="absolute w-full h-full bg-ctp-base hidden italic"
>
</p>
</div>
@@ -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);
@@ -90,7 +94,7 @@
} else {
statusText.innerHTML = `E492: Not an editor command: ${command}`;
statusText.classList.remove("hidden");
statusText.classList.add("text-red");
statusText.classList.add("text-ctp-red");
}
}

View File

@@ -6,5 +6,6 @@ import Link from "./Link.astro";
<nav class="flex flex-row items-center space-x-4">
<p class="font-bold text-lg">kennethnym</p>
<Link href="/">home</Link>
<Link href="/read">books</Link>
</nav>
</header>

View File

@@ -6,9 +6,9 @@ const { href, class: className, ...props } = Astro.props;
href={href}
class:list={[
"underline",
"text-blue",
"hover:text-lavender",
"visited:text-mauve",
"text-ctp-blue",
"hover:text-ctp-lavender",
"visited:text-ctp-mauve",
className,
]}><slot /></a
>

View File

@@ -4,17 +4,17 @@ import CommandLine from "./CommandLine.astro";
---
<div class="hidden sm:block absolute w-full bottom-0">
<footer class="w-full bg-crust flex flex-row leading-tight">
<footer class="w-full bg-ctp-mantle flex flex-row leading-tight">
<span
id="status-indicator"
class="bg-blue font-bold text-base inline-block leading-tight"
class="bg-ctp-blue font-bold text-ctp-base inline-block leading-tight"
>&nbsp;NORMAL&nbsp;</span
>
<div id="project-list" class="flex flex-row bg-surface0">
<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>
@@ -40,14 +40,14 @@ import CommandLine from "./CommandLine.astro";
cmdLine.addEventListener("cmdmodeenabled", () => {
statusIndicator.innerHTML = "&nbsp;COMMAND&nbsp;";
statusIndicator.classList.remove("bg-blue");
statusIndicator.classList.add("bg-peach");
statusIndicator.classList.remove("bg-ctp-blue");
statusIndicator.classList.add("bg-ctp-peach");
});
cmdLine.addEventListener("cmdmodedisabled", () => {
statusIndicator.innerHTML = "&nbsp;NORMAL&nbsp;";
statusIndicator.classList.add("bg-blue");
statusIndicator.classList.remove("bg-peach");
statusIndicator.classList.add("bg-ctp-blue");
statusIndicator.classList.remove("bg-ctp-peach");
});
window.addEventListener("openhelp", () => {
@@ -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,53 @@
---
title: a gentle introduction to cloudflare tunnels
description: A guide in using CloudFlare Tunnels to expose services to the Internet
pubDate: May 28 2025
---
CloudFlare Tunnel allows you to expose services within a network to the public without a publicly routable IP address. For example, you can expose a website to the public under a public hostname, without exposing your machine's IP address. You don't even need to deal with SSL for HTTPS, as this is automatically handled by CloudFlare! This can be beneficial because, for one, exposing your machine directly to the Internet poses a potential security risk; for another, it might not be feasible to do so. By exposing your services with Tunnel, you receive all the benefits of using CloudFlare network, including DDoS protection, caching, and more, without exposing your machines to the public.
CloudFlare Tunnels support many protocols, including HTTP/S, TCP, UDP, and even SSH. This unlocks many use cases beyond exposing HTTP services. As an example, you can expose a game server, such as a Minecraft server, over TCP, under a publicly accessible hostname.
## How does CloudFlare Tunnel work?
CloudFlare Tunnel relies on a persistent *connector* called `cloudflared` that establishes an **encrypted** connection between your machine and the global CloudFlare network. Each connector is bound to a tunnel, which maps a public hostname to an address the connector can access. As an example, [code.nym.sh](code.nym.sh), my public Gitea instance, is mapped to `http://localhost:3000`. Since the associated `cloudflared` is running on the same host, it can access `localhost`, and therefore any locally running service, including Gitea.
Suppose a service is exposed under `service-a.com` which is publicly accessible, and an HTTP request is made to it. The request will go through the following steps:
1. It is routed to CloudFlare's network
2. Based on the hostname, it is then forwarded to the corresponding tunnel.
3. The request is then forwarded to the appropriate connector, which then forwards it to the configured (local) address.
This is a high-level overview of CloudFlare tunnel's architecture. If you are interested in the details of Tunnel, including how it handles redundancy and failover, check out [this official reference document](https://developers.cloudflare.com/reference-architecture/architectures/sase/#tunnels-to-self-hosted-applications).
## Deploying `cloudflared`
There are different ways you can deploy `cloudflared`. I have tried the following approaches:
1. Running `cloudflared` on the same host where the services are running (this is my setup currently)
2. Running `cloudflared` on a separate machine in the same network (for example, within the same Tailnet) where the services are running
In the first scenario, an instance of `cloudflared` is deployed on the machine that is running the service you wish to expose. In my case, I have 2 machines running different services I need to expose, so I deployed 2 instances of `cloudflared`, each responsible for exposing the services running on the same machine.
In the second scenario, an instance of `cloudflared` is deployed on a dedicated machine that acts as the "gateway" to services running in your network. This can look like one machine running `cloudflared` in a Tailnet that is responsible for the tunneling of services running in the same network, potentially on different machines.
There are no right or wrong approaches. I am providing these scenarios as reference points, but use your best judgment to adapt them to your needs.
## Configuring CloudFlare Tunnel
*Before creating a Tunnel, make sure that the domains you wish to use is added to CloudFlare ([guide here](https://developers.cloudflare.com/fundamentals/setup/manage-domains/add-site/)).*
Creating a Tunnel is straightforward. First, create a CloudFlare Tunnel in the Zero Trust CloudFlare dashboard. CloudFlare should then provide instructions on how to install and run `cloudflared` for different environments and operating systems.
Once the connector is online, add a public hostname to the tunnel. You can map a root domain name, subdomains, and even subpaths. Then, specify the type and the local address for the service that you want to expose. For example, if you want to expose your backend server running on `localhost:8000` under the URL `https://api.acme.com`, do the following:
- Specify `api` as subdomain
- Specify `acme.com` as domain
- Specify the service type as `HTTP` (not `HTTPS`, as this is from the perspective of the connector)
- Specify the URL as `localhost:8000`
Save the settings, and voila! You have exposed your backend to the public Internet! How easy is that?
## Final Words
As always, feel free to shoot me an email or DM me on X if you have any questions about CloudFlare Tunnels.

View File

@@ -0,0 +1,23 @@
---
title: ask what, not why
description: stop questioning and start wondering
pubDate: Jul 30 2025
---
*Disclaimer: this is subjective, and may not apply to everyone*
One mindset shift I have been practicing is avoiding using "why" when asking questions, and instead ask "what". For example, in the context of software engineering, instead of asking, "why are you using framework *x* instead of *y*, ask "what led you to decide on using framework *x*?". This shift has caused me to question less and wonder more.
Firstly, it drives me away from putting blames. Asking "why" brings an undertone of questioning, whether intentional or not. The questionee, including myself, as a result of feeling questioned, may shift into defensive mode and subconsciously look for excuses first, rather than exploring something more productive. It may also make them feel invalidated or judge. As an example, consider the difference, as a listener, between "why are you sad?" and "what made you sad?" The former, subtly or otherwise, invalidates your feeling of sadness, while the latter demonstrates a level of rapport and understanding.
Secondly, asking "why" puts the subject under the spotlight, whereas asking "what" shifts the focus to the outside world, allowing both sides to view the situation at hand more objectively and holistically. Consider the two questions, "why did you do this?" and "what caused you to do this?" The first question focuses on the subjective intention of the person. Instead, the second question is looking for the external factors that effected their action. This is important if the situation at hand calls for an objective review, for example when dissecting architectural decisions in the context of engineering.
Here are more examples.
- "Why is the bus delayed again?" turns into "what caused the delay of the bus?"
- "Why can't we ship this feature by this Friday?" turns into "what are the blockers and challenges that prohibit us from shipping this feature by this Friday?"
- "Why am I feeling anxious" turns into "what is the source of my anxiety?"
- "Why do you like playing the piano?" turns into "what about piano makes you interested in it?"
This is a challenging practice that requires conscious effort. Every time I find myself using the word "why", I stop and think, "how can I rephrase this question so that I am not asking why?" As a result, however, I find that it not only fosters further discussions, but it also makes me less judgemental as a person.
If you do decide to give this a go, I would love to hear what you think about this, and how it shifts your mindset and perspective. Until then, thank you for reading, and see you in my next post.

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,23 @@
---
title: the pathological desire for validation
description: the experience of someone who struggles with self perception
pubDate: Jul 04 2025
---
I have a pathological crave for validation, be it in my career, or in my relationships and friendships. Unfortunately, I am still suffering from this disease, and I do not yet have the solution, but I believe I have found the trace to the problem. This is a self reflection for people who are interested in the experience of being a person who has tragically low to no self worth and are struggling to crawl out of the trench. Even more tragically is the fact that I had normalized this for most of my life until recently.
I was raised in an environment where emotions were strictly suppressed (man are not supposed to show emotions, remember!), and where love was transactional. I was conditioned from a young age that achievement was an expectation. I was taught that love and affections are manifested as efforts made to raise me, and I have this looming pressure that I need to pay it back when I grow older. I would only receive minimal affections when i had made an achievement, such as getting good grades or winning a competition. On the other hand, mistakes would bring upon severe punishments. On top of that, I was under the care of someones who lacked the ability to openly communicate and have conversations. Talking to them was like walking on eggshells. I had to be hyper attuned to their moods and be careful with my words so as to not trigger them. When I was in school, I had to hide my grades from my parents, because i knew they would not be nearly satisfactory. It would not be long, however, until the truth would drop, and then I would be met with visceral reactions and punishments.
In school, even though I would not consider myself to be bullied, I would very often be the butt of the joke, or be the target to be pranked. back then, I brushed it off as my classmates being playful, but I most definitely did not enjoy the experience. I was also strictly prohibited from developing romantic relationships with anyone, which further stunned my emotional development. The only thing I was decent at was programming, but no one at school took it seriously.
Fast forward into my early adulthood, I managed to get myself into several romantic situations online. Each further tramped on my self worth, and made me question whether I would ever, ever, be worthy of love and affection, despite all the efforts I had put in.
To this day, I still don't know what love and affection is, and thus how it feels to be the receiving end of it. My past experience had taught me that love is something I need to fight for. I need to work in order to receive affection. I need to put in effort to receive validations and approvals. If I don't receive them, it means that i am not working hard enough, and therefore not worthy and deserving of anything. In career, it means doubting my software engineering ability. In relationships and friendships, it means doubting myself as a person, constantly questioning my place in another person. Hanging out with friends brings me anxiety about how they view me; romantic interests even more so. When I bring this up with a person (which I only do if they are a very very significant person), I will receive my dose of validation, but the high will wear off shortly, like nicotine or alcohol, then I will need it again. Not only do I struggle to trust and internalize that validation, I constantly desire more of it to soothe myself.
The thirst for validation also affects how i view others. I perceive myself as inferior to all my peers and friends. They write code better. They work on cooler things. They have more friends. They have way cooler interests. They have broader perspective of the world. They are cooler and smarter. They (you if you are reading this) may reject this, but I struggle to view myself in any other way.
This is why it is deeply pathological - it is excessive, uncontrollable, and projects onto other people. It is extremely crippling, and it is unsustainable in the long term. I only recently started therapy, and I don't know how long or what it will take to pull myself out of this misery.
Before then, I want to express my deepest gratitude to the person who I admire most admire, for revealing this deep rooted issue of mine that I have set aside for the longest time, for making me self reflect more, and for giving the reassurances when I needed them.
Finally, if you have made it to here, I hope you find my experience helpful, and thank you for following along.

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,7 +10,17 @@ const blog = defineCollection({
pubDate: z.coerce.date(),
updatedDate: z.coerce.date().optional(),
heroImage: z.string().optional(),
useKatex: z.boolean().optional(),
}),
});
export const collections = { blog };
const read = defineCollection({
type: "content",
schema: z.object({
title: z.string(),
author: z.string(),
goodReadLink: z.string().optional(),
}),
});
export const collections = { blog, read };

View File

@@ -0,0 +1,6 @@
---
title: Being You
author: Anil Seth
---
I am still organizing my notes for this book.

View File

@@ -0,0 +1,34 @@
---
title: The Creative Act
author: Rick Rubin
goodReadLink: https://www.goodreads.com/book/show/60965426-the-creative-act
---
## Everyone is a creator
- To live as an artist is a way of being in the world
## Tuning in
- We are all participating in a larger creative act we are not conducting. We are being conducted. The artist is on a cosmic timetable, just like all of nature.
- In this great unfolding, ideas and thoughts, themes and songs and other works of art exist in the aether and ripen on schedule, ready to find expression in the physical world.
As artists, it is our job to draw down this information, transmute it, and share it.
- We are all antennae for creative thought.
- To pick up on signals, don't look for it, or try to predict and analyze our way into it. Instead, create an open space that allows it. A space so free of the normal overpacked condition of our minds that it functions as a vacuum.
- Practicing a way of being that allows you to see the world through uncorrupted, innocent eyes can free you to act in concert with the universe's timetable.
## The source of creativity
- The Source is out there. A wisdom surrounding us, an inexhaustible offering that is always available.
- Art is a circulation of energetic ideas. What makes them appear new is that they are combining differently each time they come back.
## Awareness
- The universe is only as large as our perception of it. When we cultivate our awareness, we are expanding the universe.
## The Vessal and the Filter
- The vessel holds the sum of our thoughts, feelings, dreams, and experiences in the world.
- Information is filtered uniquely for each person before entering the vessel.
- Artists seek to restore childlike perceptions, a more innocent state of wonder and appreciation untethered to utility or survival.
- One can think of the creative act as taking the sum of our vessel's contents as potential material, selecting for elements that seem useful or significant in the moment, and re-representing them.

View File

@@ -0,0 +1,7 @@
---
title: User Friendly
author: Cliff Kuang, Robert Fabricant
goodReadLink: https://www.goodreads.com/book/show/41940285-user-friendly
---
I will start making notes here when I start reading this book.

View File

@@ -0,0 +1,7 @@
---
title: Zero to One
author: Peter Thiel, Blake Masters
goodReadLink: https://www.goodreads.com/book/show/18050143-zero-to-one
---
I will start making notes here when I start reading this book.

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">
<html lang="en" class="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-ctp-base text-ctp-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

@@ -0,0 +1,58 @@
---
import type { CollectionEntry } from "astro:content";
import BaseHead from "../components/BaseHead.astro";
type Props = CollectionEntry<"read">["data"];
const { title, author, goodReadLink } = Astro.props;
---
<!doctype html>
<html lang="en">
<head>
<BaseHead
title={`Kenneth's Read - ${title}`}
description={`Notes I made for the book ${title}`}
/>
</head>
<body
class="w-full reads -z-2 bg-stone-200 dark:bg-stone-900 flex justify-center dark:text-stone-300 pt-20 md:py-40"
>
<header
class="fixed scroll-to-blur w-full -z-1 top-0 left-0 right-0 flex justify-center"
>
<div
class="w-full max-w-[calc(65ch+var(--spacing)*24)] px-8 md:px-12 py-4 md:py-12 opacity-80"
>
<h1 class="font-bold text-sm">Kenneth's Read</h1>
<h2 class="text-sm">Quotes and notes from my readings.</h2>
</div>
</header>
<main
class="relative w-full max-w-[calc(65ch+var(--spacing)*24)] px-8 md:px-12 py-10 rounded-t-2xl md:rounded-2xl shadow-lg border border-stone-300 dark:border-stone-600 bg-stone-100 dark:bg-stone-800 min-h-[calc(100vh-var(--spacing)*28)]"
>
<article>
<div class="flex justify-between items-center w-full mb-10">
<header class="flex flex-col">
<h1 class="font-bold">{title}</h1>
<h2 class="font-medium">{author}</h2>
</header>
<a href={goodReadLink} class="underline">GoodRead</a>
</div>
<div
class="prose dark:prose-invert prose-headings:font-medium prose-headings:tracking-tight prose-h2:mb-0.5 dark:prose-headings:text-stone-300 dark:text-stone-300 dark:marker:text-stone-300 prose-ul:list-none prose-ul:p-0 prose-li:p-0 prose-li:before:content-['•'] prose-li:relative prose-li:before:absolute prose-li:before:left-0 prose-li:before:-translate-x-4"
>
<slot />
</div>
</article>
<div
class="absolute left-6 top-0 -translate-y-1/2 transition-all rounded-lg bg-stone-100 dark:bg-stone-800 border border-stone-300 dark:border-stone-700 shadow-lg h-min"
>
<a
class="block px-2 py-0.5 text-sm opacity-80 transition-all"
href="/read">&lt;- All books</a
>
</div>
</main>
</body>
</html>

View File

@@ -1,15 +1,15 @@
---
import { type CollectionEntry, getCollection } from 'astro:content';
import BlogPost from '../../layouts/BlogPost.astro';
import { type CollectionEntry, getCollection } from "astro:content";
import BlogPost from "../../layouts/BlogPost.astro";
export async function getStaticPaths() {
const posts = await getCollection('blog');
const posts = await getCollection("blog");
return posts.map((post) => ({
params: { slug: post.slug },
props: post,
}));
}
type Props = CollectionEntry<'blog'>;
type Props = CollectionEntry<"blog">;
const post = Astro.props;
const { Content } = await post.render();

View File

@@ -10,25 +10,26 @@ 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>
<html lang="en" class="latte dark:mocha bg-base">
<html lang="en" class="dark:mocha bg-ctp-base">
<head>
<BaseHead title={SITE_TITLE} description={SITE_DESCRIPTION} />
</head>
<body
class="tilde-background text-text h-screen m-auto sm:flex items-center justify-center sm:overflow-hidden"
class="tilde-background text-ctp-text h-screen m-auto sm:flex items-center justify-center sm:overflow-hidden"
>
<div class="visible p-8 sm:hidden">
<header>
<p class="font-bold text-2xl">kennethnym</p>
</header>
<main class="py-8">
<p>dumping ground for my thoughts. all opinions are my own.</p>
<p>
software engineer @ <Link href="https://www.gitpod.io">gitpod</Link>.
all opinions are my own.
</p>
<p>check out <Link href="/read">the books I am reading!</Link></p>
<h1 class="font-bold mt-8 mb-2 text-lg visited">current projects:</h1>
<ul class="not-prose space-y-4 md:space-y-2">
<li>
@@ -36,9 +37,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,13 +70,16 @@ 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 class="text-center">
software engineer @ <Link href="https://www.gitpod.io">gitpod</Link>.
unpaid hhkb salesman.
</p>
<p>&nbsp;</p>
<p>&nbsp;</p>
<Link href="/read" class="w-full">books i am reading</Link>
<p>&nbsp;</p>
<ul
aria-label="recent blog posts"
class="w-full space-y-2 sm:space-y-0"

94
src/pages/read.astro Normal file
View File

@@ -0,0 +1,94 @@
---
import { getCollection } from "astro:content";
import BaseHead from "../components/BaseHead.astro";
const nowReading = {
title: "The Creative Act",
slug: "the-creative-act",
};
const quote = {
content:
"Practicing a way of being that allows you to see the world through uncorrupted, innocent eyes can free you to act in concert with the universe's timetable.",
bookTitle: "The Creative Act",
};
const books = (await getCollection("read")).filter(
(book) => book.slug !== nowReading.slug,
);
---
<!doctype html>
<html
lang="en"
class="bg-stone-200 dark:bg-stone-900 text-stone-700 dark:text-stone-400"
>
<head>
<BaseHead
title="Kenneth's Reads"
description="Quotes and notes from my readings."
/>
</head>
<body class="reads px-8">
<main class="mt-8 mb-20 md:mt-0 max-w-4xl grid grid-cols-11">
<header class="col-span-11 grid grid-cols-11">
<h1
class="col-span-10 md:col-span-3 md:justify-self-end text-lg font-bold mt-8"
>
<a href="/" class="hover:underline">Kenneth</a>&#39;s Reads
</h1>
<p
class="col-span-10 md:col-span-8 text-lg font-medium mb-10 md:mb-20 md:ml-12 md:mt-8"
>
Quotes and notes from my readings.
</p>
</header>
<div class="md:justify-self-end col-span-10 md:col-span-3">
<div class="flex items-center opacity-70 mb-2 md:mb-20">
<div
class="w-3 h-3 bg-green-500 border-green-200 dark:bg-green-500 border-2 dark:border-green-300 rounded-full animate-pulse mr-2"
>
</div>
<h2 class="tracking-tight">Now Reading</h2>
</div>
</div>
<div class="col-span-10 md:col-span-8 md:ml-12 mb-16 md:mb-20">
<a class="font-medium underline" href={`/read/${nowReading.slug}`}
>{nowReading.title}</a
>
</div>
<div
class="md:justify-self-end col-span-10 md:col-span-3 flex flex-col md:items-end opacity-70"
>
<p class="text-6xl leading-none font-black select-none">“</p>
<h2 class="leading-none -translate-y-8 tracking-tight">
{quote.bookTitle}
</h2>
</div>
<p
class="col-span-10 md:col-span-8 max-w-prose md:ml-12 text-2xl md:text-4xl tracking-tight mb-20"
>
{quote.content}
</p>
<h2
class="col-span-10 md:col-span-3 tracking-tight md:justify-self-end mb-4"
>
All Books
</h2>
<ul class="col-span-10 md:col-span-8 md:ml-12 space-y-2">
{
books.map((book) => (
<li>
<a class="underline" href={`/read/${book.slug}`}>
{book.data.title}
</a>
</li>
))
}
</ul>
</main>
</body>
</html>

View File

@@ -0,0 +1,20 @@
---
import { type CollectionEntry, getCollection } from "astro:content";
import BookRead from "../../layouts/BookRead.astro";
export async function getStaticPaths() {
const posts = await getCollection("read");
return posts.map((post) => ({
params: { slug: post.slug },
props: post,
}));
}
type Props = CollectionEntry<"read">;
const post = Astro.props;
const { Content } = await post.render();
---
<BookRead {...post.data}>
<Content />
</BookRead>

View File

@@ -1,3 +1,11 @@
@import "tailwindcss";
@import "@catppuccin/tailwindcss/mocha.css";
@import url("https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap");
@plugin "@tailwindcss/typography";
@source inline("text-ctp-red");
@font-face {
font-family: "CommitMono";
src: url("/fonts/CommitMono-400-Regular.otf") format("opentype");
@@ -33,11 +41,78 @@
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;
}
body.reads * {
font-family: "Inter", sans-serif;
}
.nf {
font-family: "NerdFont";
}
.scroll-to-blur {
animation-name: scroll-to-blur;
animation-duration: 1ms; /* Firefox requires this to apply the animation */
animation-timeline: scroll(block nearest);
}
@keyframes scroll-to-blur {
0% {
filter: blur(0px);
}
20% {
filter: blur(5px);
transform: scale(0.99);
opacity: 0%;
}
100% {
filter: blur(5px);
transform: scale(0.99);
opacity: 0%;
}
}