Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

30 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

leaf

AI-Driven Development

A personal, offline-only markdown notes app for macOS. Notes are plain markdown files with YAML frontmatter, stored in a configurable directory.

leaf has three ways to work with the notes:

  1. TUI: a keyboard-driven terminal UI for full note management
  2. Menu bar (leaflet): a menu bar companion for quick capture and lookup
  3. API: local HTTP API for scripting

Screenshots

leaf TUI

Screenshot

leaflet menu bar

Screenshot

Install

Requires Go 1.26+. leaflet (the menu bar app, below) requires macOS.

make build      # builds bin/leaf and bin/leaflet.app
make install    # installs leaf into ~/.local/bin, leaflet.app into ~/Applications

Usage

Run leaf. Notes live in ~/.local/share/leaf by default (or $XDG_DATA_HOME/leaf). leaf creates the directory on first launch.

Keybindings

All rebindable via config. Defaults:

Key Action
n New note (prompts for title, then opens edit)
e / enter Edit note body
R Rename note (title only, never the file)
p Pin / unpin
t Edit tags (comma-separated)
/ Search
b Backlinks: notes linking to the selected note
r Reload from disk
d Delete (to trash by default)
j/k Move down / up
g/G Jump to top / bottom of the list
ctrl+d/ctrl+u Half page down / up
tab Toggle focus: tags ↔ notes
T Theme picker (live preview)
, Open config file in TextEdit
esc Clear active filter / cancel input
q Quit
? Show all keybindings

The status bar shows only the essential hints. ? opens the full reference, reflecting any rebindings from your config.

In the builtin editor, esc saves and returns to the list, and ctrl+s saves while you keep editing. leaf writes nothing unless the body changed.

Search

Case-insensitive substring matching:

  • tag:work matches notes with exactly that tag
  • title:milk matches the title only
  • linksto:<filename> matches notes linking to that note
  • anything else searches the full text across title, tags, and body

Selecting a tag in the tags pane applies a tag: filter, and esc clears it.

Linking

