HomeAboutPostsTagsProjectsRSS
┌─
ARTICLE
─┐

└─
─┘

Before arriving at a clean, stable setup in #Obsidian, I went through a long phase of experimentation. I tried to make it everything at once — a note-taking tool, an archive library, a task tracker, a project manager, even a reading queue.

Most of those ideas didn’t last. Some sounded clever but created friction in the wrong places. Others blurred the purpose of the tool entirely. Over time, I abandoned what didn’t align with how I think and create.

This post documents those abandoned ideas — the design dead ends that taught me what my Obsidian system shouldn’t be.

Separating the Archive Vault

At first, I kept everything in a single vault with an inbox/ directory for saved articles, videos, and tweets. It seemed efficient, but I quickly noticed I was mixing consumption with creation. My vault bloated with unread material, and it became unclear whether the inbox was a reading queue or an archive.

The easier it was to save things, the less I thought about them — violating my guiding principle that friction is good.

Now I maintain two vaults:

  • Main vault for thinking and note-making
  • ONote-Archive for processed reference materials

Only notes that have passed through active thinking make it to the archive. The main vault stays lightweight and focused, while the archive can grow endlessly without guilt. Two vaults, two purposes.

Quick Capture: Apple Notes Over Obsidian

In the beginning, I captured every fleeting thought directly in Obsidian. It filled quickly with incomplete fragments that blurred the line between brainstorming and structured writing. There was no natural filtering step.

Switching to Apple Notes for quick capture introduced just enough friction to make me pause. I can jot thoughts instantly, review them weekly, and only promote the valuable ones to Obsidian.

This keeps Obsidian intentional — a space for developed ideas, not raw fragments. Apple Notes is my fast capture buffer; Obsidian is where thinking happens.

Projects: Tags Instead of Folders

My first project structure had a projects/ folder with a subdirectory for each project. It looked organized but violated my preference for tags over subfolders.

The result was artificial separation — projects felt isolated from related notes, and my graph view lost context. I was constantly wondering, “Is this in main/ or projects/?”

Now, everything lives in main/, and I tag projects with #project/<name>. This makes projects appear naturally in the graph view and allows a project note to evolve into a general reference by simply removing a tag. It’s flexible, consistent, and matches how I think.

Short-Term Tasks Don’t Belong in Obsidian

For a while, I logged daily tasks in Obsidian using a day/ folder. It didn’t take long before my notes became cluttered with low-value content — grocery lists and quick reminders mixed with long-term ideas.

That’s when I realized: Obsidian is for thinking, not executing.

Now, short-term tasks live in dedicated apps (Apple Reminders, Things, Todoist), while long-term goals and context stay in Obsidian. This separation keeps my knowledge space focused and prevents mental overload.

No Reading Queue in Obsidian

I briefly considered managing a reading queue in Obsidian using Bases, complete with metadata and progress tracking. But it quickly became clear: that approach encouraged hoarding.

It made capturing effortless — and processing rare. Smooth workflows aren’t always better ones. I reminded myself: Obsidian isn’t a task manager or read-it-later app.

Now I rely on external tools or Apple Notes for temporary saves. The archive only grows after I’ve thought about something, not before.

The Core Principle: Friction Is Good

Every decision here reflects one underlying idea:

Friction is a good thing.

Friction between capture and archive prevents hoarding.

Friction between fleeting and permanent prevents clutter.

Friction between reading and saving forces evaluation.

Friction between thinking and executing preserves purpose.

My system evolved by removing convenience where it hurt and adding structure where it helped. Each abandoned idea taught me something about balance — how to build a system that supports deliberate thinking, not just organized information.

┌─
ARTICLE
─┐

└─
─┘

Note

Note format is important, use proper template to provide insights.

Write learning note using your own words, for the mind to digest material.!

DO NOT copy and paste, otherwise you are not learning and thinking, only collecting information.

“If you’re thinking without writing, you only think you’re thinking.”

— Leslie Lamport

Adopt atomic note philosophy. When discussing a new concept, create new note and link back to it. Do not write it inside the note concept, in order to form a network of ideas, instead of a few big notes.

