Gabriel Windlin

guest@gawindlin:~$ whoami

· 2 min read

Building Kiroku-TUI: My First Rust TUI and Why I Needed It

Why I built a terminal-based note manager in Rust, and what I learned along the way.

The Problem

I take a lot of notes. They're mostly markdown files — quick thoughts, daily logs, stuff I want to reference later. The problem is I live in the terminal, and most note apps assume you don't.

I tried Obsidian. It's fine, I guess — the graph view is cool, plugins are everywhere, and I get why everyone recommends it. But I open terminals all day. The idea of clicking out of my workflow just to write a note felt wrong. I wanted something that lived where I already lived.

Kiroku

So I built Kiroku. It's a TUI note manager. Fuzzy search, markdown preview, git sync for backup. No Electron, no GUI, just a binary that runs in any terminal.

The workflow is basically: type to search, arrow keys to navigate, enter to open. Sort of like lazygit meets yazi, but for notes. That's really all I wanted.

Learning Rust

I've been wanting to learn Rust for years. I'd do exercism problems, read the book, build tiny toy programs — and then forget everything a week later. I needed something real to work on.

Kiroku was that thing. It's small enough to finish, but big enough to run into real problems:

  • The borrow checker fought me on the search logic for way too long
  • Working with serde_yaml for frontmatter was straightforward once I figured out the right types
  • ratatui is well-documented but there's a learning curve to immediate-mode rendering

One specific moment: I spent 3 hours on a compiler error that turned out to be a missing &. That's Rust in a nutshell for me so far.

The Stack

  • ratatui — TUI framework. Good docs, active maintenance.
  • fuzzy-matcher — For search. Simple, fast.
  • serde + serde_yaml — Parsing YAML frontmatter

The search code is the part I'm most happy with:

pub fn update_search(&mut self) {
    if self.search_query.is_empty() {
        self.notes = self.all_notes.clone();
    } else {
        let matcher = SkimMatcherV2::default();
        let mut matches: Vec<(&Note, i64)> = self
            .all_notes
            .iter()
            .filter_map(|note| {
                matcher
                    .fuzzy_match(&note.title, &self.search_query)
                    .map(|score| (note, score))
            })
            .collect();
        matches.sort_by(|a, b| b.1.cmp(&a.1));
        self.notes = matches.into_iter().map(|(n, _)| n.clone()).collect();
    }
}

What's Next

I use Kiroku every day now. A few things I want to add:

  • Calendar view for time-based note browsing
  • Better file watcher performance (it chokes on large directories)
  • Homebrew and AUR packages

If you try it and have thoughts, feel free to open an issue. Or don't — it's here if you need it.

Source on Codeberg.