Write [[Title]] in a note body to link to the note with that title (case-insensitive, and links inside code blocks and code spans don't count). Press b on a note to see everything linking to it. Links point at titles, so renaming a note can leave links dangling. Dangling links are inert text, never an error.

Deletion

d moves a note to .trash/ inside the save directory. It is a folder, so restore by moving the file back, and empty it whenever you like. With permanent_delete = true in config, d deletes immediately after a y/n confirmation.

Note format

Each note is one markdown file with YAML frontmatter:

---
title: Meeting notes
tags: [work, planning]
pinned: true
created: 2026-07-29T14:30:00-07:00
modified: 2026-07-29T15:02:00-07:00
---

## Actual note content starts here...

Plain markdown files without frontmatter work too. They show up titled by filename. leaf generates filenames once, at creation (2026-07-29-1430-meeting-notes.md), and they never change, even when you rename a note.

Config

~/.config/leaf/config.toml (or $XDG_CONFIG_HOME/leaf/config.toml). Press , inside leaf to open it in TextEdit. On first use this creates a fully commented starter file. leaf reads config at startup, so restart after editing.

Every field is optional. Defaults are shown:

# Appearance
appearance  = "system"               # "light" | "dark" | "system"
theme_light = "go-light"
theme_dark  = "go-dark"

# Storage
default_save_dir = "~/.local/share/leaf"

# Filenames
datetime_prefix = true
datetime_format = "2006-01-02-1504"  # Go time layout

# Editing
editor_mode = "builtin"              # "builtin" | "external" ($EDITOR)

# Deletion
permanent_delete = false

# Sorting: pinned notes always float to the top
sort_order = "recency"               # recency | alpha

# Panes
show_tags_pane = true
show_status_bar = true

[keybindings]
new = "n"
edit = "e"
rename = "R"
pin = "p"
tag = "t"
search = "/"
reload = "r"
delete = "d"
quit = "q"
up = "k"
down = "j"
top = "g"
bottom = "G"
half_down = "ctrl+d"
half_up = "ctrl+u"
next_pane = "tab"
config = ","
theme = "T"
backlinks = "b"
help = "?"

With editor_mode = "external", e opens the whole file (frontmatter included) in $EDITOR and re-reads it when you return.

Themes

Built-in: dark and light variants of atom-one, dracula, github, go (the default, inspired by pkg.go.dev), and gruvbox (atom-one-dark, atom-one-light, dracula-dark, dracula-light, github-dark, github-light, go-dark, go-light, gruvbox-dark, gruvbox-light), plus the four Catppuccin flavors: catppuccin-latte (light), catppuccin-frappe, catppuccin-macchiato, and catppuccin-mocha (dark). With appearance = "system", leaf follows macOS light/dark mode (checked at launch and on reload) and picks theme_light or theme_dark accordingly. Reload (r) also re-reads the active theme file, so you can iterate on a custom theme without restarting.

Press T to open the theme picker: / (or j/k) previews each theme live across the whole UI, enter keeps it for this session, esc reverts. leaf never writes the choice to config. After enter the status bar shows the exact theme_light/theme_dark line to add to config.toml to make it stick.

While running, leaf sets the terminal's default background and foreground to match the theme, and restores them on quit, so themes look right regardless of your terminal's own color scheme.

Custom themes go in ~/.config/leaf/themes/<name>.toml and are referenced by name in config (theme_dark = "mytheme"). A user theme with the same name as a built-in shadows it. Every key is optional. leaf derives missing markdown colors from the [ui] block:

[ui]
background    = "#282a36"
foreground    = "#f8f8f2"
border        = "#44475a"
selected_bg   = "#44475a"
selected_fg   = "#f8f8f2"
accent        = "#bd93f9"
pinned        = "#ffb86c"
tag           = "#8be9fd"
status_bar_bg = "#191a21"
status_bar_fg = "#6272a4"

[markdown]
heading          = "#ff79c6"
bold             = "#ffb86c"
italic           = "#f1fa8c"
link             = "#8be9fd"
code             = "#50fa7b"
code_block_theme = "dracula"   # Chroma syntax-highlighting style
blockquote       = "#6272a4"
# ... text, h1–h6, strikethrough, link_text, code_block_bg,
# list_bullet, list_enumeration, table_border, hr, image

Other interfaces

Two optional layers over the same internal/core engine as the TUI, for scripting your notes or capturing one without opening leaf.

HTTP API

leaf serve runs a local JSON API over the same notes. No TUI starts:

leaf serve [--addr 127.0.0.1:7337]

It binds loopback only (non-loopback needs --allow-remote) and has no authentication, so keep it local.

Method & path Action
GET /api/health is it running: {"status", "notes", "dir"}
GET /api/notes?q=&limit= list notes, where q takes the same search syntax as the TUI and limit caps the count
POST /api/notes create: {"title", "body", "tags"}, only title required
GET /api/notes/{filename} one note, body included
PATCH /api/notes/{filename} update any of title, tags, pinned
PUT /api/notes/{filename}/body replace the body
DELETE /api/notes/{filename} trash (or permanent per config)
GET /api/notes/{filename}/links outgoing links, resolved
GET /api/notes/{filename}/backlinks notes linking here
GET /api/tags tag names with counts
POST /api/reload re-scan the save directory
curl -s localhost:7337/api/notes -d '{"title":"From the CLI","tags":["api"]}'

Every note carries an absolute path, so scripts can hand a note straight to open or $EDITOR without redoing the save-dir lookup:

curl -s 'localhost:7337/api/health'
{"status":"ok","notes":42,"dir":"/Users/you/.local/share/leaf"}

curl -s 'localhost:7337/api/notes?limit=1'
[{"filename":"groceries.md","path":"/Users/you/.local/share/leaf/groceries.md", ...}]

Menu bar (leaflet)

leaflet is a small macOS menu bar companion for quick capture and lookup (no terminal needed):

make build            # produces bin/leaf and bin/leaflet.app
open bin/leaflet.app  # or: make run-leaflet

It must run as a .app bundle (make build produces bin/leaflet.app, not a plain binary). The menu bar framework initializes macOS notifications at startup, which requires a real bundle identity. make install puts the bundle in ~/Applications alongside installing leaf to ~/.local/bin.

  • New Note prompts for a title (same convention as n in the TUI), creates the note, and opens it in your default .md app.
  • Open Note is a submenu whose live filter box narrows the note list as you type, using the same search syntax as the TUI (tag:, title:, linksto:, or plain text). Clear it to see the normal pinned/recent list. Clicking a note opens it in your default .md app.
  • Reload re-scans the save directory, and Open leaf folder reveals it in Finder. Start at Login and Quit are provided automatically by the menu bar framework.
  • The status bar icon (cmd/leaflet/assets/icon.svg, FontAwesome's envira glyph) is a template image, so it adapts to light/dark menu bars automatically from the one asset.

It reads your save directory directly via the same internal/core engine as the TUI (no leaf serve process or separate config file required). One new table in config.toml:

[menubar]
notes_limit    = 10     # max items in the live list
show_icon_text = false  # true: note count appears as text next to the icon

Development

make test        # run all tests
make lint        # go vet (+ staticcheck when installed)
make fmt         # gofmt
make modernize   # apply go fix modernizations (any, minmax, rangeint, etc.)

Architecture: internal/core holds all note logic (CRUD, tags, pins, links, search) with zero UI dependencies. internal/store does file IO and frontmatter. internal/config and internal/theme handle TOML loading. cmd/leaf is the Bubble Tea TUI on top, internal/api is the thin HTTP layer behind leaf serve, and cmd/leaflet is a thin macOS menu bar layer over the same internal/core, using menuet.

See docs/design-spec.md for the full design spec.

About

A personal, offline-only markdown notes app for macOS

Resources

Stars

0 stars

Watchers

0 watching

Forks

Contributors

Languages