Give context when discussing information, either by using embedded text or adding callout.

Tag and Link

Use tag only as is-a relationship, exercise restraint when tagging.

Use tag can will be reused, not too vague, not too specific.

Use nested tags as a virtual file system

Use link for connecting, it is OK to use HighOrderNote which is just empty note for connecting notes.

A Guide On Links vs. Tags In Obsidian

General suggestions

Do not edit notes in a Wikipedia style, it loses all the points that matter.

Note taking is different from information collection, distinguish between these two, do not only do information collection. proper 2024-06-10-friction-is-a-good-thing|friction is a good thing

write about the why the motivation

┌─
ARTICLE
─┐

└─
─┘

Per-Display Layout Configuration in Yabai Using spacespy

The Problem

When running yabai with multiple displays, you often want different layouts per screen:

  • Built-in laptop display: Stack layout (one window at a time)
  • External displays: BSP tiling (multiple windows side-by-side)

The challenge? yabai -m query --displays gives you display indices but doesn’t tell you which is the built-in screen.

The Solution: spacespy

spacespy is a lightweight macOS utility that provides display information as JSON, including whether a display is “Built-in” or external.

Install from source:

git clone https://github.com/nohzafk/spacespy.git
cd spacespy
make
sudo make install

The Configuration Script

Create ~/.config/yabai/configure-displays.sh:

#!/usr/bin/env bash

# Get display information
spacespy_output=$(spacespy)
displays=$(yabai -m query --displays)

# Configure layout for all spaces on a display
configure_display() {
  local display_index=$1
  local layout=$2

  spaces=$(echo "$displays" | jq -r ".[] | select(.index == $display_index) | .spaces[]")

  for space in $spaces; do
    yabai -m config --space "$space" layout "$layout"
  done
}

# Find built-in display
builtin_display_number=$(echo "$spacespy_output" | jq -r '.monitors[] | select(.name | contains("Built-in")) | .display_number')
all_display_numbers=$(echo "$spacespy_output" | jq -r '.monitors[].display_number')
display_count=$(echo "$displays" | jq 'length')

if [ "$display_count" -eq 1 ]; then
  # Single display - use stack
  configure_display 1 "stack"
else
  # Multiple displays
  [ -n "$builtin_display_number" ] && configure_display "$builtin_display_number" "stack"

  # External displays - use BSP
  for display_num in $all_display_numbers; do
    [ "$display_num" != "$builtin_display_number" ] && configure_display "$display_num" "bsp"
  done
fi

Make it executable:

chmod +x ~/.config/yabai/configure-displays.sh

Event-Driven Reconfiguration

Add to your .yabairc:

# Configure on startup
bash ~/.config/yabai/configure-displays.sh

# Reconfigure when displays change
yabai -m signal --add event=display_added \
  action="bash ~/.config/yabai/configure-displays.sh"

yabai -m signal --add event=display_removed \
  action="bash ~/.config/yabai/configure-displays.sh"

yabai -m signal --add event=display_moved \
  action="bash ~/.config/yabai/configure-displays.sh"

yabai -m signal --add event=mission_control_exit \
  action="bash ~/.config/yabai/configure-displays.sh"

Why This Works

  1. Automatic: Connect/disconnect displays → layouts reconfigure automatically
  2. Clean separation: spacespy handles display detection, yabai handles window management
  3. SIP-compatible: Works with System Integrity Protection enabled
  4. Simple: One script, a few signal handlers

The Result

On the go (laptop only) → stack layout maximizes limited screen space At my desk (external monitors) → BSP tiling on big screens, stack on laptop

The transition happens automatically. No manual intervention needed.

The key insight: spacespy provides the missing piece—identifying which display is built-in. Once you know that, per-display layout configuration becomes trivial.

┌─
ARTICLE
─┐

└─
─┘

2025-11-11-checkpointing-conversations-with-claude

When I’m deep in a long Claude session, I use what I call a checkpoint strategy. It’s basically a conversational anchor.

