How Zakirullin Files Tracks Habits: A Year-Long Markdown Matrix Approach

Zakirullin Files stores habits as a year-long emoji matrix in markdown files, parsing them into an in-memory map that powers a real-time web interface with GET and POST endpoints.

Zakirullin Files is a personal knowledge management system that implements habit tracking through a novel markdown-based architecture. Instead of relying on a traditional database, the zakirullin/files.md repository stores daily habit completions as emoji strings in yearly markdown files located in the Insights directory. This design enables version control, simple text editing, and fast in-memory querying through a sparse map structure.

Storage Format and Parsing Logic

The habit tracking system persists data in ~/Insights/<year> Habits.md files (for example, 2024 Habits.md). Each file contains a markdown matrix where habits are represented as lines of emojis followed by the habit name.

The file format uses month headers and emoji sequences:


### January

🟢⚪️🟢🟢  Went to gym
⚪️⚪️⚪️⚪️  Read book

### February

🟢🟢⚪️🟢  Went to gym

To parse this format, server/habits/habits.go implements the Habits() function which performs the following steps:

  • Lists existing habit definitions using userFS.FilesAndDirs(fs.DirHabits) to determine valid habit names
  • Reads the yearly insights file via userFS.Read(fs.DirInsights, filename)
  • Detects month headers with strings.HasPrefix(line, "###") to calculate day offsets correctly
  • Parses mood lines specially when strings.Contains(habit, MoodHabit) to store emoji power levels
  • Parses regular habit lines mapping each emoji position to dayOfTheYear+dayOffset with status values (0 for skipped, 1 for completed)

In-Memory Data Structure

Once parsed, the system stores habits in a sparse map structure defined as map[string]Year, where Year is map[int]int (day-of-year → status). This means only days with recorded activity occupy memory, making the representation efficient for year-long tracking.

The type definitions function as follows:

  • Key: Habit name string (e.g., "Went to gym")
  • Value: Map of integer day-of-year (1-365/366) to integer status
  • Status values: 0 indicates skipped/incomplete, 1 indicates completed, with special handling for mood emojis

Rendering the Weekly Habit View

The habits.Render() function in server/habits/habits_render.go generates the user interface by calling LastWeekHabits() to extract a 7-day slice of the yearly data. It then injects this data into the embedded template located at server/habits/templates/habits.html.

The template constructs an HTML table where each cell contains data attributes for interactivity:

<td data-habit="{{$habit}}" data-day="{{$day}}" data-status="1"
    class="day {{ if eq $status 1 }}checked{{ end }} {{ if eq $.currentDay $day }}today{{ end }}">
    {{ if $firstLine }}<div class="text">{{$habit}}</div>{{ end }}
</td>

These data-* attributes enable the JavaScript layer to identify which habit and day to update when users click specific cells.

API Endpoints for Real-Time Updates

The server/sync/webserver.go file exposes two HTTP endpoints that coordinate the read/write cycle between the frontend and the markdown storage.

GET /habits_v2/{userID} (lines 33-57)

  • Constructs a user-specific filesystem abstraction using server/fs/fs.go
  • Calls habits.Render() to generate the HTML table for the current week
  • Returns the rendered template with embedded CSS classes checked and today

POST /habits_v2/{userID}/{habitName}/{yearDay}/{status} (lines 60-104)

  • Parses path parameters: userID, habitName, yearDay (1-365), and status (0/1)
  • Loads the user's habit map using habits.Habits()
  • Validates the habit exists: if _, ok := userHabits[habitName]; !ok { ... }
  • Updates the specific day: userHabits[habitName][int(yearDay)] = int(status)
  • Persists changes via habits.Write() back to the yearly markdown file
  • Resolves the appropriate emoji using habits.Emoji() and appends to the daily journal via journal.AddEmoji() and journal.AddRecord()

Frontend Interaction Flow

The rendered page in habits.html includes client-side JavaScript that captures click events on table cells. When a user toggles a habit completion, the script extracts the data-habit, data-day, and data-status attributes, then issues a POST request:

fetch(`https://{{.host}}/habits_v2/{{.userID}}/${habit.habit}/${habit.day}/${habit.status}`, {
    method: "POST"
});

The UI updates immediately by toggling the checked CSS class, while the server asynchronously writes the change to the markdown file and updates the journal.

Practical Code Examples

Reading a User's Full Habit Map

fs, _ := fs.NewUserFS(userID)
habitsMap, err := habits.Habits(fs, 2024) // Returns map[string]habits.Year
if err != nil { 
    log.Fatal(err) 
}
fmt.Println(habitsMap["Went to gym"][150]) // Status for day 150 (May 30)

Updating a Habit via HTTP

curl -X POST "https://api.example.com/habits_v2/12345/Went%20to%20gym/200/1"

Where 200 represents the day-of-year, 1 marks completion, and the server writes the change back to 2024 Habits.md while logging to the journal.

Rendering the Weekly View

<div id="habits">
  {{template "habits.html" .}}
</div>

The template receives a map structure where each column represents a day of the current week relative to the user's configured timezone.

Summary

  • Zakirullin Files stores habits as emoji matrices in ~/Insights/<year> Habits.md markdown files
  • The parser in server/habits/habits.go converts these files into sparse map[string]Year structures
  • server/habits/habits_render.go generates interactive HTML tables with data attributes for each cell
  • REST endpoints in server/sync/webserver.go handle reads (GET) and updates (POST) without requiring a database
  • Changes persist back to markdown while simultaneously updating the journal system via journal.AddEmoji()
  • The frontend uses vanilla JavaScript to toggle statuses and synchronize with the server in real-time

Frequently Asked Questions

How does Zakirullin Files physically store habit data?

The system writes habits to yearly markdown files (e.g., 2024 Habits.md) in the Insights directory using emoji characters to represent daily status. Each line contains a string of emojis followed by the habit name, with month headers (### January) providing structural boundaries for parsing day calculations.

What happens when I click a habit cell in the web interface?

The client-side JavaScript in templates/habits.html reads the data-habit, data-day, and data-status attributes from the clicked <td> element, toggles the status value, and sends a POST request to /habits_v2/{userID}/{habitName}/{yearDay}/{status}. The server then updates the in-memory map and rewrites the entire yearly markdown file via habits.Write().

Can I edit habit files manually without breaking the parser?

Yes, because server/habits/habits.go parses the file line-by-line looking for emoji sequences and month headers. You can manually add emojis or edit existing ones using any text editor; the parser recalculates day offsets based on the ### <Month> headers and emoji positions, making the system compatible with standard git workflows and manual edits.

How does the system handle different years and timezones?

The Habits() function accepts a year parameter (e.g., habits.Habits(fs, 2024)) to load the specific file, while LastWeekHabits() uses cfg.Timezone() to calculate the correct 7-day window for display. The day-of-year integer (1-366) serves as the universal index across both storage and API layers, ensuring consistent addressing regardless of client timezone.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →