diff --git a/AGENT.md b/AGENT.md new file mode 100644 index 0000000..2d6e6cd --- /dev/null +++ b/AGENT.md @@ -0,0 +1,78 @@ +# Agent Reference + +Guidelines for AI agents working on this blog. + +--- + +## Blog Post Style + +- **Voice:** First-person, direct, opinionated. No preamble, no "Hope this helps" closings. +- **Opening:** Hook into a specific frustration or observation. Skip the generic intro. +- **Headings:** + - `##` — lede/subtitle (one per post, immediately after the title, punchy) + - `###` — main sections + - `---` — horizontal rule between sections + - No `#` (H1) anywhere in the post body +- **Closing:** End decisively. A statement, not a sendoff. +- **Grammar:** Correct. No "Do you ever wondering", no "Let's get started!" +- **Tone reference:** See the three most recent posts by date. + +--- + +## Tag System + +### Current Vocabulary + +| Tag | Description | +| --------------------- | ------------------------------------------------------------------------- | +| `linux` | Linux-specific content — distros, tools, fixes | +| `flutter` | Flutter framework, mobile development | +| `android` | Android-specific content, ADB, AVD | +| `windows` | Windows-specific content, PowerShell, Win tooling | +| `git` | Git commands, workflows, hosting | +| `hardware` | Physical hardware, drivers, firmware quirks | +| `ai` | AI-assisted development, LLM tools, agents | +| `dev-setup` | Developer environment, tooling, workflow, DX | +| `dart` | Dart language, Dart-specific libraries | +| `go` | Go language | +| `craftsmanship` | Software design, architecture, TDD, DDD, Clean Architecture, code quality | +| `self-hosting` | Self-hosted services, VPS, infrastructure | +| `distributed-systems` | Distributed data, sync, CRDTs, consensus | +| `misc` | Catch-all for posts that don't fit elsewhere | + +### Per-Post Tags (current state) + +| Post | Tags | +| -------------------------------------------- | ------------------------------ | +| stop-stashing-use-git-worktree | `git`, `dev-setup` | +| vibe-coding-still-needs-a-craftsman | `ai`, `craftsmanship` | +| building-my-own-self-hosted-music-library | `linux`, `self-hosting` | +| building-load-testing-script-with-claude | `go`, `ai` | +| crdt-conflict-free-replicated-data-types | `dart`, `distributed-systems` | +| fix-adb-unsufficient-permission-linux | `linux`, `android` | +| fix-infinix-air-pro-plus-quad-speakers-linux | `linux`, `hardware` | +| fix-infinix-air-pro-plus-screen-color | `windows`, `linux`, `hardware` | +| flutter-android-emulator-not-showing | `flutter`, `android` | +| flutter-clean-architecture | `flutter`, `craftsmanship` | +| immutable-workstation-fedora-kinoite | `linux`, `dev-setup` | +| kuwot (draft) | `flutter`, `dart` | +| remap-copilot-key-infinix-air-pro-plus | `linux`, `hardware` | +| sign-github-commit-on-windows | `git`, `windows` | +| using-direnv-in-powershell-on-windows | `windows`, `dev-setup` | +| welcome | `misc` | + +### Rules for Tags + +**When tagging a post:** + +- Use 2–4 tags per post. More than 4 is a sign you're being too specific. +- Prefer tags from the existing vocabulary above. +- A tag should describe what the post _is about_, not every concept it _mentions_. + +**When adding a new tag:** + +- Ask: will this tag apply to at least one other existing post, or is it clearly a category this blog will write about again? +- If yes: add it to the vocabulary table above and apply it. +- If no: fold into an existing tag or leave it out. +- Do not add tags for topics covered once and unlikely to recur (e.g. `music`, `testing`, `open-source`). +- Document new tags in this file. diff --git a/src/content/blog/building-load-testing-script-with-claude.md b/src/content/blog/building-load-testing-script-with-claude.md index f5f199c..617dad8 100644 --- a/src/content/blog/building-load-testing-script-with-claude.md +++ b/src/content/blog/building-load-testing-script-with-claude.md @@ -5,7 +5,6 @@ date: 2026-02-22T13:23:00+07:00 draft: false tags: - go - - testing - ai --- diff --git a/src/content/blog/building-my-own-self-hosted-music-library.md b/src/content/blog/building-my-own-self-hosted-music-library.md index 1b48e49..734d4a5 100644 --- a/src/content/blog/building-my-own-self-hosted-music-library.md +++ b/src/content/blog/building-my-own-self-hosted-music-library.md @@ -6,8 +6,6 @@ draft: false tags: - linux - self-hosting - - music - - open-source --- There's a specific kind of dissatisfaction that comes with streaming services. The music is there, the app is polished, but none of it is really _yours_. The moment you stop paying, it disappears. The algorithm decides what comes next. Your listening history is someone else's data. diff --git a/src/content/blog/crdt-conflict-free-replicated-data-types.md b/src/content/blog/crdt-conflict-free-replicated-data-types.md index 6487e8b..59ff842 100644 --- a/src/content/blog/crdt-conflict-free-replicated-data-types.md +++ b/src/content/blog/crdt-conflict-free-replicated-data-types.md @@ -5,14 +5,16 @@ date: 2025-02-24T09:58:08+07:00 draft: false tags: - dart - - programming + - distributed-systems --- -# Background +## Two devices. Independent changes. No server. No conflicts. -When searching techniques for syncing data between peers, I stumbled upon CRDT (Conflict-free Replicated Data Types). It's basically a algorithm for syncing for distributed systems. CRDT ensures all data changes between peer will be synced with correct order and no data loss. +I was building an offline-first feature in Flutter — two devices needed to modify the same data independently and sync later without losing anything. Last-write-wins throws data away. Distributed locks need a server. Neither worked. -Since I working with Dart (for Flutter project), I use a [CRDT library for Dart](https://github.com/cachapa/crdt). This library implements core concept of CRDT and it's pretty basic. Here some types of CRDT that often used: +That's when I found CRDTs — Conflict-free Replicated Data Types. A data structure that can be modified independently on multiple nodes and always merged into a consistent final state, without real-time coordination. + +Since I was working in Dart, I used the [CRDT library](https://github.com/cachapa/crdt) by cachapa. It implements the core concepts. The common types: 1. **G-Counter (Grow-only Counter)**: A counter that can only be incremented. 2. **P-Counter (Decrement Counter)**: A counter that can be both incremented and decremented. @@ -22,40 +24,33 @@ Since I working with Dart (for Flutter project), I use a [CRDT library for Dart] 6. **LWW-Register (Last Write Wins Register)**: A register that stores the last written value, using a timestamp to determine the most recent update. 7. **MV-Register (Multi-Value Register)**: A register that stores all values that have been written, using unique identifiers to track writes. -# How it works -- basic version +--- -Main components: +### How It Works -- HLC (Hardware Logical Clock). Combines _wall clock/local time_, a counter that increments, and an optional _node ID_ for uniqueness sake. -- The data itself (usually contains a key, value, and the HLC object). +Two main components: -## **Scenario: Two Devices Synchronizing Data** +- **HLC (Hybrid Logical Clock)** — combines wall clock time, an incrementing counter, and an optional node ID for uniqueness. +- **The record itself** — a key, a value, and the HLC of its last modification. -We have two devices, **Device A** and **Device B**, which both maintain their own local datasets. Each device can modify data independently. When they synchronize, their CRDT implementations will merge their changes and resolve conflicts. +**The scenario:** Device A and Device B both start with the same dataset. They each make changes offline. When they sync, CRDT merges both changesets deterministically. -## Initial State - -- Both devices start with the same data: +**Initial state (both devices):** | Key | Value | isDeleted | Last Modified | | --- | ----- | --------- | ------------- | | 1 | Alice | false | HLC: A1 | | 2 | Bob | false | HLC: A2 | -- Device A's last modified HLC: `A2`. -- Device B's last modified HLC: `A2`. - --- -## Changes Made on Each Device - -1. **Device A deletes Bob's record (key 2):** +**Device A deletes Bob's record (key 2):** | Key | Value | isDeleted | Last Modified | | --- | ----- | --------- | ------------- | | 2 | null | true | HLC: A3 | -2. **Device B updates Alice's name to Alice Smith (key 1):** +**Device B updates Alice's name (key 1):** | Key | Value | isDeleted | Last Modified | | --- | ----------- | --------- | ------------- | @@ -63,68 +58,32 @@ We have two devices, **Device A** and **Device B**, which both maintain their ow --- -## Synchronization and Merge +### Synchronization and Merge -- Device A sends its **changeset** to Device B: +Each device sends only its changeset — the records it modified since the last sync. -| Key | Value | isDeleted | Last Modified | -| --- | ----- | --------- | ------------- | -| 2 | null | true | HLC: A3 | +The `merge` method processes incoming records: -- Device B sends its **changeset** to Device A: +1. **Validate the changeset** — schema check, valid HLC timestamps. +2. **Compare records for key 1 (Alice):** + +Device A has `HLC: A1`. Incoming from Device B: `HLC: B3`. Higher HLC wins — Device A updates Alice to: | Key | Value | isDeleted | Last Modified | | --- | ----------- | --------- | ------------- | | 1 | Alice Smith | false | HLC: B3 | ---- +3. **Compare records for key 2 (Bob):** -## Step-by-Step Conflict Resolution - -The `merge` method processes these changes: - -1. **Validate Changeset**: - Each incoming record is validated to ensure it matches the expected schema and contains valid HLC timestamps. -2. **Compare Records for Key 1 (`Alice`)**: - -| Key | Value | isDeleted | Last Modified | -| --- | ----- | --------- | ------------- | -| 1 | Alice | false | HLC: A1 | - -Incoming record from Device B: -| Key | Value | isDeleted | Last Modified | -| --- | ----- | --------- | ------------- | -| 1 | Alice Smith | false | HLC: B3 | - -**Conflict Resolution Rule**: The record with the higher `Last Modified` HLC wins. HLC `B3 > A1,` so Device A updates Alice's record to: - -| Key | Value | isDeleted | Last Modified | -| --- | ----------- | --------- | ------------- | -| 1 | Alice Smith | false | HLC: B3 | - -3. **Compare Records for Key 2 (`Bob`)**: - -| Key | Value | isDeleted | Last Modified | -| --- | ----- | --------- | ------------- | -| 2 | Bob | false | HLC: A2 | - -Incoming record from Device A: -| Key | Value | isDeleted | Last Modified | -| --- | ----- | --------- | ------------- | -| 2 | null | true | HLC: A3 | - -**Conflict Resolution Rule**: The record with the higher `Last Modified` HLC wins. HLC `A3 > A2`, so Device B updates Bob's record to: +Device B has `HLC: A2`. Incoming from Device A: `HLC: A3`. Higher HLC wins — Device B applies the delete: | Key | Value | isDeleted | Last Modified | | --- | ----- | --------- | ------------- | | 2 | null | true | HLC: A3 | -4. **Propagate Changes**: - Both devices now have identical datasets after merging. +4. **Both devices now have identical datasets.** ---- - -## Final Merged Dataset on Both Devices +**Final merged state:** | Key | Value | isDeleted | Last Modified | | --- | ----------- | --------- | ------------- | @@ -133,11 +92,10 @@ Incoming record from Device A: --- -## Summary of Conflict Resolution Rules +### The Rules -1. **Higher HLC Wins** - Records with higher HLCs (later timestamps) overwrite those with lower HLCs. -2. **Soft Deletes** - A `null` value with `isDeleted: true` is treated as a soft delete. It wins if its HLC is higher. -3. **Deterministic Behavior** - All nodes independently apply the same conflict resolution logic, ensuring eventual consistency. +1. **Higher HLC wins** — later timestamps overwrite earlier ones. +2. **Soft deletes win when newer** — a `null` + `isDeleted: true` record beats an older live record. +3. **Deterministic everywhere** — every node applies the same logic independently and arrives at the same result. + +That's eventual consistency without coordination. The data converges no matter what order the syncs happen in. diff --git a/src/content/blog/fix-adb-unsufficient-permission-linux.md b/src/content/blog/fix-adb-unsufficient-permission-linux.md index 06ced3d..f759a0b 100644 --- a/src/content/blog/fix-adb-unsufficient-permission-linux.md +++ b/src/content/blog/fix-adb-unsufficient-permission-linux.md @@ -8,9 +8,9 @@ tags: - android --- -# Why need this? +## `adb devices` shows `unauthorized`. One udev rule fixes it. -Connecting an Android device to a Linux computer could has problem with permission. Listing devices with `adb devices` might show `unauthorized` status or `insufficient permission` error message. +Plug in an Android device on Linux, run `adb devices`, and sometimes you get this: ```bash $ adb devices @@ -20,11 +20,15 @@ List of devices attached RR2M9002AHY unauthorized ``` -This happens because by default, in Linux, when you connect an Android device via USB, the system assigns it to the `root` user and a restrictive permission mode (often 0600), meaning that regular users cannot access it. +The problem is that Linux assigns USB devices to `root` with restrictive permissions (0600) by default. Regular users can't access them directly, so ADB sees the device but can't talk to it. -# How to fix +The fix is a udev rule that assigns the right permissions when the device is plugged in. -1. Check `vendor id` and `device id` from connected Android device with `lsusb`. +--- + +### Step 1: Find the vendor and product IDs + +Run `lsusb` with the device connected: ```bash $ lsusb @@ -33,21 +37,31 @@ $ lsusb Bus 001 Device 005: ID 18d1:4ee7 Google Inc. Nexus/Pixel Device ``` -`18d1` is _vendor id_ and `4ee7` is the _product id_. +`18d1` is the _vendor ID_ and `4ee7` is the _product ID_. -2. Create an `udev` rule for this device, I create mine in `/etc/udev/rules.d/51-android.rules`. +--- + +### Step 2: Create the udev rule + +Create a file at `/etc/udev/rules.d/51-android.rules`: ```bash SUBSYSTEM=="usb", ATTRS{idVendor}=="18d1", ATTRS{idProduct}=="4ee7", MODE="0666", GROUP="plugdev", SYMLINK+="google_pixel_4a_%n" ``` -- `SUBSYSTEM=="usb"` ensures the rule applies only to USB devices. -- `ATTRS{idVendor}=="18d1" and ATTRS{idProduct}=="4ee7"` match the Google Pixel USB devices by their vendor and product IDs. -- `MODE="0666"` sets the device's permission mode to 0666, meaning read/write access for all users. -- `GROUP="plugdev"` assigns the device to the plugdev group, which allows users in that group to access it. -- `SYMLINK+="google_pixel_4a_%n"` creates a symlink (shortcut) under /dev/ with a readable name for easier identification. +What each field does: -3. Reconnect the device to make sure the rule is correct. If not, try to reload `udev` rules and restart it. +- `SUBSYSTEM=="usb"` — applies only to USB devices +- `ATTRS{idVendor}` and `ATTRS{idProduct}` — matches this specific device +- `MODE="0666"` — read/write access for all users +- `GROUP="plugdev"` — assigns the device to the `plugdev` group +- `SYMLINK+="google_pixel_4a_%n"` — creates a readable symlink under `/dev/` for easier identification + +--- + +### Step 3: Reconnect + +Reconnect the device. If it still doesn't work, reload the udev rules and try again: ```bash $ sudo udevadm control --reload-rules diff --git a/src/content/blog/fix-infinix-air-pro-plus-quad-speakers-linux.md b/src/content/blog/fix-infinix-air-pro-plus-quad-speakers-linux.md index 6b8ee12..c3815f5 100644 --- a/src/content/blog/fix-infinix-air-pro-plus-quad-speakers-linux.md +++ b/src/content/blog/fix-infinix-air-pro-plus-quad-speakers-linux.md @@ -6,12 +6,15 @@ draft: false tags: - linux - hardware - - tweak --- -I installed Linux ([EndeavourOS](https://endeavouros.com/)) in my Infinix Air Pro+ last week and noticed that the sound coming from the speakers was bad. This laptops has 4 speakers, hence only 2 of them are working. This is how I fix this issue. +The Infinix Air Pro+ has four speakers — a tweeter pair and a woofer pair. On Linux, only two of them activate by default. The audio sounds thin as a result. This is how to get all four working. -# Check ALSA for Hidden Speakers +--- + +### Finding the Hidden Nodes + +ALSA sees the hardware but doesn't always know what to do with every output node. Dump the codec info to see what's there: ```bash $ cat /proc/asound/card0/codec* | grep -i "node" @@ -67,24 +70,25 @@ Node 0x0e [Pin Complex] wcaps 0x40778d: 8-Channels Digital Amp-Out CP Node 0x0f [Pin Complex] wcaps 0x40778d: 8-Channels Digital Amp-Out CP ``` -Looking at that output, it seems that this laptop has several nodes that could be an audio output. They are nodes with `Stereo Amp-In Amp-Out` in it's description. Filtering the result with that, I got: +The nodes worth paying attention to: -- 0x14 - Stereo Amp-Out (I suspect this is the front speakers that are working) -- 0x15 - Stereo Amp-Out (likely another speakers?) -- 0x18, 0x19, 0x1a, 0x1b - Stereo Amp-In Amp-Out (might be extra speaker outputs) +- `0x14` — Stereo Amp-Out (the active front speakers) +- `0x15` — Stereo Amp-Out (another output, likely inactive) +- `0x18`, `0x19`, `0x1a`, `0x1b` — Stereo Amp-In Amp-Out (candidates for the extra pair) -# Enable Additional Speakers with `hdajackretask` +--- -`hdajackretask` is a tool from ALSA that allow us to remap/retask those nodes/jack into different purposes. +### Enabling the Extra Speakers with hdajackretask + +`hdajackretask` is an ALSA tool that lets you reassign what each node does. Install it: ```bash $ sudo pacman -S alsa-tools $ sudo hdajackretask ``` -After opening the tool, check the `Show unconnected pins` and there will be list of nodes that can be retasked. -I need to experiment with this remapping. After trials and errors, I found that `0x1a` and `0x1b` is the responsible nodes for my extra speakers. Overriding them and changing their role as `Internal Speaker` solve my issue. +Once open, enable **Show unconnected pins** to see all the nodes that can be retasked. After some trial and error: `0x1a` and `0x1b` are the nodes for the second speaker pair. Override them and set their role to **Internal Speaker**. ![hdajackretask remap nodes](/images/hdajackretask-remap-node.png) -Now all my speakers is working! I hope this will help someone in the future. +All four speakers active. diff --git a/src/content/blog/fix-infinix-air-pro-plus-screen-color.md b/src/content/blog/fix-infinix-air-pro-plus-screen-color.md index f0e92d2..f79971b 100644 --- a/src/content/blog/fix-infinix-air-pro-plus-screen-color.md +++ b/src/content/blog/fix-infinix-air-pro-plus-screen-color.md @@ -7,26 +7,23 @@ tags: - windows - linux - hardware - - tweak --- -I have Infinix Air Pro+ and I use it for my work. I can say it is a good laptop coding mainly because it has 2.5k OLED 16:10 screen. But I found a problem with its screen color. When the screen brightness is below about 50% and the screen turned off (to save power, not necessarily going system sleep/suspend) and turns back on, the color looks washed out. +The Infinix Air Pro+ has a 2.5K OLED panel — one of the reasons I picked it. It also has a firmware quirk: drop the brightness below ~50%, let the display sleep, and when it wakes up the colors look washed out. Flat blacks, muddy shadows. Crank brightness above 50% and everything snaps back. Drop it again and the blacks stay correct. -First time I noticed this issue is because I was using a pitch black wallpaper image (so I can flex my OLED display). After my screen turns back on, my wallpaper's black color becomes grainy, washed out, as its doesn't have pitch black color anymore. Then I noticed, the color will be fixed after I crank the brightness to above 50%. Turning the brightness down again after this still gives me correct black level. +The fix: briefly spike the brightness above 50% on every screen wake, then restore the original level. Two scripts, one trigger — covered for both Windows and Linux. -So, I was wondering if I create a script that will turn the brightness to above 50% and restore it to where it was every time my screen is waking up from a sleep. With a help from Google and ChatGPT, I create these scripts as a workaround for this annoying issue. +--- -# Windows +### Windows -Before continuing, I'm sorry I can't give any screenshot for this Windows section because I already switched to Linux, but I hope I can write it clearly. +The Windows approach uses [Event Viewer](https://learn.microsoft.com/en-us/shows/inside/event-viewer) to catch the screen wake event and [NirCmd](https://www.nirsoft.net/utils/nircmd.html) to control brightness. -## Get screen wake up event +**Catching the screen wake event** -I need to listen to an event that tells me "Hey, the screen is turning on". Fortunately, Windows has [Event Viewer](https://learn.microsoft.com/en-us/shows/inside/event-viewer) that I can use for this. I found that an event from _Kernel-Power_ with event ID _507_ is the correct event that means the screen in turned back on. +The right event is _Kernel-Power_, event ID _507_ — fires when the display turns back on. -## Script - -Next thing to do is create the script to control screen brightness. After trial and error, I found [NirCmd](https://www.nirsoft.net/utils/nircmd.html) can help me to change my screen brightness. Then I create this Powershell script. +**The script** ```powershell # Infinix Air Pro Plus suffers from washed out colors @@ -66,23 +63,21 @@ if ($currentBrightness -lt 50) { } ``` -## Make a schedule +**Scheduling it** -I use Windows' [Task Scheduler](https://www.windowscentral.com/how-create-automated-tasks-windows-11) to run the script each time _Kernel-Power_ with event ID _507_ occurs. I can't show the step-by-step guide because I'm on Linux now, but I have a backup file for this task. All you need is just to import [this task](/misc/Restore%20OLED%20Colors.xml) in Task Scheduler. +Use [Task Scheduler](https://www.windowscentral.com/how-create-automated-tasks-windows-11) to run the script whenever the Kernel-Power 507 event fires. You can import [this task](/misc/Restore%20OLED%20Colors.xml) directly — just update the script path and change the author to `YOUR_PC_NAME\YOUR_USERNAME`. -> Note: You have to change the command it executes to where you save the Powershell script. Also change the author into `YOUR_PC_NAME\YOUR_USERNAME`. +--- -# Linux +### Linux -I'm using [EndeavourOS](https://endeavouros.com/) which use `systemd`. So this guide is applicable to `systemd` init system only. If your linux use something else, you need to adjust it with your init system. +This guide uses `systemd`. Adjust accordingly if you're on a different init system. -## Get screen wake up event +**Catching the screen wake event** -I already tried several ways to listen the screen wake up events. But I can't find any using `acpi` and `udev`. So I tried different approach. I check `dpms` property from screen device in `/sys/class/drm/card1-eDP-1/dpms`. It has `On` and `Off` value that I can use for triggering a script to fix the color. +`acpi` and `udev` didn't yield a reliable screen-on event. The approach that works: poll `/sys/class/drm/card1-eDP-1/dpms`, which switches between `On` and `Off` as the display state changes. -## Script - -I have 2 scripts for this approach. One for checking `/sys/class/drm/card1-eDP-1/dpms` value and another one for fixing the color. +**The scripts** ```bash #!/bin/bash @@ -143,9 +138,9 @@ else fi ``` -## Make a systemd service +**Setting up the systemd services** -Make a `systemd` service in `/etc/systemd/system/brightness-fix.service` to run the first script. +Create `/etc/systemd/system/brightness-fix.service` to run the monitor script: ```plaintext [Unit] @@ -161,7 +156,7 @@ User=user WantedBy=multi-user.target ``` -and another one to run `brightness-fix.sh` after waking up from suspend/sleep, I put it in `/etc/systemd/system/brightness-fix-wakeup.service`. +And `/etc/systemd/system/brightness-fix-wakeup.service` to run the fix after suspend: ```plaintext [Unit] @@ -176,7 +171,7 @@ ExecStart=/usr/local/bin/brightness_fix.sh WantedBy=suspend.target ``` -Then register, enable, and start it. +Enable and start: ```bash sudo systemctl daemon-reload @@ -185,4 +180,4 @@ sudo systemctl enable brightness-fix-wakeup.service sudo systemctl start brightness-fix.service ``` -One more thing, you can add also `/usr/local/bin/brightness_fix.sh` to autostart (I'm using KDE) so it will run each time you login. +Also add `/usr/local/bin/brightness_fix.sh` to autostart so it runs on login — KDE's Autostart settings handle this. diff --git a/src/content/blog/flutter-android-emulator-not-showing.md b/src/content/blog/flutter-android-emulator-not-showing.md index d631fb7..1f12f35 100644 --- a/src/content/blog/flutter-android-emulator-not-showing.md +++ b/src/content/blog/flutter-android-emulator-not-showing.md @@ -8,16 +8,16 @@ tags: - android --- -When I create a new Flutter project targeting Android device, I can't choose which Android device to run it. Either it a real connected devices or AVDs, even the devices is available and listed in `Device Manager` tab. +New Flutter project in Android Studio. Real device plugged in, AVDs configured, everything showing in Device Manager — and the device dropdown in the run toolbar is completely empty. ![no devices showing](/images/no-devices.webp) -All I need to do is open `File > Project Structure...` or `Ctrl + Alt + Shift + S`. +Open **File > Project Structure** (`Ctrl + Alt + Shift + S`). ![project structure window](/images/project-structure.webp) -As you can see, I have no Android SDK selected for my Flutter project. So, go ahead and select one of SDK listed there and click `OK`. That's it! Now you can see all the devices available to run my Flutter project. +No Android SDK is selected for the project. Pick one from the list and click **OK**. ![choose android sdk](/images/choose-sdk.webp) -Hope this can help someone who has similar issues with Flutter project and Android devices. +That's it. All devices appear immediately — no restart required. diff --git a/src/content/blog/flutter-clean-architecture.md b/src/content/blog/flutter-clean-architecture.md index 6807e06..e414b2a 100644 --- a/src/content/blog/flutter-clean-architecture.md +++ b/src/content/blog/flutter-clean-architecture.md @@ -7,22 +7,24 @@ date: 2025-01-12T00:12:04+07:00 draft: false tags: - flutter - - architecture + - craftsmanship --- ![flutter-clean-architecture](/images/flutter_dash.png) -# What is Clean Architecture? +## Structure your Flutter code so it can survive contact with real-world requirements. -Do you ever wondering how to manage your Flutter code? How to make it neat, modular, easy to maintain and test? Here where _clean architecture_ comes in. +Clean Architecture keeps your codebase modular, testable, and maintainable. The layers enforce clear dependency rules: UI depends on business logic, business logic depends on nothing external. When requirements change — and they always do — you change one layer without touching the others. -Basically, clean architecture is a way to organize your code into separated pieces that will make your project cleaner. It may looks complicated at first and a lot of boiler code for some reasons. But trust me, it will be a lot easier if you apply the clean architecture in your code, especially in medium to bigger projects. +It does mean more files upfront. That cost pays off fast once the project grows past a handful of features. -In this set of Clean Architecture articles, we will create a basic mobile app that uses [WeatherAPI](https://www.weatherapi.com/) to get current weather. Let's get started! +This guide walks through a concrete Flutter implementation using [WeatherAPI](https://www.weatherapi.com/). The patterns apply to any app of medium complexity or larger. -> Please note that this guide requires basic knowledge of Dart and Flutter. So I don't recommend going through this guide if you are completely new to the topic. +> Assumes basic familiarity with Dart and Flutter. -# Directory Structure +--- + +## Directory Structure I use this directory structure to organize my code into clean architecture. Once you got the idea, you may modify the structure to match your needs. diff --git a/src/content/blog/immutable-workstation-fedora-kinoite.md b/src/content/blog/immutable-workstation-fedora-kinoite.md index c87bade..117c6d6 100644 --- a/src/content/blog/immutable-workstation-fedora-kinoite.md +++ b/src/content/blog/immutable-workstation-fedora-kinoite.md @@ -83,12 +83,3 @@ PURO_ROOT=/home/user/.puro Running Fedora 43 on my IdeaPad 14AHP10 has been a revelation. Because I’ve only "layered" my shell (Fish) onto the host, the system is incredibly lean. I’ve stopped worrying about my OS. I’m just building things again. - ---- - -### **Final Documentation Summary** - -- **OS:** Fedora 43 Kinoite (KDE Plasma 6) -- **Shell:** Fish -- **Hardware:** IdeaPad 14AHP10 (Ryzen 7 8845HS) -- **Method:** Hybrid-Atomic / Home-run management diff --git a/src/content/blog/kuwot-flutter-daily-quote-app.md b/src/content/blog/kuwot-flutter-daily-quote-app.md index ae80b60..aac31c7 100644 --- a/src/content/blog/kuwot-flutter-daily-quote-app.md +++ b/src/content/blog/kuwot-flutter-daily-quote-app.md @@ -5,7 +5,7 @@ date: 2025-02-20T15:26:04+07:00 draft: true tags: - flutter - - project + - dart --- I made [Kuwot](https://codeberg.org/fiatcode/kuwot-app) last year in my spare time. I want to share it, you can use it as an inspiration, practice app, or something else is up to you since I open-sourced the code (links below). diff --git a/src/content/blog/remap-copilot-key-infinix-air-pro-plus.md b/src/content/blog/remap-copilot-key-infinix-air-pro-plus.md index f85997e..fa7d93f 100644 --- a/src/content/blog/remap-copilot-key-infinix-air-pro-plus.md +++ b/src/content/blog/remap-copilot-key-infinix-air-pro-plus.md @@ -6,7 +6,6 @@ draft: false tags: - linux - hardware - - tweak --- Honestly, even on Windows I never once pressed that Copilot key intentionally. It just sat there, taking up prime keyboard real estate. Moving to Linux gave me the perfect excuse to finally do something about it. diff --git a/src/content/blog/sign-github-commit-on-windows.md b/src/content/blog/sign-github-commit-on-windows.md index 9a82ce8..8411682 100644 --- a/src/content/blog/sign-github-commit-on-windows.md +++ b/src/content/blog/sign-github-commit-on-windows.md @@ -8,14 +8,27 @@ tags: - windows --- -# What's needed +## Every commit, cryptographically yours. -[Git for Windows](https://git-scm.com/install/windows) already shipped with `gpg`. You only need to add `C:\Program Files\Git\usr\bin\` to PATH. +GitHub shows a green "Verified" badge on commits signed with GPG. On Windows, Git for Windows already ships with `gpg` — you just need to expose it and wire up the config. -# How-to +First, add `C:\Program Files\Git\usr\bin\` to your PATH. -- generate gpg key: `gpg --full-generate-key` -- show generated key: `gpg --list-secret-keys --keyid-format=long` +--- + +### Generate and Export the Key + +Generate a new GPG key: + +```bash +gpg --full-generate-key +``` + +List it to get the key ID: + +```bash +gpg --list-secret-keys --keyid-format=long +``` ```plaintext Output: @@ -27,9 +40,17 @@ uid [ultimate] Your Name ssb cv25519/B962022817E5DXXX 2025-04-30 [E] [expires: 2030-04-29] ``` -- export the key to register it to Github account: `gpg --armor --export C50213C2685D0XXX` -- copy the output and add it to Github GPG key in the setting page -- tell git to sign all commits and tags +Export the public key: + +```bash +gpg --armor --export C50213C2685D0XXX +``` + +Copy the output and add it to your GitHub account under **Settings > SSH and GPG keys**. + +--- + +### Configure Git ```plaintext git config --global user.signingkey C50213C2685D0XXX @@ -38,6 +59,4 @@ git config --global commit.gpgsign true git config --global gpg.program "C:\\Program Files\\Git\\usr\\bin\\gpg.exe" ``` -# Important - -don't forget to set `gpg.program` to configure `gpg` executable in global git config just in case you already have another `gpg` installed somewhere else. +Setting `gpg.program` explicitly is important — if you have another `gpg` installed elsewhere on your PATH, Git will find the wrong one. diff --git a/src/content/blog/stop-stashing-use-git-worktree.md b/src/content/blog/stop-stashing-use-git-worktree.md index 0b9bab8..743d75e 100644 --- a/src/content/blog/stop-stashing-use-git-worktree.md +++ b/src/content/blog/stop-stashing-use-git-worktree.md @@ -3,7 +3,7 @@ title: "Stop Stashing. Use Git Worktree." description: "Switching branches to review a PR shouldn't cost you your mental context. It doesn't have to." date: 2026-03-26T00:00:00+07:00 draft: false -tags: ["git", "workflow", "tooling", "dx"] +tags: ["git", "dev-setup"] --- ## Switching branches to review a PR shouldn't cost you your mental context. It doesn't have to. diff --git a/src/content/blog/using-direnv-in-powershell-on-windows.md b/src/content/blog/using-direnv-in-powershell-on-windows.md index 40c2434..6f279b9 100644 --- a/src/content/blog/using-direnv-in-powershell-on-windows.md +++ b/src/content/blog/using-direnv-in-powershell-on-windows.md @@ -8,16 +8,22 @@ tags: - dev-setup --- -# Background +## `.envrc` files shouldn't be a Linux-only thing. -I want a `direnv` functionality in PowerShell running in Windows. Using the [official direnv](https://direnv.net/docs/installation.html) introduces problems because of platform compatibility (Unix-based vs Windows). So I decided to make a PowerShell script to load (and unload) `.envrc` file automatically just like `direnv` does. +`direnv` automatically loads and unloads environment variables when you `cd` into or out of a directory. On Linux and macOS it's a first-class tool. On Windows in PowerShell, the [official direnv](https://direnv.net/docs/installation.html) has platform compatibility issues. -# How to Use +So I wrote a drop-in replacement: a PowerShell script that hooks into the prompt, watches for `.envrc` files, and loads or unloads variables automatically — same behavior, no Unix dependency. -- save the `direnv` script below to `$PROFILE\Scripts\direnv.ps1`. -- add `. "$PSScriptRoot\Scripts\direnv.ps1"` line to `$PROFILE` to load it. +--- -# Script +### Installation + +- Save the script below to `$PROFILE\Scripts\direnv.ps1` +- Add `. "$PSScriptRoot\Scripts\direnv.ps1"` to your `$PROFILE` + +--- + +### The Script ```powershell # PowerShell direnv alternative - Add to $PROFILE diff --git a/src/content/blog/vibe-coding-still-needs-a-craftsman.md b/src/content/blog/vibe-coding-still-needs-a-craftsman.md index 529aee7..8fdc6d0 100644 --- a/src/content/blog/vibe-coding-still-needs-a-craftsman.md +++ b/src/content/blog/vibe-coding-still-needs-a-craftsman.md @@ -3,7 +3,7 @@ title: "Vibe Coding Still Needs a Craftsman" description: "AI agents can write code faster than you ever will. That doesn't mean you can stop thinking." date: 2026-03-25T00:00:00+07:00 draft: false -tags: ["ai", "software-craftsmanship", "tdd", "ddd", "clean-architecture"] +tags: ["ai", "craftsmanship"] --- ## AI agents can write code faster than you ever will. That doesn't mean you can stop thinking. diff --git a/src/content/blog/welcome.md b/src/content/blog/welcome.md index 70178b6..cc81d29 100644 --- a/src/content/blog/welcome.md +++ b/src/content/blog/welcome.md @@ -7,16 +7,12 @@ tags: - misc --- -# Introduction - Hey, I'm FiatCode — a software engineer who's been poking at computers since childhood. It started with games on my dad's office PC (SkiFree, Age of Empires II), then spiraled into learning office software, saving my diary on a floppy disk (encrypted with Webdings font — very secure), flipping through PC magazines, and eventually building my own machine and installing OSes from scratch. Good times. Eventually I took an Information Technology degree, landed my first job right after graduating as a mobile app developer, and have been writing software for seven years since. These days I'm part of a small software house startup. One thing I never did in all that time was write anything down — and I regret it. So here's the blog. I'll write about whatever I find interesting: code, tech, or the occasional random thought. Read and share as you like. -# Bonus - This is a cat from my workplace. Someday she might make a great software tester — sniffing out ~~mice~~ bugs in my code. ![Piko 1](/images/piko-1.webp) ![Piko 2](/images/piko-2.webp) ![Piko 3](/images/piko-3.webp)