Let’s say Claude throws out a list of ideas or questions. Before diving into one of them, I’ll drop a quick line like:

Use this as Checkpoint A for our conversation — when we come back, track the decisions made.

That way, I can explore one branch in depth, make a few choices, then jump back to the checkpoint and pick a different path — without losing the thread of what’s already been decided.

It’s like branching in Git, but for chat. Each checkpoint keeps the flow organized while I experiment with different directions.

┌─
ARTICLE
─┐

└─
─┘

In django rest framework, a ViewSet class can have an authentication_classes with multiple authentication backend and a permission_classes with multiple permission backends.

For the authentication backends, it is a OR relation, meaning one authentication backend returns None, the framework will try next backend.

But for permission backends, it is a AND relation, meaning all permission backends must return True to indicate that the request passes the permission check. It is a common mistake to assume that permission_classes behave like authentication_classes.

┌─
ARTICLE
─┐

└─
─┘

Notes about Django migration generated SQL

I have recently transitioned a service from Flyway to Django-migration based database management. To ensure a smooth data migration process, I need to verify that the Django-migrations generated DDL is compatible with the existing one.

pytest-django: how to create empty database for test cases

I am using pytest with the pytest-django plugin to write unit tests that compare the generated raw SQLs. I have two test cases, both of them start with empty database, one test case executes the Flyway migration, the other test case applies Django migrations. If both test cases pass the same assertions of the database, for example database contains certain tables, indexes, enum type, constraints, etc.), I can be confident about the Django migration files.

The issue is that pytest-django creates a test database instance with Django migrations executed by default. The –no-migrations option does not create an empty database instance. Instead, it disables Django migration execution and creates tables by inspecting the Django models.

I would like pytest-django to have an option to disable Django migration execution, allowing for an empty database instance to be created. This would enable me to test the compatibility of my Django migration files more effectively.

Solution

The solution is to use a custom django_db_setup fixture for the test cases.

@pytest.fixture
def django_db_setup(django_db_blocker):
    """Custom db setup that creates a new empty test db without any tables."""

    original_db = connection.settings_dict["NAME"]
    test_db = "test_" + uuid.uuid4().hex[:8]

    # First, connect to default database to create test database
    with django_db_blocker.unblock():
        with connection.cursor() as cursor:
            print(f"CREATE DATABASE {test_db}")
            cursor.execute(f"CREATE DATABASE {test_db}")

    # Update connection settings to use test database
    for alias in connections:
        connections[alias].settings_dict["NAME"] = test_db

    # Close all existing connections
    # force new connection to be created with updated settings
    for alias in connections:
        connections[alias].close()

    yield

    # Restore the default database name
    # so it won't affect other tests
    for alias in connections:
        connections[alias].settings_dict["NAME"] = original_db

    # Close all existing connections
    # force new connection to be created with updated settings
    for alias in connections:
        connections[alias].close()

Django generated foreign key with deferrable constraints

While comparing the generated DDL, i noticed that in Django-generated DDL, foreign key constraints has a DEFERRABLE INITIALLY DEFERRED. This constraint means checking is delayed until transaction end.

It allows temporary violations of the foreign key constraint within a transaction, this can be helpful for inserting related records in any order within a transaction.

Django’s ORM is designed to work with deferrable constraints:

  • It can help prevent issues when saving related objects, especially in complex transactions
  • Some Django features (like bulk_create with related objects) work better with deferrable constraints

No Downside for Most Applications:

  • Deferrable constraints still ensure data integrity by the end of each transaction
  • The performance impact is typically negligible
  • If a constraint must be checked immediately, it can still enforce it at the application level

So I keep the Django-generated foreign key constraints and consider following two are equivalent

FOREIGN KEY (manufacturer) REFERENCES organizations(id)

FOREIGN KEY (manufacturer) REFERENCES organizations(id) DEFERRABLE INITIALLY DEFERRED
┌─
ARTICLE
─┐

└─
─┘

To my surprise when you search for gleam read file in google, they are not much helpful information in the first page and no code example.

There are a post in Erlang Forums where the author of Gleam language pointed to a module that no longer exists in gleam_erlang pacakge, and a abandoned pacakge call gleam_file, and a couple pacakges like simplifile.

It turns out that Gleam has excellent FFI that if you are running it on BEAM (which is the default option unless you compile Gleam to javascript), for a simple case you just need to import the function from erlang, in just two lines of code.

@external(erlang, "file", "read_file")
fn read_file(file_path: String) -> Result(String, Nil)

and you can use it as a normal function to read the file content into a string.

pub fn read_file_as_string(path: String) {
  use content <- result.try(
    read_file(path)
    |> result.map_error(fn(_) { "Failed to read file: " <> path }),
  )
  content
}
┌─
ARTICLE
─┐

└─
─┘

2025-02-19-django-grpc-workflow

Django-socio-grpc (DSG) is a framework for using gRPC with Django. It builds upon django-rest-framework (DRF), making it easy for those familiar with DRF to get started with DSG.

Although I decided to go back to DRF after exploring DSG, I chose to do so because I needed to get things done quickly. Using gRPC is considered a potential way to achieve performance gains, and there are some obstacles need to be addressed before going full gRPC. I’m leaving these notes as my learning experience.

The workflow

Using django-socio-grpc (DSG) the workflow is like following:

flowchart TD
  subgraph backend[Backend Side]
	  db-schema[Design Database Schema] --> |create| django-models[Django Models] --> |define| serializers
	  pb-backend[Protocol Buffers]	  
	  django-models --> server-app[Server Application]
	  django-models --> |migrate| database
  end

  subgraph protobuf-repo[Protobuf repository]
	  .proto[.proto] --> |run| buf-lint[buf lint] --> |detect| buf-check[Breaking Change] --> |if pass|protobuf-gen[Generate Code]
	  protobuf-gen --> server-stub[gRPC Server skeleton]
	  protobuf-gen --> client-stub[gRPC Client Stub]
	  protobuf-gen --> mypy-types[.pyi type stub]
  end

  subgraph frontend[Frontend Side]
	  pb-frontend[Protocol Buffers]
		client-app[Client Application] --> |call| client-stub
  end

  serializers --> |DSG generates| .proto

  server-app --> |implement| server-stub
  server-stub --> |serializes| pb-backend

  mypy-types --> server-app

  client-stub --> |serializes| pb-frontend
  pb-backend <--> |binary format over HTTP/2| pb-frontend

Make DSG aware of big integer model fields

Currently DSG (version 0.24.3) has an issue of mapping some model fields to int32 type in protobuf incorrectly due to DRF’s decision, they should be mapped to int64 type.

It’s kind of hard to implement at library level, in application level I implemented something like this, using BigIntAwareModelProtoSerializer as the parent class of the proto serializer will correctly map BigAutoField BigIntegerField PositiveBigIntegerField to int64 type in protobuf.

from django.db import models
from django_socio_grpc import proto_serializers
from rest_framework import serializers

class BigIntegerField(serializers.IntegerField):
    """Indicate that this filed should be converted to int64 on gRPC message.

    This should apply to
    - models.BigAutoField.
    - models.BigIntegerField
    - models.PositiveBigIntegerField

    rest_framework.serializers.ModelSerializer.serializer_field_mapping
    maps django.models.BitIntegerField to serializer.fields.IntegerField.

    Although the value bounds are set correctly, django-socio-grpc can only map it to int32,
    we need to explicitly mark it for django-socio-grpc to convert it to int64.
    """

    proto_type = "int64"

class BigIntAwareModelProtoSerializer(proto_serializers.ModelProtoSerializer):
    @classmethod
    def update_field_mapping(cls):
        # Create a new mapping dictionary inheriting from the base
        field_mapping = dict(getattr(cls, "serializer_field_mapping", {}))

        # Update the mapping for BigInteger fields
        field_mapping.update(
            {
                models.BigIntegerField: BigIntegerField,
                models.BigAutoField: BigIntegerField,
                models.PositiveBigIntegerField: BigIntegerField,
            }
        )

        # Set the modified mapping
        cls.serializer_field_mapping = field_mapping

    def __init_subclass__(cls, **kwargs):
        super().__init_subclass__(**kwargs)
        cls.update_field_mapping()

    """A ModelProtoSerializer that automatically converts Django BigInteger fields to gRPC int64 fields by modifying the field mapping."""

Major obstacles for using gRPC

The biggest obstacle is that browsers do not natively support gRPC, which relies on HTTP/2.0. As a result, client-side frontend calls to backend services from a browser using gRPC require a proxy, typically Envoy. This setup involves additional overhead, such as configuring a dedicated API gateway or setting up an ingress. Even with a service mesh like Istio, some extra work is still necessary.

The next challenge is how to corporate with existing RESTful services if we chose to add a gRPC service. For communications happen between RESTful service and gRPC service, a gRPC-JSON transcoder (for example Enovy ) is need so that HTTP/JSON can be converted to gRPC. Again some extra work is needed at infrastructure level.

The last part of using gRPC is that data is transferred in binary form (which is the whole point of using gRPC for performance) makes it a little bit harder for debugging.

Conclusion

Django-socio-grpc is solid and its documentation is good. However, the major issue is the overhead work that comes with using gRPC. I will consider it again when I need extra performance and my team’s tech stack is adapted to gRPC.

┌─
ARTICLE
─┐

└─
─┘

Gleam language: how to find the min element in a list

Gleam language standard library has a list.max to find the maximum element in a list, but to my surprise it doesn’t provide a counterpart list.min function, in order to do that, you have to use compare function with order.negate

import gleam/list
import gleam/int
import gleam/order

pub fn main() {
  let numbers = [5, 3, 8, 1, 9, 2]
  
  // Find the minimum value using list.max with order.negate
  let minimum = list.max(numbers, with: fn(a, b) {
    order.negate(int.compare(a, b))
  })
  
  // Print the result (will be Some(1))
  io.debug(minimum)
}

Another noteworthy aspect is that when a list contains multiple maximum values, list.max returns the last occurrence of the maximum value. This behavior contrasts significantly with Python’s list.max, which returns the first occurrence in such cases. I observed this discrepancy while comparing different implementations in both languages.

  partitions
  |> list.max(fn(a, b) {
    float.compare(a |> expected_entropy, b |> expected_entropy)
    |> order.negate
  })

In the code snippet provided, the result will be the last element in the list that has the minimal entropy value.

┌─
ARTICLE
─┐

└─
─┘

This git principle advocates for a workflow that balances a clear main branch history with efficient feature development.

1. Merge into main:

  • Purpose: Keeps the main branch history clean and linear in terms of releases and major integrations.
  • How it works: When a feature is complete and tested, it’s integrated into main using a merge commit. This explicitly marks the point in time when the feature was incorporated.
  • Benefit: main branch history clearly shows the progression of releases and key integrations, making it easier to track releases and understand project evolution.

2. Rebase feature branches:

  • Purpose: Maintains a clean and linear history within each feature branch and simplifies integration with main.
  • How it works: Before merging a feature branch into main, you rebase it onto the latest main. This replays your feature branch commits on top of the current main, effectively rewriting the feature branch history.
  • Benefit:
    • Linear History: Feature branch history becomes a straight line, easier to understand and review.
    • Clean Merges: Merging a rebased feature branch into main often results in a fast-forward merge (if main hasn’t advanced since the rebase), or a simpler merge commit, as the feature branch is already based on the latest main.
    • Avoids Merge Bubbles: Prevents complex merge histories on feature branches that can arise from frequently merging main into the feature branch.

In essence:

  • main branch: Preserve a clean, chronological, and release-oriented history using merges.
  • Feature branches: Keep them clean and up-to-date with main using rebase to simplify integration and maintain a linear development path within the feature.

Analogy: Imagine main as a clean timeline of major project milestones. Feature branches are like side notes. Rebase neatly integrates those side notes onto the main timeline before officially adding them to the main history via a merge.