Compare commits

44 Commits

Author SHA1 Message Date
graham 2b8cea8e50 open external links in new tab 2026-05-05 18:08:15 -04:00
graham 0899304a62 /uses page 2026-05-05 17:38:22 -04:00
graham 562aa21e57 new post 2026-05-02 21:53:46 -04:00
graham 7d76d2bb03 astro 6 2026-04-30 11:34:09 -04:00
graham 9ac6efc55a update /now 2026-04-28 14:41:52 -04:00
graham 1dbc1b16dd accessibility improvements 2026-04-21 15:35:24 -04:00
graham a2ba126ef8 fix missing word 2026-04-14 20:28:38 -04:00
graham 927f43a2b2 new post 2026-04-14 19:36:06 -04:00
graham a62a8f7356 update /now 2026-04-11 15:56:26 -04:00
graham 2ba98a6965 update /now 2026-03-29 11:15:45 -04:00
graham f8d3fa4005 update /projects and /now 2026-03-11 16:16:44 -04:00
graham 5c116f16b7 “share on mastodon” link 2026-03-04 08:23:29 -05:00
graham 3dfa53a8af new blog post + update /now 2026-02-21 22:32:03 -05:00
graham 73f73f4646 new post - my gaming journal 2026-02-03 22:04:09 -05:00
graham d663087cd6 update /now 2026-02-02 13:31:18 -05:00
graham a7441d5968 clean up 2026-01-27 19:11:06 -05:00
graham 294b856573 new post - revisiting nova 2026-01-27 14:59:27 -05:00
graham c6d2195041 update /now 2026-01-17 19:31:34 -05:00
graham a8c1df6cea remove github actions again 😵‍💫 2026-01-17 19:10:02 -05:00
graham 1afefe1301 build before deploying to sevalla 2026-01-17 18:55:16 -05:00
graham 4458db0cdd version bump 2026-01-17 18:25:38 -05:00
graham 02cc46e203 sevalla deploy workflow 2026-01-17 13:07:34 -05:00
graham b902c6522d upgrade to astro v5.16.11 2026-01-17 11:57:31 -05:00
graham d3fc322075 new blog post - notes on last.fm 2026-01-17 11:53:11 -05:00
graham 7b249aeab1 update /now 2026-01-14 16:13:45 -05:00
graham 0b2ae40d56 remove mastodon post card (for now) 2026-01-10 17:08:58 -05:00
graham 328262604b new post - setting my music free 2026-01-03 22:18:32 -05:00
graham 66d1cdb9db new post - favorite things of 2025 2026-01-02 12:08:40 -05:00
graham 76cbcc7297 update /now 2025-12-30 19:24:57 -05:00
graham 187945fd0a fix publish date 2025-12-20 13:57:36 -05:00
graham 063c0fe5d9 oops forgot to save changes 2025-12-20 13:55:55 -05:00
graham 19839f3812 new post + update /now 2025-12-20 12:19:01 -05:00
graham 924b0a4491 new post 2025-12-18 19:34:10 -05:00
graham fdbcc8306f lint config fixes 2025-12-18 19:33:36 -05:00
graham 6a235f56ae update post 2025-12-15 17:08:12 -05:00
graham 3e6cb85b0a update now page 2025-12-07 14:19:14 -05:00
graham edc5c8e8c4 new post 2025-12-04 18:36:07 -05:00
graham a3598149fe pnpm 2025-12-04 12:22:58 -05:00
Graham Hall a931b2950a Merge pull request #21 from ghall89/more-sass 2025-12-04 11:49:15 -05:00
graham d58a17c801 new post 2025-12-02 18:42:55 -05:00
graham fc2ace903b trying a new /now format 2025-12-01 20:33:54 -05:00
graham 0e620696b4 fix typo 2025-12-01 13:14:18 -05:00
graham d5b758aa96 added some context 2025-12-01 11:59:50 -05:00
graham fb8fc5e697 new post 2025-12-01 11:53:10 -05:00
35 changed files with 5959 additions and 3984 deletions
+2 -8
View File
@@ -1,7 +1,5 @@
{ {
"$ref": "#/definitions/blog", "$schema": "https://json-schema.org/draft/2020-12/schema",
"definitions": {
"blog": {
"type": "object", "type": "object",
"properties": { "properties": {
"title": { "title": {
@@ -24,9 +22,5 @@
"title", "title",
"pubDate", "pubDate",
"tags" "tags"
], ]
"additionalProperties": false
}
},
"$schema": "http://json-schema.org/draft-07/schema#"
} }
+7 -6
View File
@@ -1,10 +1,11 @@
import __ASTRO_IMAGE_IMPORT_1G57ng from "src/assets/blog/ileopard/mac-os-10-1.png?astroContentImageFlag=&importer=src%2Fcontent%2Fblog%2F2022%2Fileopard-a-retrospective.md"; import __ASTRO_IMAGE_IMPORT_1mhYTO from "src/assets/blog/gifs/destroy-taskmaster.gif?astroContentImageFlag=&importer=src%2Fcontent%2Fblog%2F2026%2Fmy-favorite-things-of-2025.md";
import __ASTRO_IMAGE_IMPORT_Z1ESWoO from "src/assets/blog/ileopard/itunes-7.gif?astroContentImageFlag=&importer=src%2Fcontent%2Fblog%2F2022%2Fileopard-a-retrospective.md";
import __ASTRO_IMAGE_IMPORT_rrnp from "src/assets/blog/ileopard/ileopard-2-0-1.png?astroContentImageFlag=&importer=src%2Fcontent%2Fblog%2F2022%2Fileopard-a-retrospective.md";
import __ASTRO_IMAGE_IMPORT_3KcDr from "src/assets/blog/my-ai-portrait.jpg?astroContentImageFlag=&importer=src%2Fcontent%2Fblog%2F2022%2Fcreating-a-dating-profile-with-ai.md";
import __ASTRO_IMAGE_IMPORT_Zi2DqH from "src/assets/blog/gunpla/box.jpg?astroContentImageFlag=&importer=src%2Fcontent%2Fblog%2F2023%2Fmy-gunpla-adventure.md";
import __ASTRO_IMAGE_IMPORT_1OkzEl from "src/assets/blog/gunpla/all-the-parts.jpg?astroContentImageFlag=&importer=src%2Fcontent%2Fblog%2F2023%2Fmy-gunpla-adventure.md"; import __ASTRO_IMAGE_IMPORT_1OkzEl from "src/assets/blog/gunpla/all-the-parts.jpg?astroContentImageFlag=&importer=src%2Fcontent%2Fblog%2F2023%2Fmy-gunpla-adventure.md";
import __ASTRO_IMAGE_IMPORT_Zi2DqH from "src/assets/blog/gunpla/box.jpg?astroContentImageFlag=&importer=src%2Fcontent%2Fblog%2F2023%2Fmy-gunpla-adventure.md";
import __ASTRO_IMAGE_IMPORT_FYQiW from "src/assets/blog/gunpla/final.jpg?astroContentImageFlag=&importer=src%2Fcontent%2Fblog%2F2023%2Fmy-gunpla-adventure.md"; import __ASTRO_IMAGE_IMPORT_FYQiW from "src/assets/blog/gunpla/final.jpg?astroContentImageFlag=&importer=src%2Fcontent%2Fblog%2F2023%2Fmy-gunpla-adventure.md";
export default new Map([["src/assets/blog/ileopard/mac-os-10-1.png?astroContentImageFlag=&importer=src%2Fcontent%2Fblog%2F2022%2Fileopard-a-retrospective.md", __ASTRO_IMAGE_IMPORT_1G57ng], ["src/assets/blog/ileopard/itunes-7.gif?astroContentImageFlag=&importer=src%2Fcontent%2Fblog%2F2022%2Fileopard-a-retrospective.md", __ASTRO_IMAGE_IMPORT_Z1ESWoO], ["src/assets/blog/ileopard/ileopard-2-0-1.png?astroContentImageFlag=&importer=src%2Fcontent%2Fblog%2F2022%2Fileopard-a-retrospective.md", __ASTRO_IMAGE_IMPORT_rrnp], ["src/assets/blog/my-ai-portrait.jpg?astroContentImageFlag=&importer=src%2Fcontent%2Fblog%2F2022%2Fcreating-a-dating-profile-with-ai.md", __ASTRO_IMAGE_IMPORT_3KcDr], ["src/assets/blog/gunpla/box.jpg?astroContentImageFlag=&importer=src%2Fcontent%2Fblog%2F2023%2Fmy-gunpla-adventure.md", __ASTRO_IMAGE_IMPORT_Zi2DqH], ["src/assets/blog/gunpla/all-the-parts.jpg?astroContentImageFlag=&importer=src%2Fcontent%2Fblog%2F2023%2Fmy-gunpla-adventure.md", __ASTRO_IMAGE_IMPORT_1OkzEl], ["src/assets/blog/gunpla/final.jpg?astroContentImageFlag=&importer=src%2Fcontent%2Fblog%2F2023%2Fmy-gunpla-adventure.md", __ASTRO_IMAGE_IMPORT_FYQiW]]); import __ASTRO_IMAGE_IMPORT_rrnp from "src/assets/blog/ileopard/ileopard-2-0-1.png?astroContentImageFlag=&importer=src%2Fcontent%2Fblog%2F2022%2Fileopard-a-retrospective.md";
import __ASTRO_IMAGE_IMPORT_Z1ESWoO from "src/assets/blog/ileopard/itunes-7.gif?astroContentImageFlag=&importer=src%2Fcontent%2Fblog%2F2022%2Fileopard-a-retrospective.md";
import __ASTRO_IMAGE_IMPORT_1G57ng from "src/assets/blog/ileopard/mac-os-10-1.png?astroContentImageFlag=&importer=src%2Fcontent%2Fblog%2F2022%2Fileopard-a-retrospective.md";
import __ASTRO_IMAGE_IMPORT_3KcDr from "src/assets/blog/my-ai-portrait.jpg?astroContentImageFlag=&importer=src%2Fcontent%2Fblog%2F2022%2Fcreating-a-dating-profile-with-ai.md";
export default new Map([["src/assets/blog/gifs/destroy-taskmaster.gif?astroContentImageFlag=&importer=src%2Fcontent%2Fblog%2F2026%2Fmy-favorite-things-of-2025.md", __ASTRO_IMAGE_IMPORT_1mhYTO], ["src/assets/blog/gunpla/all-the-parts.jpg?astroContentImageFlag=&importer=src%2Fcontent%2Fblog%2F2023%2Fmy-gunpla-adventure.md", __ASTRO_IMAGE_IMPORT_1OkzEl], ["src/assets/blog/gunpla/box.jpg?astroContentImageFlag=&importer=src%2Fcontent%2Fblog%2F2023%2Fmy-gunpla-adventure.md", __ASTRO_IMAGE_IMPORT_Zi2DqH], ["src/assets/blog/gunpla/final.jpg?astroContentImageFlag=&importer=src%2Fcontent%2Fblog%2F2023%2Fmy-gunpla-adventure.md", __ASTRO_IMAGE_IMPORT_FYQiW], ["src/assets/blog/ileopard/ileopard-2-0-1.png?astroContentImageFlag=&importer=src%2Fcontent%2Fblog%2F2022%2Fileopard-a-retrospective.md", __ASTRO_IMAGE_IMPORT_rrnp], ["src/assets/blog/ileopard/itunes-7.gif?astroContentImageFlag=&importer=src%2Fcontent%2Fblog%2F2022%2Fileopard-a-retrospective.md", __ASTRO_IMAGE_IMPORT_Z1ESWoO], ["src/assets/blog/ileopard/mac-os-10-1.png?astroContentImageFlag=&importer=src%2Fcontent%2Fblog%2F2022%2Fileopard-a-retrospective.md", __ASTRO_IMAGE_IMPORT_1G57ng], ["src/assets/blog/my-ai-portrait.jpg?astroContentImageFlag=&importer=src%2Fcontent%2Fblog%2F2022%2Fcreating-a-dating-profile-with-ai.md", __ASTRO_IMAGE_IMPORT_3KcDr]]);
+35 -80
View File
@@ -1,7 +1,7 @@
declare module 'astro:content' { declare module 'astro:content' {
interface Render { interface Render {
'.mdx': Promise<{ '.mdx': Promise<{
Content: import('astro').MarkdownInstance<{}>['Content']; Content: import('astro').MDXContent;
headings: import('astro').MarkdownHeading[]; headings: import('astro').MarkdownHeading[];
remarkPluginFrontmatter: Record<string, any>; remarkPluginFrontmatter: Record<string, any>;
components: import('astro').MDXInstance<{}>['components']; components: import('astro').MDXInstance<{}>['components'];
@@ -26,21 +26,13 @@ declare module 'astro:content' {
[key: string]: unknown; [key: string]: unknown;
}; };
} }
}
declare module 'astro:content' {
type Flatten<T> = T extends { [K: string]: infer U } ? U : never; type Flatten<T> = T extends { [K: string]: infer U } ? U : never;
export type CollectionKey = keyof AnyEntryMap; export type CollectionKey = keyof DataEntryMap;
export type CollectionEntry<C extends CollectionKey> = Flatten<AnyEntryMap[C]>; export type CollectionEntry<C extends CollectionKey> = Flatten<DataEntryMap[C]>;
export type ContentCollectionKey = keyof ContentEntryMap;
export type DataCollectionKey = keyof DataEntryMap;
type AllValuesOf<T> = T extends any ? T[keyof T] : never; type AllValuesOf<T> = T extends any ? T[keyof T] : never;
type ValidContentEntrySlug<C extends keyof ContentEntryMap> = AllValuesOf<
ContentEntryMap[C]
>['slug'];
export type ReferenceDataEntry< export type ReferenceDataEntry<
C extends CollectionKey, C extends CollectionKey,
@@ -49,41 +41,17 @@ declare module 'astro:content' {
collection: C; collection: C;
id: E; id: E;
}; };
export type ReferenceContentEntry<
C extends keyof ContentEntryMap,
E extends ValidContentEntrySlug<C> | (string & {}) = string,
> = {
collection: C;
slug: E;
};
export type ReferenceLiveEntry<C extends keyof LiveContentConfig['collections']> = { export type ReferenceLiveEntry<C extends keyof LiveContentConfig['collections']> = {
collection: C; collection: C;
id: string; id: string;
}; };
/** @deprecated Use `getEntry` instead. */ export function getCollection<C extends keyof DataEntryMap, E extends CollectionEntry<C>>(
export function getEntryBySlug<
C extends keyof ContentEntryMap,
E extends ValidContentEntrySlug<C> | (string & {}),
>(
collection: C,
// Note that this has to accept a regular string too, for SSR
entrySlug: E,
): E extends ValidContentEntrySlug<C>
? Promise<CollectionEntry<C>>
: Promise<CollectionEntry<C> | undefined>;
/** @deprecated Use `getEntry` instead. */
export function getDataEntryById<C extends keyof DataEntryMap, E extends keyof DataEntryMap[C]>(
collection: C,
entryId: E,
): Promise<CollectionEntry<C>>;
export function getCollection<C extends keyof AnyEntryMap, E extends CollectionEntry<C>>(
collection: C, collection: C,
filter?: (entry: CollectionEntry<C>) => entry is E, filter?: (entry: CollectionEntry<C>) => entry is E,
): Promise<E[]>; ): Promise<E[]>;
export function getCollection<C extends keyof AnyEntryMap>( export function getCollection<C extends keyof DataEntryMap>(
collection: C, collection: C,
filter?: (entry: CollectionEntry<C>) => unknown, filter?: (entry: CollectionEntry<C>) => unknown,
): Promise<CollectionEntry<C>[]>; ): Promise<CollectionEntry<C>[]>;
@@ -95,14 +63,6 @@ declare module 'astro:content' {
import('astro').LiveDataCollectionResult<LiveLoaderDataType<C>, LiveLoaderErrorType<C>> import('astro').LiveDataCollectionResult<LiveLoaderDataType<C>, LiveLoaderErrorType<C>>
>; >;
export function getEntry<
C extends keyof ContentEntryMap,
E extends ValidContentEntrySlug<C> | (string & {}),
>(
entry: ReferenceContentEntry<C, E>,
): E extends ValidContentEntrySlug<C>
? Promise<CollectionEntry<C>>
: Promise<CollectionEntry<C> | undefined>;
export function getEntry< export function getEntry<
C extends keyof DataEntryMap, C extends keyof DataEntryMap,
E extends keyof DataEntryMap[C] | (string & {}), E extends keyof DataEntryMap[C] | (string & {}),
@@ -111,15 +71,6 @@ declare module 'astro:content' {
): E extends keyof DataEntryMap[C] ): E extends keyof DataEntryMap[C]
? Promise<DataEntryMap[C][E]> ? Promise<DataEntryMap[C][E]>
: Promise<CollectionEntry<C> | undefined>; : Promise<CollectionEntry<C> | undefined>;
export function getEntry<
C extends keyof ContentEntryMap,
E extends ValidContentEntrySlug<C> | (string & {}),
>(
collection: C,
slug: E,
): E extends ValidContentEntrySlug<C>
? Promise<CollectionEntry<C>>
: Promise<CollectionEntry<C> | undefined>;
export function getEntry< export function getEntry<
C extends keyof DataEntryMap, C extends keyof DataEntryMap,
E extends keyof DataEntryMap[C] | (string & {}), E extends keyof DataEntryMap[C] | (string & {}),
@@ -137,40 +88,47 @@ declare module 'astro:content' {
): Promise<import('astro').LiveDataEntryResult<LiveLoaderDataType<C>, LiveLoaderErrorType<C>>>; ): Promise<import('astro').LiveDataEntryResult<LiveLoaderDataType<C>, LiveLoaderErrorType<C>>>;
/** Resolve an array of entry references from the same collection */ /** Resolve an array of entry references from the same collection */
export function getEntries<C extends keyof ContentEntryMap>(
entries: ReferenceContentEntry<C, ValidContentEntrySlug<C>>[],
): Promise<CollectionEntry<C>[]>;
export function getEntries<C extends keyof DataEntryMap>( export function getEntries<C extends keyof DataEntryMap>(
entries: ReferenceDataEntry<C, keyof DataEntryMap[C]>[], entries: ReferenceDataEntry<C, keyof DataEntryMap[C]>[],
): Promise<CollectionEntry<C>[]>; ): Promise<CollectionEntry<C>[]>;
export function render<C extends keyof AnyEntryMap>( export function render<C extends keyof DataEntryMap>(
entry: AnyEntryMap[C][string], entry: DataEntryMap[C][string],
): Promise<RenderResult>; ): Promise<RenderResult>;
export function reference<C extends keyof AnyEntryMap>( export function reference<
collection: C, C extends
): import('astro/zod').ZodEffects< | keyof DataEntryMap
import('astro/zod').ZodString,
C extends keyof ContentEntryMap
? ReferenceContentEntry<C, ValidContentEntrySlug<C>>
: ReferenceDataEntry<C, keyof DataEntryMap[C]>
>;
// Allow generic `string` to avoid excessive type errors in the config // Allow generic `string` to avoid excessive type errors in the config
// if `dev` is not running to update as you edit. // if `dev` is not running to update as you edit.
// Invalid collection names will be caught at build time. // Invalid collection names will be caught at build time.
export function reference<C extends string>( | (string & {}),
>(
collection: C, collection: C,
): import('astro/zod').ZodEffects<import('astro/zod').ZodString, never>; ): import('astro/zod').ZodPipe<
import('astro/zod').ZodString,
type ReturnTypeOrOriginal<T> = T extends (...args: any[]) => infer R ? R : T; import('astro/zod').ZodTransform<
type InferEntrySchema<C extends keyof AnyEntryMap> = import('astro/zod').infer< C extends keyof DataEntryMap
ReturnTypeOrOriginal<Required<ContentConfig['collections'][C]>['schema']> ? {
collection: C;
id: string;
}
: never,
string
>
>; >;
type ContentEntryMap = { type ReturnTypeOrOriginal<T> = T extends (...args: any[]) => infer R ? R : T;
type InferEntrySchema<C extends keyof DataEntryMap> = import('astro/zod').infer<
}; ReturnTypeOrOriginal<Required<ContentConfig['collections'][C]>['schema']>
>;
type ExtractLoaderConfig<T> = T extends { loader: infer L } ? L : never;
type InferLoaderSchema<
C extends keyof DataEntryMap,
L = ExtractLoaderConfig<ContentConfig['collections'][C]>,
> = L extends { schema: import('astro/zod').ZodSchema }
? import('astro/zod').infer<L['schema']>
: any;
type DataEntryMap = { type DataEntryMap = {
"blog": Record<string, { "blog": Record<string, {
@@ -184,8 +142,6 @@ declare module 'astro:content' {
}; };
type AnyEntryMap = ContentEntryMap & DataEntryMap;
type ExtractLoaderTypes<T> = T extends import('astro/loaders').LiveLoader< type ExtractLoaderTypes<T> = T extends import('astro/loaders').LiveLoader<
infer TData, infer TData,
infer TEntryFilter, infer TEntryFilter,
@@ -194,7 +150,6 @@ declare module 'astro:content' {
> >
? { data: TData; entryFilter: TEntryFilter; collectionFilter: TCollectionFilter; error: TError } ? { data: TData; entryFilter: TEntryFilter; collectionFilter: TCollectionFilter; error: TError }
: { data: never; entryFilter: never; collectionFilter: never; error: never }; : { data: never; entryFilter: never; collectionFilter: never; error: never };
type ExtractDataType<T> = ExtractLoaderTypes<T>['data'];
type ExtractEntryFilterType<T> = ExtractLoaderTypes<T>['entryFilter']; type ExtractEntryFilterType<T> = ExtractLoaderTypes<T>['entryFilter'];
type ExtractCollectionFilterType<T> = ExtractLoaderTypes<T>['collectionFilter']; type ExtractCollectionFilterType<T> = ExtractLoaderTypes<T>['collectionFilter'];
type ExtractErrorType<T> = ExtractLoaderTypes<T>['error']; type ExtractErrorType<T> = ExtractLoaderTypes<T>['error'];
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -1,5 +1,5 @@
{ {
"_variables": { "_variables": {
"lastUpdateCheck": 1763053152837 "lastUpdateCheck": 1778018797551
} }
} }
+4
View File
@@ -4,6 +4,7 @@ import alpinejs from '@astrojs/alpinejs';
import mdx from '@astrojs/mdx'; import mdx from '@astrojs/mdx';
import { defineConfig } from 'astro/config'; import { defineConfig } from 'astro/config';
import pagefind from 'astro-pagefind'; import pagefind from 'astro-pagefind';
import rehypeExternalLinks from 'rehype-external-links';
// https://astro.build/config // https://astro.build/config
export default defineConfig({ export default defineConfig({
@@ -16,6 +17,9 @@ export default defineConfig({
dark: 'github-dark', dark: 'github-dark',
}, },
}, },
rehypePlugins: [
[rehypeExternalLinks, { target: '_blank', rel: ['noopener', 'noreferrer'] }],
],
}, },
integrations: [ integrations: [
mdx(), mdx(),
Generated
-3831
View File
File diff suppressed because it is too large Load Diff
+7 -5
View File
@@ -15,22 +15,24 @@
}, },
"dependencies": { "dependencies": {
"@alpinejs/persist": "^3.15.1", "@alpinejs/persist": "^3.15.1",
"@astrojs/alpinejs": "^0.4.9", "@astrojs/alpinejs": "^0.5.0",
"@astrojs/mdx": "^4.3.7", "@astrojs/mdx": "^5.0.4",
"@astrojs/rss": "^4.0.12", "@astrojs/rss": "^4.0.18",
"@types/alpinejs": "^3.13.11", "@types/alpinejs": "^3.13.11",
"@types/markdown-it": "^14.1.2", "@types/markdown-it": "^14.1.2",
"alpinejs": "^3.15.0", "alpinejs": "^3.15.0",
"astro": "^5.14.4", "astro": "^6.2.1",
"astro-pagefind": "^1.8.5", "astro-pagefind": "^1.8.5",
"date-fns": "^4.1.0", "date-fns": "^4.1.0",
"markdown-it": "^14.1.0", "markdown-it": "^14.1.0",
"pagefind": "^1.4.0", "pagefind": "^1.4.0",
"rehype-external-links": "^3.0.0",
"sanitize-html": "^2.17.0", "sanitize-html": "^2.17.0",
"sharp": "^0.34.4" "sharp": "^0.34.4"
}, },
"devDependencies": { "devDependencies": {
"@astrojs/ts-plugin": "^1.10.4", "@astrojs/ts-plugin": "1.10.7",
"@eslint/js": "^9.39.2",
"@types/alpinejs__persist": "^3.13.4", "@types/alpinejs__persist": "^3.13.4",
"bun-types": "1.3.0", "bun-types": "1.3.0",
"eslint": "^9.39.1", "eslint": "^9.39.1",
+5264
View File
File diff suppressed because it is too large Load Diff
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

+4
View File
@@ -83,6 +83,10 @@ import { navLinks } from '../../data/nav-links';
flex-direction: column; flex-direction: column;
gap: 12px; gap: 12px;
font-size: 1.2rem; font-size: 1.2rem;
& a {
text-decoration: none;
}
} }
.hidden-drawer { .hidden-drawer {
+1 -1
View File
@@ -49,10 +49,10 @@ import Nav from './Nav.astro';
font-size: 1.6rem; font-size: 1.6rem;
color: var(--text); color: var(--text);
font-weight: bold; font-weight: bold;
text-decoration: none;
&:hover { &:hover {
opacity: 0.75; opacity: 0.75;
text-decoration: none;
} }
} }
</style> </style>
+1 -1
View File
@@ -29,10 +29,10 @@ import { navLinks } from '../../data/nav-links';
& a { & a {
color: var(--text); color: var(--text);
padding: 5px 10px; padding: 5px 10px;
text-decoration: none;
&:hover { &:hover {
opacity: 0.75; opacity: 0.75;
text-decoration: none;
} }
} }
} }
+83
View File
@@ -0,0 +1,83 @@
---
import { navLinks } from '../../data/nav-links';
---
<aside>
<div class="logo">
<a href="/">ghall.space</a>
</div>
<nav>
<ul>
{
navLinks.map((link) => (
<li>
<a href={`/${link.path}`}>{link.label}</a>
</li>
))
}
</ul>
</nav>
</aside>
<style lang="scss">
@use '../../styles/variables.scss' as *;
aside {
display: none;
}
.logo {
padding: 16px;
border-bottom: var(--border);
a {
font-size: 1.6rem;
color: var(--text);
font-weight: bold;
text-decoration: none;
&:hover {
opacity: 0.75;
}
}
}
nav {
ul {
display: flex;
flex-direction: column;
list-style: none;
gap: 8px;
padding: 0;
margin: 0;
}
a {
color: var(--text);
padding: 8px 16px;
text-decoration: none;
font-weight: 600;
font-size: 1.1rem;
border-radius: $radius;
&:hover {
background-color: rgba(0, 0, 0, 0.05);
}
}
}
@media screen and (min-width: $max-mobile-width + 1) {
aside {
display: block;
position: fixed;
left: 0;
top: 0;
width: 220px;
height: 100vh;
background-color: var(--background);
border-right: var(--border);
z-index: 1;
overflow-y: auto;
}
}
</style>
@@ -0,0 +1,56 @@
---
title: 'Building a Mac App Without Xcode - Part 1'
pubDate: '12/1/25'
tags: ['Making Stuff', 'MacOS', 'Programming']
---
While I enjoy developing Mac apps, I find Xcode to be exceptionally clunky, especially when compared to the tools I use for web development. I thought it would be nice to be able to write and compile a fully-functional Mac application using any text editor, and without having to open Xcode.
So, I decided to try migrating my app, [AutoDock](https://github.com/ghall89/AutoDock), to this Xcode-free build process, and actually release it as the next update. I have a few requirements for this to be viable:
1. Whichever editor I use, I need access to the Swift language server.
2. The build process should produce a functional MacOS application bundle as a universal binary (for both Intel and ARM CPUs).
3. I should be able to use regular security features available to apps built with Xcode, like sandboxing, hardened runtime, etc.
4. The final binary should be signed with my developer certificate.
To prevent this blog post from being far too long, Ive decided to split it up into multiple parts. This first part will focus on LSP support, and building the application bundle.
## Setting Up the Swift Language Server
Being the Mac nerd that I am, I felt it would be uncouth to not even try using BBEdit for writing and editing the actual Swift code. Thankfully, BBEdit has some pretty nice LSP support built in, so it was simply a matter of creating a symlink to the Swift language server in BBEdits Language Servers directory:
`ln -s -f /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/sourcekit-lsp ~/Library/Application\ Support/BBEdit/Language\ Servers/sourcekit-lsp`
~~However, I found it to be a tad slow, so~~[^1] I opted for the lazy route and just installed [Zeds Swift extension](https://github.com/zed-extensions/swift). There is also [one for VSCode](https://marketplace.visualstudio.com/items?itemName=swiftlang.swift-vscode), for anyone who wants to go that route.
However, now that I have it set up in BBEdit, I could just jump in and edit a file quickly if needed.
## Building the Application Bundle
With a bit of research, I found this tool called [Swift Bundler](https://swiftbundler.dev/). While it's possible to make it all work without this tool, it enables you to configure your app bundle with a simple .toml file, and will even scaffold out a starter project for you based on [a variety of templates](https://github.com/stackotter/swift-bundler-templates).
I created a new SwiftUI project with it, and copied the source code for AutoDock into the project. I also created a Taskfile.yml for my go-to task-runner, simply named [Task](https://taskfile.dev/) for running the various commands. This will especially come in handy for the later steps.
Aside from an error being thrown during build due to a possible race condition in my code (dont know why Xcode never caught this 🤷‍♂️), everything worked almost perfectly. The only issues I ran into were the Dock icon not hiding (AutoDock is a menubar-based application), and the app not seeing the user preferences from the Xcode build.
The first issue was down to an issue with the documentation, or perhaps my misunderstanding of it. In order to add additional properties to the applications Info.plist, such as setting `LSUIElement` to `true` for hiding the apps Dock icon. My read of the documentation was that I should add the following to the `Bundler.toml`:
```
[apps.AutoDock.plist]
LSUIElement = "True"
```
However, upon doing a bit of digging, I found this was the correct way to add it:
```
[apps.AutoDock.extra_plist_entries]
LSUIElement = "True"
```
The second issue, the new build of AutoDock not being able to see the user settings from the Xcode build, is something I will have to dive into later, since I have the app in a state that I can begin testing in my everyday usage.
One huge benefit I noticed immediately was a significant reduction in bundle size. The Xcode build of AutoDock clocks in at 2 MB, which is by no means huge, but dwarfs the new build which clocks in at just 670 KB. It seems the only difference is an Assets.car file, which I assume holds images and such from the Assets.xcassets file, which in this case would just be redundant icon image files, and the apps accent color, which I can define elsewhere.
_In the [next part](/blog/2025/building-a-mac-app-without-xcode-part-2), I tackle code signing and notarization._
[^1]: I have since discovered the slowness was user error, due to a setting in BBEdit that delayed completion suggestions for longer than I was used to.
@@ -0,0 +1,42 @@
---
title: 'Building a Mac App Without Xcode - Part 2'
pubDate: '12/4/25'
tags: ['Making Stuff', 'MacOS', 'Programming']
---
While I enjoy developing Mac apps, I find Xcode to be exceptionally clunky, especially when compared to the tools I use for web development. I thought it would be nice to be able to write and compile a fully-functional Mac application using any text editor, and without having to open Xcode.
So, I decided to try migrating my app, [AutoDock](https://github.com/ghall89/AutoDock), to this Xcode-free build process, and actually release it as the next update. I have a few requirements for this to be viable:
1. Whichever editor I use, I need access to the Swift language server.
2. The build process should produce a functional MacOS application bundle as a universal binary (for both Intel and ARM CPUs).
3. I should be able to use regular security features available to apps build with Xcode, like sandboxing, hardened runtime, etc.
4. The final binary should be signed with my developer certificate.
To prevent this blog post from being far too long, Ive decided to split it up into multiple parts. The [previous part](/blog/2025/building-a-mac-app-without-xcode-part-1) focused on LSP support, and building the application bundle. This part will focus on code signing, and uploading the application bundle to Apple for notarization so it can be distributed.
## Code Signing
If youre going to be distributing your app, its important to have it signed and notarized for the security of your app and its users. This process adds a digital certificate to your app that becomes invalidated if your app is ever modified by, for example, a malicious actor. It also can be revoked by Apple if youre found to be distributing malware, which you, if youre not a scumbag, wont have to worry about. 🙂
This, I thought, was going to be the most annoying part of working outside of Xcode. Apple does provide `codesign` and `notarytool` as part of the Xcode CLI Tools, but I just assumed using them would be a headache.
In order to sign an application, you need a certificate and, due to me not really knowing what I was doing, this was where I ran into trouble. I tried using my Apple Distribution certificate, which made intuitive sense to me at the time, and I didnt run into any issues at first. I didnt run into any errors, and verifying the signature showed everything as a-okay. So I continued...
## Notarization
For notarization, you have to use `notarytool` to upload either a ZIP file or a DMG disk image containing your application to Apple. I opted to go with a disk image, mainly because that was the method that was being used in the various Stack Overflow threads I came across. So, I created the disk image (I opted to use [create-dmg](https://github.com/create-dmg/create-dmg) to automate the process), and uploaded it to Apple. After a few moments, I received a response that my app was invalid.
I reached out for help on Mastodon, and [Dmitry Rodionov](https://mastodon.social/@rodionovd), a developer at [Sketch](https://www.sketch.com/), was kind enough to point me in the right direction. Turns out, the Apple Distribution certificate I was using not what I should be using, what I needed to to was create a Developer ID Application certificate which was the only part of this whole process I used Xcode for - and use that instead.
I also want to shout out [David Bureš](https://mstdn.social/@davidbures), developer of [Cork]() a must-have app if you use Homebrew whos response to my initial post asking for help is, I believe, how Dmitry found me. (I could be wrong, I just dont want to leave anybody out 🙂)
So, I re-signed my application, created a new disk image, and uploaded that to be notarized by Apple, and just a few moments later I got a successful!
## Final Thoughts
Despite the challenges and quirks with trying to create Mac apps outside of Xcode, I think this is the process Im sticking with going forward. The freedom to use the kinds of tools Im more comfortable with is worth the drawbacks for me.
However, if I werent simply developing Mac apps as a hobby, and I didnt enjoy the act of tinkering, I might be looking at those drawbacks very differently. It would be interesting to explore ways the developer experience can be improved, but that will have to be a project for another time.
Finally, for those who are interested in digging more into how I got this all to work, and perhaps follow my crazy example, Ive created a [template repo on GitHub](https://github.com/ghall89/mac-app-template) you can check out.
@@ -0,0 +1,63 @@
---
title: 'Default Apps 2025'
pubDate: '12/18/25'
tags: ['Apps', 'Tech']
---
An updated version of [this post](/posts/2024/default-apps-2024) from 2024, inspired by [this post](https://chriscoyier.net/2023/11/25/default-apps-2023/) from Chris Coyier (which was in turn inpsired by [this post](https://mattcool.tech/posts/default-apps-2023)).
I changed up some of the categories to avoid any entries with "none" or "N/A".
---
✉️ Mail Client: [Mail.app](https://support.apple.com/guide/mail/welcome/mac)
📮 Mail Server: [Fastmail](https://www.fastmail.com/)
📓 Notes: [Bear](https://bear.app/), [Tot](https://tot.rocks/)
✅ To Do: [Reminders](https://support.apple.com/guide/reminders/welcome/mac)
🎨 Photo Editing: [Affinity](https://www.affinity.studio/)
📆 Calendar: [Calendar.app](https://support.apple.com/guide/calendar/welcome/mac)
☁️ Cloud Storage: [iCloud](https://www.icloud.com/)
📰 RSS: [Reeder](https://www.reederapp.com/)
📇 Contacts: [Contacts.app](https://support.apple.com/guide/contacts/welcome/mac)
🌐 Browser: [Zen](https://zen-browser.app/)
💬 Chat: [Messages](https://support.apple.com/guide/messages/welcome/mac), [Discord](https://discord.com/), [WhatsApp](https://www.whatsapp.com/)
🔖 Bookmarks: [Anybox](https://anybox.app/)
📑 Read It Later: [Reeder](https://www.reederapp.com/)
📝 Writing & Text Editing: [BBEdit](https://www.barebones.com/products/bbedit/index.html)
📈 Spreadsheets: [Numbers](https://www.apple.com/numbers/)
🪟 Window Management: [Rectangle Pro](https://rectangleapp.com/pro)
🛒 Shopping Lists: [Tot](https://tot.rocks/)
🏋️‍♀️Workout Tracking: [Fitness](https://apps.apple.com/us/app/fitness/id1208224953)
💰 Budgeting: [Expenses](https://getexpenses.app/)
🗞 News: [RSS](https://en.wikipedia.org/wiki/RSS)
🎵 Music: [Apple Music](https://www.apple.com/apple-music/)
🎙 Podcasts: [Overcast](https://overcast.fm/)
🔑 Password Management: [Passwords](https://support.apple.com/en-us/120758)
👨‍💻 Code Editor: [Zed](https://zed.dev/)
📺 Terminal: [Ghostty](https://ghostty.org/)
🐘 Mastodon Client: [Ivory](https://tapbots.com/ivory/)
@@ -0,0 +1,35 @@
---
title: 'My Top 3 Games of 2025'
pubDate: '2025-12-20'
tags: ['Gaming']
---
2025 has been a tough year Due to losing my job back in June, I havent spent nearly as much money on gaming as I have in past years. As such, I spent most of 2025 replaying old favorites, or finally finishing games that have been sitting in my backlog for ages.
So, my top 3 list this year is going to be very skewed towards early 2025, as the only post-June release Ive even got to play was Hades II. So yes, from what Ive seen, if Id been able to pick up Ghost of Yōtei, it would have probably made this list.
## Hades II
Im not a big rogue-like fan, but I did enjoy the original Hades back in 2020 (despite not actually finishing it), so I was very excited for the sequel. Unlike many rogue-likes that I've played, Hades has a very nice sense of progression, and Hades II expands on that in fun and interesting ways.
In my gaming journal, I had a hard time identifying what really grabbed me about this game that led me to actually finish it. But, upon further reflection, I think it's a combination of the more interesting progression system, and the fact that there were two paths to follow a path down to Tarturus, and another up to Mount Olympus. This added enough gameplay variety that, when I was bored with o path, I'd just switch to trying to make progress in another.
I also enjoyed the concrete goals, outside of the main objective of defeating Chronos, via "prophecies". It made me feel like I was accomplishing things even when failing runs, and they continue to keep me invested in playing even though I've finished the main story.
## Clair Obscur: Expedition 33
Clair Obcur, in many ways, feels like the natural evolution Final Fantasy could have taken in another universe where they stuck with turn-based combat. Though visually, and in terms of story, it's very much its own thing.
To say this game has ruined turn-based JRPG style games for me is far from an understatement. Each party memeber not only has their own set of skills, they also have unique mechanics that make each feel very distinct, and makes combat strategy far more interesting than just picking a character's best skill for a given situation. Maelle is probably my favorite, with her stances that are applied by using certain moves usually alongside some otehr condition and apply different effects, like, "Deal 50% more damage, but take 50% more damage".
I liked pretty much all the characters, which is a rarity for me. Generally there's one or two characters I cannot stand, but they're all so likable, and you come to care about all their stories, which makes a certain choice at the end of the game very heartbreaking.
My favorite character overall though is Esquie, who serves as this game's equivient of the classic Final Fantasy airship. He's sometimes sad, rad, or even bad. But never mad.
## Xenoblade Chronicles X: Definitive Edition
Ok, this choice is a bit of a cheat, as this is a re-release of a game that I played when it originally launched on the Wii U back in 2015. But my other options for “games I played that released in 2025" were Assassins Creed: Shadows and Lost Records: Bloom and Rage, neither of which I enjoyed as much.
The main draw for this game, unlike the other Xenoblade games, is not the main story. What I think makes this game so great is the focus on exploration and getting to know the characters, party members _and_ NPCs, through the various side quests on offer. It really makes you feel like you're all survivors on a dangerous alien planet, just trying to stay alive.
My only complaint is that the extra story content added in this new version of the game is pretty lackluster. Similar to the additional content in the remake of the original Xenoblade Chronicles that came out a few years ago, I just didn't find it all that compelling.
@@ -0,0 +1,19 @@
---
title: 'The Real Tragedy of AI'
pubDate: '12/2/25'
tags: ['Tech']
---
The other day, I read [a report from Eurogamer](https://www.eurogamer.net/fortnite-fans-are-saying-no-to-ai-slop-after-spotting-what-they-believe-are-ai-generated-images-in-game) about fans of the online shooter game Fortnite getting up-in-arms about alleged AI images in the game. Specifically an image of a character with nine toes - 5 on one foot, 4 on the other. After reading the article, and looking at the image, I was convinced the criticism was valid.
Earlier today however, I read [an article on IGN](https://www.ign.com/articles/fortnite-artist-responds-as-fans-claim-game-now-includes-examples-of-blatant-ai-artwork-including-a-nine-toed-character-in-a-hammock) that raised some doubts for me about the use of generative AI artwork in Fortnite.
The truth is, I dont know what to believe.
Art in all its forms has become exhausting to consume, outside of creators I personally trust to not use AI tools to generate their creations. Stumbling upon some cool art online from someone I never heard of before used to be an exciting experience, but now I find myself immediately suspicious of everything I see.
As the articles I reference show, Im clearly not alone in that attitude, and its hard not to be. I think thats the real tragedy of generative AI; it causes us to doubt everything, including actual creative works from real, human artists. It makes what should be an enjoyable part of the human experience into a chore.
The constant AI vigilance has got to be something artists consider whenever they share something they created, how can it not be? Hell, its something I think about whenever I'm writing for this blog, something I wouldnt consider remotely close to art. Im constantly over-analyzing my writing, asking myself, “does this sound like something AI would write?”
The unfortunate truth is, even if the AI bubble pops tomorrow, the technology isnt going away. The genie is out of the bottle. Given that reality, how do we stay vigilant without flagging someones work as AI slop when it isnt?
@@ -0,0 +1,35 @@
---
title: "An AI Skeptic's Take on Agentic Coding"
pubDate: '2/21/2026'
tags: ['Tech', 'Programming']
---
As part of OpenAIs latest effort to push their agentic coding tool, Codex, I was able to snag a free month of access to the latest models. So, I thought it would be an interesting opportunity to experiment, actually do a deep dive on coding agents, and gain a better understanding of their capabilities and limitations.
Firstly, I want to address the fact that Ive been very vocal in my disdain for AI tools, but at the same time Ive come to accept that these tools are here to stay, at least for the time being. Therefore, I want to experience them first-hand to give myself the most informed opinion I can. Though, as always, my opinion is subject to change, and its very likely I will swing back more towards the anti-AI side before long. 😅
I had three main use-cases I wanted to try out; making incremental changes to an existing codebase, converting an app from one language to another, and creating a new project from scratch.
First up, I took a music player app Ive been working on for the last several weeks. I was having difficulty processing large batches of audio files efficiently, so I asked Codex to come up with some ideas to improve what I had wrote. It took a couple minutes to process everything, but it spat out some code and asked me to approve the changes.
It all looked good to me, so I gave it the OK, and ran the application. I was pretty impressed by the results, so I decided to take it a step further, and use it to implement a feature from scratch. So, I prompted it to create an audio playback queue, and connect it to the UI that I built. In less than 10 minutes, I had a very rough implementation of the feature up and running.
There were some structural changes I had to make to the batch processing code to be more reusable, and I still have a chunk of work to do on the playback queue, but Codex got me a lot further along than I would have got otherwise.
The other two use-cases converting code from one language to another, and creating a project from scratch were more along the lines of what I expected from Codex going in.
The code conversion experiment involved porting my app [KeyStash](https://github.com/ghall89/KeyStash) (an app for storing software license keys) from macOS to the web. While it did a serviceable job translating data structures, and approximating the UI, it made a lot of sloppy mistakes, like using an old and vulnerable version of Next.js, and referencing variables it didnt even create. It even tried creating an image upload mechanism that not only didnt even work, but wasnt even a sensical addition. [^1]
My final experiment was pointing it to an empty directory and telling it to create a web-based rich text editor that can import and export .RTF files. It did a far better job with file uploads than the last experiment, but everything else was pretty poorly implemented. Though, it did manage to pull in more up-to-date dependencies. Also, while I wouldnt call the code unreadable, I certainly would not want to maintain it.
Of the three use-cases, the only one I can see as being remotely useful for anything, beyond just messing around with experiments like these, is making incremental changes to an existing codebase. These tools seem to be at their best when generating small bits of code with as much context as possible, and, incidentally, the cognitive load required to review its output is so much more manageable.
Its crystal clear to me how generating swathes of code either ported from an existing codebase, or starting from a blank-slate is a recipe for poor maintainability. Even the relatively simple rich text editor that I “vibe coded” had enough code to sift through and debug that I probably would have been better off just writing it myself in the first place. If youre of the belief that a developer should be responsible for the code they ship, regardless of if it was written by hand or an LLM[^2], its easy to see how that model of development is grossly unsustainable.
All that being said, its incredibly tempting to utilize these tools for just building stuff quickly, and dealing with the consequences later. I get why its so appealing, and I have found myself struggling and occasionally, failing to resist that temptation. However, every time I gave in to that temptation, I found myself extremely unhappy with what was produced. I think “slop” would be a perfectly accurate word to describe it.
I think its important to remember, these tools arent magic, theyre exactly what they are tools. Like any tool, they have a purpose that theyre good at, and you have to know when, and how, to use them properly. This technology is all so new; I think were all still trying to figure out what “properly” means, and its sensible to be skeptical at this point. Anyone who tells you otherwise, or says that youll be left behind if you dont jump on the bandwagon now, is lying to you.
[^1]: I can only assume it was trying its best to “recreate” the functionality in KeyStash that grabs app icons when adding a new license key to the Mac version of KeyStash, without having any access to system APIs or the file system. 🤷‍♂️
[^2]: And if youre not, maybe find a different career. Sorry, not sorry.
@@ -0,0 +1,21 @@
---
title: 'My Music Server Setup'
pubDate: '4/14/2026'
tags: ['Music', 'Tech']
---
For several months now, I've been running an old M1 Mac Mini (weird to think of that as old) as a music server. It's mainly served as source for me to bring music into [Groove](https://grooveplayer.app), the iOS music app I've been building, but I've wanted to do more with it namely, use it as a source of music for my stereo.
So, a little over a month ago, I moved the Mac Mini into my living room and connected it to my stereo system that was the easy part.
The hard part was figuring out the software solution, which involved a lot of trial and error. I tried being a total nerd, and installed a few command line tools I could control via SSH, but that A: required me to control the music from my MacBook, and B: caused playback to stop if the connection was lost for any reason.
I also tried out media servers like Plex and Jellyfin, both of which not only ended up being overkill for my purposes, but ended up being clunky to control remotely. And, in the case of Jellyfin, it was very much a pain to set up, and it didn't organize music in a way that I liked.
In the end, I decided the best solution was probably the simplest, and figured I should just use the tools Apple provides out of the box. So, here is my final music server setup:
1. Music.app, with Home Sharing turned on for playing music via the [Remote](https://apps.apple.com/app/itunes-remote/id284417350) app on iOS
2. [Sleeve](https://apps.apple.com/app/sleeve-for-spotify-music/id1606145041) to handle Last.fm scrobbling
3. File sharing turned on for importing music into [Doppler](https://brushedtype.co/doppler/) on Mac, and Groove on iOS
There are still a few things to iron out primarily: adding music to the server entirely through the file system without having to use Screen Sharing, but also figuring out where the "source of truth" should be for my music library. But I think, for the most part, this is the best solution for my needs.
@@ -0,0 +1,58 @@
---
title: 'My Favoite Things of 2025'
pubDate: '1/2/2026'
tags: ["Life", "Learning"]
---
We're already a couple days into 2026, but due to family obligations, as well as getting far too absorbed into Ghost of Yōtei mostly the latter, if I'm honest I'm only getting around to doing this post now.
2025 was a stressful year. On a personal level, I lost my job and have been scrambling to find one for over 6 months now. More importantly though, it's just felt like the world has been constantly on fire, and AI has been quickly turning everything good about the human experience into souless slop. That being said, it's not all bad, and I wanted to reflect on my favorite things/moments in 2025.
I won't be talking any video games, as I [already covered that](/blog/2025/my-top-3-games-of-2025).
## Catching Up On Movies
One positive that came from me losing my job was that I finally had the desire to watch movies again. Seeing as my job involved me being in front of a screen for 8 hours, I was very selective about how I spent my screen time outside of work, and that time primarily went to gaming. But over the last 6 months I've watched more movies than I had in a long time. These are some of my favorites:
- The Batman
- Kiki's Delivery Service
- The Life of Chuck
- Edge of Tomorrow
Though only one is from 2025 (The Life of Chuck), these are all movies I watched for the first time last year.
## Taskmaster Series 19 - 20
On the topic of watching things, 2025 saw 2 new ~~seasons~~ series of Taskmaster, and they both had some of my favorite lineups in the show.
As if that wasn't enough, we also got the 4th Champion of Champions, where the previous 5 winners compete against eachother. While it wasn't the strongest in terms of the actual tasks, the lineup made it work. Even Sam Campbell, who's brand of humor generally wasn't really to my taste in series 16, got a few chuckles out of me.
![](src/assets/blog/gifs/destroy-taskmaster.gif)
## Music
2025 wasn't a huge year for me in terms of music, but there were a couple albums that stood out for me.
First, Ego Death At A Bachelorette Party from Hayley Williams was a standout as my most played album released this year, and might actually be my most played overall. Some standout tracks to me are "Kill Me", "Mirtazapine", and "True Believer".
Second, the soundtrack to Clair Obscur: Expedition 33[^1] is some of the most beautiful game music I've heard. It's up there with Journey, Nier Replicant, and The Witcher 3 for me. Like many others, I'm obsessed with the track "Lumière". But, another standout for me is "Flying Waters - Rain From The Ground", which brings in a synth track which sounds like a terrible idea on paper, but just adds to the odd atmosphere of the game.
The end of 2025 also saw the beginnings of my project to liberate my music library, and give up streaming altogether much to the chagrin of my Mastodon followers, who have been bombarded with my grievences on the horrors of Apple Music.
## The Lord of the Rings: Fate of the Fellowship
I've played a lot of different board games in 2025, but the new release from the past year that I enjoyed the most was The Lord of the Rings: Fate of the Fellowship. It's yet another game designed around the core mechanics of Pandemic, but it introduced enough to make if feel distinct, but not overbloated (looking at you, World of Warcraft: Wrath of the Litch King).
I tend to not enjoy board games based on existing IPs but, being a huge fan of The Lord of the Rings, I had to give it a shot, and I'm glad I did.
## Learning New Stuff
Last year was a year of learning for me, right from the get-go. In January I created an app called AutoDock, which involved me diving deeper into Mac development than I have in previous years, diving into system APIs to detect display changes - mainly resolution changes, and external displays getting connected/disconnected.
I also expanded my horizons in the world of web development. I've primarily been a React developer, and, though I was aware of other frameworks, I never had the time or motivation to dive into them. But over the last 6 months I got up to speed with SolidJS and Svelte, and I've got pretty comfortable using them.
In addition to all that, I learned the basics of other languages: Rust, Go, and Ruby. While I wouldn't say I gained proficiency in any of those languages, I did gain a better understanding of things like memory management, and I think I'm going into 2026 a much better programmer than I was a year ago.
I also learned how to play croquet over the Summer, though I clearly need a lot of practice if I ever want to be moderately okay at it.
[^1]: I said I wouldn't be talking about _video games_. I said nothing about _video game soundtracks_. 😜
@@ -0,0 +1,21 @@
---
title: 'My Gaming Journal'
pubDate: '2/3/2026'
tags: ['Life', 'Gaming']
---
For the last 2 years, Ive kept a gaming journal in an actual, physical notebook to record my thoughts and feelings on the games Ive finished.
Journaling is something Ive struggled to keep up with but have wanted to do for many years. Ive gone through some fits and starts with it, but Ive never been able to keep up with it consistently. So, its been nice to have something in that realm that I have been able to keep up with.
For me, its not just about getting away from the screen or the craft of writing by hand—my handwriting is atrocious and definitely not “crafty.” Its about putting the ink to the paper and not being able to change it. Yes, you can scribble things out or tear out the page and start over, but in the end, you have to embrace the imperfections.
Thats a challenging thing for me. When I type, I sometimes find writing exhausting because Im constantly editing, rewording, moving things around, and trying to make what I write some version of “perfect.” I find it difficult to just write and not worry about making mistakes or wording something poorly.
Writing by hand forces me to just accept my mistakes and move on. That frees my brain up to just write, stream-of-consciousness-style. In the case of my gaming journal, I end up writing things that I maybe wouldnt write if I were typing something out about the game for my blog.[^1]
For example, in my entry on Cyberpunk 2077, which I finished this past December, I wrote a little story about what happened to my V after the events of the game and about how she and Judy lived out their lives and grew old together. Something I would never have written if I was writing my thoughts in a review format.
Theres nothing in this journal that I would consider good writing, but thats okay. Its not for anybody but myself, and its a way to keep me journaling on a semi-regular basis. And the most embarrassing thing in it is the page I wrote on why I thought Queens Blood from Final Fantasy VII Rebirth is better than Gwent from The Witcher 3 (dont @ me).
[^1]: Incidentally, I have referred to my gaming journal when writing my yearly “Top 3 Games” posts.
@@ -0,0 +1,25 @@
---
title: 'My OCD Experience'
pubDate: '5/2/2026'
tags: ['Life', 'Mental Health']
---
If you go to my [profile on Mastodon](https://mastodon.social/@ghalldev), you'll see one of my hashtags: #ActuallyOCD, but I don't think that's something I've ever really elaborated on. I also feel like it's generally pretty misunderstood by a lot of people. I myself had a pretty surface-level and, perhaps, stereotypical view of OCD, to the point where I didn't really consider it a possibility for myself, and it made me scared to even bring it up with therapists for reasons I'll get into.
So, my perception of Obsessive-Compulsive Disorder, combined with the fact that I've made a lot of progress over the last decade, means it's not something I think many people would notice about me, outside of maybe thinking I'm a bit odd. Though I'm pretty sure that would be the case regardless of having OCD.
The primary aspect of my OCD experience was always intrusive thoughts, which are always a tough one to explain to people who have never experienced them. It's not as straightforward as needing to check the oven five times to make sure it's really off—most people, at some point, have worried about whether they turned the oven off, so that example is more relatable to the average person.
Intrusive thoughts, like the ones I experience, are recurring thoughts that enter my head and are upsetting, sometimes scary, and seemingly impossible to brush aside. Imagine a voice in your head telling you something bad will happen, or you might do something bad, if you don't do something specific or don't get out of the situation. It's an unsettling feeling, especially if you don't know it's OCD and you think that you're maybe, for lack of a better word, crazy.
One example that comes to mind—and was perhaps one of the most upsetting moments I've experienced with intrusive thoughts—was on a walk with my last girlfriend. We were walking down the sidewalk near some water, and I forget the exact circumstance, but she asked me to hold her keys for a moment—so I did. Moments later, the thought entered my mind that I was just going to throw her keys into the water. I wouldn't ever do that, of course, but the thought had entered my mind, and I was legitimately scared that I was going to lose control and just toss the keys. I gave them back to her because the thought was so overwhelmingly strong, and told her what happened. She didn't really understand, and at that point in my life, neither did I.
I also didn't own any sharp cooking knives for a lot of my adult life because whenever I held a knife, I would inevitably become overwhelmed with intrusive thoughts about slipping up and accidentally cutting myself or someone else, complete with full gory details. This was the thing I finally told my therapist about, and what led to actually getting my OCD diagnosis.
Between adjustments to my meds and exposure therapy—part of which involved cutting vegetables with a giant kitchen knife—I now manage my OCD a lot better. It still has an effect on my day-to-day life, but it's not quite so disruptive as often.
Most of the challenges I still face are things like needing to touch solid objects I walk by to silence the thoughts of impending doom and choosing my walking path to optimize the touching of those objects.
Occasionally, I do deal with some more upsetting intrusive thoughts. A recent one that comes to mind was when I had this thought about flipping the board when playing a game with some people. Not out of anger or frustration—it just entered my head as something I might just lose control of myself and do. But, unlike in the past, in these situations, I am able to acknowledge that the thought is just a thought and then refocus on something else. It's still intensely uncomfortable, but it becomes a little blip instead of a total disruption.
This is, of course, just my experience with OCD, and, like anything we humans struggle with, there is no such thing as a typical experience. Figuring out I have OCD was actually quite liberating because, due to my own preconceived notions of OCD, it never occurred to me that I wasn't totally bonkers for having to close the window while riding in a car because I was having intrusive thoughts about chucking my phone into the street.
+13
View File
@@ -0,0 +1,13 @@
---
title: 'Notes on Last.fm'
pubDate: '1/17/2026'
tags: ['Music']
---
After [freeing my music library](/blog/2026/setting-my-music-free), I knew the one thing I would miss from Apple Music was the discovery aspect. It was very easy to just open the music app and browse suggestions for new music to check out, or releases from artists I enjoy that maybe I havent heard yet.
Ive been using [Doppler](https://brushedtype.co/doppler/) on my Mac and iPhone to manage and listen to my library, and while poking around the settings I discovered [Last.fm](https://www.last.fm) integration. What a throwback!
I signed up for an account my old account is either connected to an email I no longer have access to, or long since deleted and started “scrobbling".
Over the last week-and-a-half, Ive listened to 668 different songs from 120 artists across 186 albums, and its been fun seeing some of the suggestions that have come from that. Its been a good mix of stuff I have in my library but havent listened to in a while, and new music I probably never would have found otherwise.
+15
View File
@@ -0,0 +1,15 @@
---
title: 'Revisiting Nova'
pubDate: '1/27/2026'
tags: ['Apps', 'Programming']
---
Almost 2 years ago, [I made the switch from Nova to Zed](/blog/2024/am-i-switching-to-zed/) for writing code. Id been mostly happy with it during that time, but it never felt like a truly polished piece of software, despite being a pretty smooth experience in terms of its text-editing capabilities.
Over the last few weeks, Ive been giving Nova another try. Ive been able to get the LSPs for TypeScript, Svelte, and Astro working for web development, in addition to Swift, Ruby, and Go for other projects. I do find myself having to restart the TypeScript language server at least once a day, but I had to do the same thing in Zed so maybe its not an editor problem.
I get joy from coding with Nova in a way I dont get from Zed, VS Code, or even Xcode (yuck!). It looks nice, it feels nice to use, and it doesnt eat up system resources unnecessarily.
The primary con is the app's uncertain future. Panic, Inc. has been frustratingly uncommunicative about Nova's development, despite promises to be more transparent with the community. For an app that costs $100, plus another $50 every year for updates, and is a tool many of their users rely on for work, it feels a little bit like a betrayal.
Apparently built-in LSP support is on the way at some point, eventually, so we won't have to rely on extensions for that functionality. While I am excited for that feature, it feels less like an effort to make the app more appealing, and more like throwing in the towel. I hope I'm wrong, because Nova is the kind of top-tier Mac app that just doesn't get made anymore.
@@ -0,0 +1,41 @@
---
title: 'Setting My Music Free'
pubDate: '1/3/2026'
tags: ['Music', 'Tech', 'Apps']
---
I've been a loyal Apple Music subscriber pretty much since it launched 10 years ago, and I've really got used to having almost any song I could imagine wanting right in my pocket. I liked the promise of carrying my existing music library into the world of music streaming, and having everything sync seamlessly between devices. I was very frustrated with how buggy and unreliable syncing music to iOS devices at the time (keep reading to find out if this will bite me in the butt again), so an all-you-can-eat buffet of music, combined with what was promised to be a seamless syncing method, was far too good to pass up.
## The Final Straw
Historically I've been quite meticulous with how I organize my music though I know there are people _far_ more meticulous than I am. That includes changing metadata for things like genre, because I don't need 15 variations on "Alternative" when just one will do. I also like changing track titles to remove extraneous information that could be gleaned from the metadata, like featured artist. Turns out, Apple Music does NOT like this, and it's a constant fight to keep things the way I like them.
But, even if you decide to throw in the towel like I eventually did, Apple Music often doesn't even like _its own_ metadata. Instead of just leaving things you've added to your library alone, it will just randomly change things around, which would be ok except it often doesn't propagate correctly, which leads to albums being split (at least, that's my theory as to why it happens). I even had it change album art to something completely unrelated to the album.
What finally broke me however was when an album I had imported not even from the iTunes Store, I got it in a Humble Music Bundle over a decade ago became "unavailable to play". At some point in the last decade, Apple Music must have co-opted that album and then, when it was taken off streaming, decided I could no longer have access to it.
One thing became clear; with Apple Music, I don't even own the music that I own. Maybe I should have seen this coming, but better late than never I suppose.
## Libera Musica
Anyway, I began my quest to liberate my music, but it wasn't without issue. As I mentioned, at some point Apple Music co-opted a chunk of my music. There was no rhyme or reason to which albums, or even which individual tracks, got absorbed into the streaming machine. This meant a non-insignificant portion of my music was either just unavailable, or imprisoned behind DRM. Yes, that means Apple locked music I own and imported into their service behind a subscription!!! That's when I knew I was making the right call...
My first step was to see how much music I could rescue just by downloading the tracks that remained untouched and DRM-free. This is where I ran into the next major issue, but this time it was mostly my fault, and I could have saved myself _hours_ if I spent more time troubleshooting. See, a huge number of tracks downloaded as weird `.movpkg` files, which I couldn't really do anything with.
Turns out, if I had just turned off "Enable Lossless Audio" in the Music app's settings, under the "Playback" tab, I could have saved myself a lot of time and headache...
However, the silver lining is that, while I was downloading the handful of albums I purchased from Amazon over the years, I discovered that just about every vinyl record I ever bought from them was made available as a DRM-free digital download. A nice little bonus, considering it gave me a little bit of a head start on buying some music I only had access to through my subscription.
## 8,000 Songs In My Pocket
Now that I had my music all together, I needed some way to (obsessively) manage it all, and get it synced to my phone.
I had hope that someone at Apple found it in their heart to fix the music syncing issues that cropped up in iOS well over a decade ago. Nope, apparently it's even worse, and a quick web search showed me I'm not imagining it. I'd also ruled out iTunes Match out the gate, because of the sub-par experience I had with it, and if they hadn't fixed manual syncing, I wasn't going to spend $25 to see if they fixed iTunes Match. The product page that clearly hasn't been touched since 2011 didn't inspire much confidence either.
If I wanted something reliable, I would have to look outside the slowly deteriorating mess that is Apple's music infrastructure.
After a bit of searching, I came across an app called [Doppler](https://brushedtype.co/doppler/), for both iPhone and Mac. I've been trying it out, and it's been a pretty solid experience so far. It's a simpler app, but I think that works to its benefit. Getting music from the Mac to the iPhone using Doppler isn't as seamless as, say, getting music from iTunes to an iPod back in the day. It's for sure a more manual process. But it works, and can even be done wirelessly.
It sort of reminds me of loading music onto my first MP3 player back in like 2004, and I don't think I hate it. There's a certain charm to it, and I think it makes the whole process feel more intentional. Not quite putting on a vinyl record, or even a CD, but it adds a bit of soul to an otherwise cold and mechanical process. I think that was lost in the age of the smartphone, and even moreso since streaming really took hold.
Am I romanticizing it a bit? Probably. On the other hand though, I might have a total breakdown, and buy an iPod Classic on eBay. Whatever happens though, I think I'm sticking with owning my music, and taking back control over how it's organized, for the foreseeable future.
+1
View File
@@ -6,6 +6,7 @@ export interface NavLink {
export const navLinks: NavLink[] = [ export const navLinks: NavLink[] = [
{ label: 'Blog', path: 'blog/page/1' }, { label: 'Blog', path: 'blog/page/1' },
{ label: 'Now', path: 'now' }, { label: 'Now', path: 'now' },
{ label: 'Uses', path: 'uses' },
{ label: 'Projects', path: 'projects' }, { label: 'Projects', path: 'projects' },
{ label: 'Search', path: 'search' }, { label: 'Search', path: 'search' },
]; ];
+7
View File
@@ -1,6 +1,7 @@
import autodockImg from '@assets/projects/autodock.png'; import autodockImg from '@assets/projects/autodock.png';
import bggClientImg from '@assets/projects/bgg-client.png'; import bggClientImg from '@assets/projects/bgg-client.png';
import keystashImg from '@assets/projects/keystash.png'; import keystashImg from '@assets/projects/keystash.png';
import grooveImg from '@assets/projects/groove.png'
export interface Project { export interface Project {
title: string; title: string;
@@ -10,6 +11,12 @@ export interface Project {
} }
export const projects: Project[] = [ export const projects: Project[] = [
{
title: 'Groove',
description: 'An offline music player for iOS. ',
image: grooveImg,
link: "https://grooveplayer.app"
},
{ {
title: 'AutoDock', title: 'AutoDock',
description: description:
+3
View File
@@ -17,6 +17,8 @@ const { post } = Astro.props;
const { data } = post; const { data } = post;
const { Content } = await render(post); const { Content } = await render(post);
const canonicalUrl = new URL(Astro.url.pathname, import.meta.env.SITE).href;
--- ---
<Layout title={data.title}> <Layout title={data.title}>
@@ -24,6 +26,7 @@ const { Content } = await render(post);
<BlogHeader title={data.title} date={data.pubDate} /> <BlogHeader title={data.title} date={data.pubDate} />
<Tags tags={data.tags} /> <Tags tags={data.tags} />
<Content /> <Content />
<a href={`https://share.joinmastodon.org/#text=${data.title}%0A%0A${canonicalUrl}`} target="_blank" rel="noopener">Share on Mastodon</a>
<a href="https://notbyai.fyi/" target="_blank"> <a href="https://notbyai.fyi/" target="_blank">
<i class="not-by-ai"></i> <i class="not-by-ai"></i>
</a> </a>
+6 -6
View File
@@ -8,7 +8,8 @@ import Layout from '@layouts/Layout.astro';
import BlueskyIcon from '../assets/svg/bluesky.svg'; import BlueskyIcon from '../assets/svg/bluesky.svg';
import MastodonIcon from '../assets/svg/mastodon.svg'; import MastodonIcon from '../assets/svg/mastodon.svg';
const iconSize = 16; const ICON_SIZE = 16;
const posts = await getCollection('blog'); const posts = await getCollection('blog');
const latestPost = posts.sort( const latestPost = posts.sort(
@@ -20,8 +21,8 @@ const latestPost = posts.sort(
<Layout title="Welcome"> <Layout title="Welcome">
<Avatar size={200} /> <Avatar size={200} />
<p> <p>
My name is <strong>Graham</strong> (he/him), a full-stack web developer, and My name is <strong>Graham</strong> (he/him), a full-stack web developer, and tech
tech enthusiast. enthusiast.
</p> </p>
<p> <p>
When I'm not writing code, I'm usually enjoying one of my other hobbies; When I'm not writing code, I'm usually enjoying one of my other hobbies;
@@ -50,12 +51,12 @@ const latestPost = posts.sort(
href="https://mastodon.social/@ghalldev" href="https://mastodon.social/@ghalldev"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
><MastodonIcon width={iconSize} height={iconSize} />Mastodon</a ><MastodonIcon width={ICON_SIZE} height={ICON_SIZE} />Mastodon</a
> and <a > and <a
href="https://bsky.app/profile/ghalldev.bsky.social" href="https://bsky.app/profile/ghalldev.bsky.social"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
><BlueskyIcon width={iconSize} height={iconSize} />Bluesky</a ><BlueskyIcon width={ICON_SIZE} height={ICON_SIZE} />Bluesky</a
>. >.
</p> </p>
<p> <p>
@@ -65,7 +66,6 @@ const latestPost = posts.sort(
rel="noopener noreferrer">Natalia Vazquez</a rel="noopener noreferrer">Natalia Vazquez</a
>. >.
</p> </p>
<LatestPost />
</Layout> </Layout>
<style lang="scss"> <style lang="scss">
+20 -20
View File
@@ -7,35 +7,35 @@ title: Now
What's a [Now page](https://nownownow.com/about)? What's a [Now page](https://nownownow.com/about)?
_Last updated: November 29, 2025_ _Last updated: April 28, 2026_
Currently unemployed, looking for a new job in web development. In between applying for jobs, I've been volunteering as a maintainer on the website for a humanitarian aid organization, and working on side projects on both web and Mac.
I'm also building [Groove](https://grooveplayer.app), a music player for iPhone, because I [recently liberated my music collection from streaming](/blog/2026/setting-my-music-free), and I've not been happy with any of the existing options.
## 🎧 Listening ## 🎧 Listening
- [Ego Death at a Bachelorette Party - Hayley Williams](https://album.link/i/1833006180) - [Apollo 18 - They Might Be Giants](https://www.last.fm/music/They+Might+Be+Giants/Apollo+18)
- [The Else - They Might Be Giants](https://album.link/i/635922095) - [Memory - Vivian Girls](https://www.last.fm/music/Vivian+Girls/Memory)
- [Clair Obscur: Expedition 33 (Original Soundtrack) - Lorien Testard](https://album.link/i/1808472460) - [Mikaela Davis - Mikaela Davis](https://www.last.fm/music/Mikaela+Davis/Mikaela+Davis)
## 🎮 Playing ## 🎮 Playing
- [Baldur's Gate 3](https://www.igdb.com/games/baldurs-gate-iii) - PS5 **[Persona 5 Royal](https://www.igdb.com/games/persona-5-royal) - Switch**
<!--- [Cyberpunk 2077](https://www.igdb.com/games/cyberpunk-2077) - PS5-->
- [Rise of the Tomb Raider](https://www.igdb.com/games/rise-of-the-tomb-raider) - PS5 I've dove back into this game after an extended break and my goal is to focus on finishing it up. It's one of my favorite RPGs but I've been playing the Switch version on and off for the last 2 years.
**[Baldur's Gate 3](https://www.igdb.com/games/baldurs-gate-iii) - PS5**
Playing a co-op campaign with some friends. We've been playing for a few hours almost weekly. We are currently deep in Act 3.
## 🎲 Rolling ## 🎲 Rolling
- [Knarr](https://boardgamegeek.com/boardgame/379629/knarr) **[Lancaster](https://boardgamegeek.com/boardgame/96913/lancaster)**
- [Fall of Rome](https://boardgamegeek.com/boardgame/260428/fall-of-rome)
- [CuBirds](https://boardgamegeek.com/boardgame/245476/cubirds) My favorite heavy game of the moment. I don't own a copy but I've been playing it anytime I get a chance.
## 📺 Watching ## 📺 Watching
- Nothing at the moment... ☹️ - [Daredevil: Born Again](https://www.themoviedb.org/tv/202555-daredevil-born-again?language=en-US) Season 2
- [Taskmaster](https://www.themoviedb.org/tv/63404-taskmaster?language=en-US) Series 21
## 💻 Using
- MacBook Air M2 - Midnight
- 24GB RAM
- 512GB SSD
- LG UltraFine 27"
- Nuphy Air75 v2
- Apple Magic Trackpad/Logitech A72 (depending on my mood)
+48
View File
@@ -0,0 +1,48 @@
---
layout: ../layouts/Layout.astro
title: Uses
---
# Uses
As both a professional web developer, an amatuer Mac and iPhone developer, as well as a hobbyist blogger, I have a variety of tools I use to get my work done.
_Last updated: March 5, 2026_
## Hardware
My current workstation is an M2 MacBook Air, which I have connected to an [LG UltraFine 27"](https://www.lg.com/us/monitors/lg-27up850k-w-4k-uhd-led-monitor) display, with a [NuPhy Air75 V2](https://nuphy.com/products/air75-v2) keyboard (with classic Mac-style keycaps), and a [Logitech M720](https://www.logitech.com/en-us/shop/p/m720-triathlon) mouse.
### MacBook Air Specs
- OS: macOS Tahoe
- RAM: 24GB
- Storage: 512GB SSD + external 1TB Samsung SSD
## Software
### Text Editors
- [Zed](https://zed.dev/) - My daily driver for web development. I like it because it's fast, minimal, and just works.
- [Nova](https://nova.app/) - I very much want this to be my daily driver for web development...
- [Xcode](https://developer.apple.com/xcode/) - Where I do all my iPhone and Mac dev, but also _anything_ Swift-related.
- [BBEdit](https://www.barebones.com/products/bbedit/index.html) - My go-to app for making quick edits, and for writing.
### Productivity
- [Zen Browser](https://zen-browser.app/) - Primary web browser.
- [Bear](https://bear.app/) - Jotting down and organizing notes across my Mac, iPad, and iPhone.
### Other Desktop Apps
- [Doppler](https://brushedtype.co/doppler/) - My desktop music player of choice.
- [WindowKeys](https://www.apptorium.com/windowkeys) - For assigning custom keyboard shortcuts to macOS's built-in window management.
- [ForkLift](https://binarynights.com/) - Finder alternative I mainly use for moving or copying files quickly.
### CLI Tools
My terminal emulator of choice is [Ghostty](https://ghostty.org/). These are some of the CLI tools I find useful:
- [Homebrew](https://brew.sh/)
- [Atuin](https://atuin.sh/)
- [Zoxide](https://github.com/ajeetdsouza/zoxide)
- [trash](https://hasseg.org/trash/)
+1 -5
View File
@@ -46,11 +46,7 @@ main {
a { a {
color: var(--blue); color: var(--blue);
text-decoration: none; text-decoration-style: dotted;
&:hover {
text-decoration: underline;
}
} }
main p { main p {