Building an Immutable Activity Monitor in Rust
Back to Log
RustSystems EngineeringSQLiteWindows API

Building an Immutable Activity Monitor in Rust

5 min read
Rust

Problem Context

I needed an accountability system that couldn't be manipulated. Most time trackers rely on manual input or can be disabled when convenient. The requirement was stricter: capture all foreground window activity, classify it as productive or distracting, and enforce consequences automatically.

Constraints:

  • Must run continuously without noticeable CPU impact
  • All data stays local (no cloud, no analytics)
  • Once logged, data cannot be edited or deleted
  • The system should intervene when I'm not meeting productivity targets

Initial Approach

The first version used Python with polling every second and stored data in JSON files. This worked for collecting activity but had problems:

What didn't work:

  • JSON files became megabytes within days, slowing down reads
  • Python's startup time made the system service feel sluggish
  • No enforcement mechanism—just passive tracking
  • File-based storage was easy to delete (defeating the immutability goal)

Design Decisions & Trade-offs

Language: Rust

Rust was chosen for:

  • Memory safety without garbage collection: The Windows API polling needs to be reliable
  • Small binary size: The entire release build is <10MB
  • Safe FFI: Interfacing with user32.dll for window handles is straightforward
  • Low resource usage: Runs at <5MB RAM with negligible CPU

The trade-off: Rust's learning curve made development slower initially, but eliminated entire classes of runtime errors.

Architecture: Four-Layer Data Flow

The system processes data through four isolated stages:

1. System Watcher (Raw Observation)

  • Polls GetForegroundWindow() every 10 seconds
  • Writes append-only logs to SQLite
  • No classification logic at this layer—just observation

2. Session Engine (Merge & Classify)

  • Merges raw logs into continuous sessions
  • Applies app classification rules (productive vs distracting)
  • Enforcement logic lives here: If effective work < 2 hours, terminate distracting apps

3. Daily Evaluator (Scoring)

  • Freezes data at end of day
  • Calculates "Effective Work" using:
    Effective Work = Productive Time - (Distracting Time × 0.75)
  • The 0.75 multiplier is subjective but reflects that distraction actively undoes focus

4. Streak Engine (Judgment)

  • Simple boolean: Did I meet the target? Yes → Streak++, No → Streak = 0
  • No partial credit

This separation prevents mixing concerns and makes each layer testable independently.

Database: SQLite with Append-Only Pattern

SQLite was chosen over cloud storage for:

  • Offline operation: No network dependency
  • Atomic writes: Guaranteed consistency even if the process crashes
  • Query flexibility: Can analyze historical patterns with SQL

The append-only pattern ensures immutability. Once a record is written, it's never updated or deleted through the UI.

Schema evolution challenge: When I added "focus session" tracking later, I used SQLite migrations but preserved old data structures. This meant carrying forward schema complexity, but it was worth it to maintain historical integrity.

Implementation Notes

Windows API Polling

The System Watcher uses GetForegroundWindow() to get the active window handle, then GetWindowTextW() to extract the title. This runs on a background thread with a 10-second interval.

Trade-off: 10 seconds was chosen because:

  • 1-second polling felt intrusive (CPU spikes)
  • 30-second polling missed brief app switches
  • 10 seconds captures meaningful activity without overhead

Process Termination for Distracting Apps

When effective work is below the threshold, the Session Engine terminates processes by name using taskkill. This is blunt but effective.

What I would change:

  • This doesn't handle browser tabs running distracting sites (YouTube, Reddit)
  • A smarter approach would hook into browser APIs, but that introduces complexity

UI: Read-Only by Design

The Tauri-based UI only displays data—it cannot mutate the database. This enforces the rule: the system is an observer, not an editor.

Results & Impact

The system has been running daily for three months. It successfully:

  • Captures all activity without user intervention
  • Enforces app termination when I fall below productivity targets
  • Maintains a tamper-proof historical record

What stayed hard:

  • Classifying ambiguous apps (e.g., is "VS Code" always productive?)
  • Handling system updates that restart the service
  • Browser-based distractions remain untracked

Takeaways

This project reinforced that immutability isn't just a data structure property—it's a design constraint that influences architecture. Once I committed to append-only logs, the rest of the system fell into place.

Building enforcement mechanisms taught me that deterrence works best when it's automatic and impartial. The system doesn't negotiate, which is exactly what makes it effective.

For anyone building similar tools: start with the simplest possible data model, make it immutable from day one, and resist the urge to add "undo" features.

Share this article