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+dayOffsetwith status values (0for skipped,1for 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:
0indicates skipped/incomplete,1indicates 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
checkedandtoday
POST /habits_v2/{userID}/{habitName}/{yearDay}/{status} (lines 60-104)
- Parses path parameters:
userID,habitName,yearDay(1-365), andstatus(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 viajournal.AddEmoji()andjournal.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.mdmarkdown files - The parser in
server/habits/habits.goconverts these files into sparsemap[string]Yearstructures server/habits/habits_render.gogenerates interactive HTML tables with data attributes for each cell- REST endpoints in
server/sync/webserver.gohandle 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →