dgrco.dev

Tolari

A local-first desktop productivity app for students. No account, no sync, no server — everything lives on your machine. Built with Wails, which lets you write a Go backend and a React frontend that talk to each other through a generated bindings layer, compiled down to a single native binary.

The premise is that three well-researched study techniques — spaced repetition, Kanban planning, and the Pomodoro technique — are most useful when they're part of the same workflow rather than three separate tools you switch between. Tolari puts them in one place.

Stack

The backend is Go, exposed to the frontend via Wails bindings. The frontend is React with Vite. Flashcard data is stored in SQLite. Kanban state is persisted as structured data files on disk. The whole thing ships as a single executable — no installer needed on most platforms.

Wails works by embedding a WebView in a native window and injecting a JS bindings layer that maps directly to exported Go functions. From React's perspective, calling a Go function looks like calling an async JS function:

// Go — exported to the frontend automatically by Wails
func (a *App) GetReviewCards() ([]Flashcard, error) {
	rows, err := a.db.Query(`
		SELECT * FROM flashcards
		WHERE review_date < CURRENT_TIMESTAMP;
	`)
  ...
}
// React — generated binding, called like any async function
const cards = await GetReviewCards();

No REST API, no HTTP, no serialization boilerplate. Wails handles the bridge.

Spaced repetition flashcards

The flashcard system is modelled after Anki — cards are reviewed on a schedule determined by how well you recalled them, not just how recently you studied them. The core idea is that memory decays predictably, and the optimal time to review something is just before you'd forget it. Reviewing too early wastes time; reviewing too late means relearning from scratch.

Each card has an interval (days until next review) and an ease factor (a multiplier that grows when you recall well and shrinks when you struggle). After each review you rate your recall, and the next interval is calculated:

FUNCTION review(flashcard, difficulty):
    // Get the current time
    current_time = GET_CURRENT_TIME()

    // If response was incorrect/poor (difficulty < 3)
    IF difficulty < 3 THEN
        flashcard.repetitions = 0
        flashcard.interval = 1
        
    // If response was correct (difficulty >= 3)
    ELSE
        // Calculate new Easiness Factor (EF) -> from the SM2 algorithm
        flashcard.easiness = flashcard.easiness + 0.1 - 
          (5 - difficulty) * (0.08 + (5 - difficulty) * 0.02)
        
        // Keep Easiness Factor from dropping below the minimum threshold of 1.3
        IF flashcard.easiness < 1.3 THEN
            flashcard.easiness = 1.3
        END IF

        // Increment repetitions and determine the next interval
        flashcard.repetitions = flashcard.repetitions + 1
        
        CHOOSE CASE flashcard.repetitions:
            CASE 1:
                flashcard.interval = 1
            CASE 2:
                flashcard.interval = 6
            DEFAULT:
                flashcard.interval = ROUND(flashcard.interval * flashcard.easiness)
        END CHOOSE
    END IF

    // Calculate and save the next review date
    flashcard.review_date = SERIALIZE_DATE(current_time + flashcard.interval DAYS)
END FUNCTION

Cards are stored in SQLite with their next due date. At startup, the app queries for all cards due today and surfaces them for review. Once you've cleared your due cards, you're done — no pressure to do more.

Kanban board

The Kanban board is for planning study sessions and tracking tasks across subjects. Columns represent stages (To Do, In Progress, Done) and cards move between them as work progresses.

State is persisted as structured data files on disk rather than in SQLite. The board is its own file. The Go backend handles reads and writes, and the React frontend sends the full updated board state on every change rather than patching individual fields. Simple and easy to reason about.

Pomodoro timer

The Pomodoro timer is the glue between the other two features. The technique is straightforward: work in focused 25-minute blocks, take a short break, repeat. After four blocks take a longer break. The structure combats the tendency to study in unfocused marathon sessions.

Why local-first

Most productivity apps want an account. That means your data lives on someone else's server, the app breaks without internet, and you're one subscription cancellation away from losing access. For a study tool used daily by students who might be on a school network, a plane, or just don't want to hand over an email address, local-first is the right default.

SQLite and flat files on disk means your data is yours — readable, backable-up, and not going anywhere.