English | 简体中文
Chinese name: 拾枝.
A local-first personal life database: capture with #tags, search, filter, and export to Excel.
Everything lives in one stream of notes — diary entries, films, novels, TV shows, todos, travel
notes and plans. There are no per-domain modules: each note is a single record, and the #tags
inside its text classify it automatically.
- Background
- Features
- Install
- Usage
- Architecture
- Data and storage
- Development
- Tech stack
- Project conventions
- License
A personal database for everyday notes should be instant to write into and easy to search — not a filing system you have to maintain. Most open-source note servers either trade startup speed for a web stack (Electron, server + browser client) or split "capture" and "retrieval" into features you have to switch between.
This project takes the opposite approach:
- Capture is a search bar. A global hotkey opens a frameless, always-on-top input box — one
input field, no title bar, no buttons; type, press
Ctrl+Enter, and it is stored. No window switching, no category to pick first. - Organisation is derived, never declared. No folders, no note types, no diary/film/todo
modules — one stream plus
#tagsparsed out of the text. - Retrieval is filtering. Full-text search (SQLite FTS5 with the
trigramtokenizer, so Chinese text and mid-word matches work), tag filters with counts, and ordering. - Everything stays local. One SQLite file under your user profile. No account, no sync service, no network calls.
- Excel is the escape hatch. The whole database exports to a single
.xlsx, so the data outlives the application.
Built on Tauri 2, so the executable is around 12 MB and the installer around 3 MB, with no bundled browser engine (it uses the system WebView2).
Input bar
The window is the input box: no frame, no title bar, no buttons, transparent square corners.
- Toggled by the global hotkey
Ctrl+Shift+Q(falls back to the tray menu if the hotkey is taken) - The input fills the whole window: one line when empty, growing with the content up to five lines, then scrolling inside
- A glow instead of a frame — idle: 1 px
rgba(0,0,0,0.12)border with a0 2px 10pxsoft shadow; focused: 2 pxrgba(59,130,246,0.65)blue border with a0 0 12pxblue glow - The outermost 8 logical pixels are a drag band, so the window can be moved from any edge; the left and right edges instead drag the width (240–900 logical pixels, and never more than 80% of the screen work area)
- Sticker mode by default: it stays on top and does not hide when it loses focus, so it can be left open while you work (switch it back to hide-on-blur in the settings page)
Esc, or a double-click on a drag band, hides itCtrl+Entersaves: the input clears,已保存 HH:MMflashes in grey for 1.5 s at the bottom right, and the caret stays in the input for the next entry- Typing
#shows tag suggestions the way a browser search box shows its recommendations: the list appears directly under the input (the window grows to fit it and shrinks back when it closes);Up/Downto choose,Enter/Tabto accept,Escto close just the list. Entries are marked as an alias (an old name that still points at the canonical tag) or as similar (an existing lookalike, reuse it instead of creating a new tag). The list height is never remembered - Mouse-wheel zoom from 50% to 200% with no modifier key;
Ctrl+ wheel changes the opacity from 30% to 100%; middle-click restores 100% zoom and the configured default opacity - Three locks — block moving, block closing, lock content — each switchable in the settings page; while any lock is on, a padlock appears in the top-right corner and unlocks all three in one click
- Remembers position, size, zoom and opacity across restarts
Main window
- A single column: the unified input box, a condition bar (chips plus a one-line summary) and the note stream
- Filtering is one set of conditions whichever the entry point: pick tags in the sidebar (or click a
#tagchip inside a note), add conditions in the condition bar, or type a/keyword in the input box; it survives a restart. Ordering newest-first or oldest-first takes one command each (reachable from>or from the top bar's⋯menu); the stream pages 50 notes at a time and there is no date-range filter - Time tags are ordinary tags:
时间/YYYY/MM/DDlives in the tag tree like any other tag, so browsing a day means clicking that day's tag (rename, move or delete it freely) - The tag tree can be reorganised by dragging (tree and flat mode alike): dropping on the middle of a row nests the tag under it; dropping on the boundary between two rows makes it a sibling — an insertion line appears at the boundary and its indent shows the level you are about to land on; dropping on empty space moves it back to the root. The context menu offers rename / move / delete, and sibling order is remembered
- The tag context menu offers rename / move / alias / merge. An alias records which tag a name
points at (old names and short forms normalise automatically, so writing
#old-namestill lands on the renamed tag; aliases can be added and removed in the menu). Merge folds tag A into tag B: links move over, the old name is kept as an alias by default, and the current filter is rewritten — only leaf tags can merge - Parent tags are clickable: clicking
电影,信息or日期/2026filters every descendant (a child implies its parent, so notes do not need redundant parent tags) - Note cards split chips into two rows: topic (domain / genre / region) and attribute (status / origin /
channel / platform / author nationality / date); each row collapses to
+Npast its threshold (6 / 4) and expands on click - The editor shows the tag count and, above five, a gentle 3-5 hint; saving is never blocked- Inline editing in a VSCode-style split pane: Markdown source on the left, live preview on the
right,
Ctrl+Enterto save - Deleting asks for confirmation
- A gear in the top bar opens an inline settings page (the stream stays mounted behind it, so returning neither re-queries nor loses the scroll position): the nine input-bar options, the notes section (whether new notes get a time tag automatically, and the template that decides the tag path), a startup section (autostart with the real registry state and a repair button, plus what to show on launch), a hotkeys section (the system-wide input hotkey plus the two in-app ones below), and a general section with the version, the database path, an "open containing folder" button and a reset for the input-bar section
Unified input box and quick open
Ctrl+Shift+Pfocuses the unified input box with>(commands) andCtrl+Pwith@(open a note); both are in-app hotkeys (nothing is registered system-wide) and can be re-recorded in the settings page, with a Chinese conflict message when a combination clashes with the global input hotkey or with the other in-app one- One input box, four prefixes:
/filters the stream as you type,#picks a tag,@opens a note,>runs a command; without a prefix it searches notes. The sidebar's 筛选标签 button and both hotkeys only prefill the box, so there is no second search field anywhere - Ranking and the
<mark>highlight come from the same fuzzy scorer that the input bar's#suggestions use, so the row that ranks first is the row that is highlighted - A command's visibility and its checked state are data (
when/toggled), not component branches: a hidden command is absent from the DOM, and隐藏侧栏becomes显示侧栏while the sidebar is hidden - Twelve commands: new note, open settings, cycle the theme, hide the sidebar, focus mode, newest first, oldest first, add a condition, export everything, rebuild the search index, edit the global hotkey, quit
- Sorting, exporting and adding a condition also have a mouse entry: the
⋯menu in the top bar, which marks the active sort with a check mark and shows the export feedback next to it. The condition bar itself carries nothing but the active chips and the summary line - Notes and commands each keep a most-recently-used list in the settings table
- Accepting a command first flushes the open editor, so switching views never drops unsaved text
Notes
#tagsare extracted from the text and stripped from the body, then stored in a tag table with link rows, so a tag rename or an edit updates the associations- Markdown rendering (tables, task lists, fenced code with syntax highlighting) through a single sanitising entry point
- Completion is expressed in the note body as a Markdown task list (
- [ ]/- [x]): the checkboxes are rendered in the note stream and can be ticked right there - a tick rewrites the body (- [ ]<->- [x]) and writes every tag of the note back with it; there are nodone/doingtags and no per-note inline checkbox - The note stream shows no time at all: the only time-like information is the ordinary
时间/YYYY/MM/DDtag tree - There are no saved views and no multiple filter snapshots: classification is done with tags alone, and the current filter is a single piece of state
Export
- "Export the whole library" writes the entire database to one
.xlsxsheet with four columns:id, body, creation time, and reference path; creation time is the note's diary day, not the moment it was imported
Download the installer (*-setup.exe) from the
releases page and run it; it bootstraps WebView2
if that is missing. The release also carries the standalone LifeLog.exe, which needs no
installation.
Requirements: Node.js, pnpm, a Rust toolchain and the MSVC build tools on Windows.
pnpm install
pnpm tauri buildArtifacts:
src-tauri/target/release/LifeLog.exe— standalone executablesrc-tauri/target/release/bundle/nsis/拾枝_0.1.0_x64-setup.exe— installer
-
On the very first launch a four-step tour runs on the main window (input box, prefix hint, tag tree, top bar menu); the main window opens by itself for it. Finish or skip it and it never comes back - replay it any time from Settings -> General -> "watch again". After that, launching the app shows only the input bar; open the main window from the tray icon's right-click menu. The tray icon appears next to the clock.
-
Press
Ctrl+Shift+Qanywhere to open the input bar. It can be moved by dragging any edge, resized in width from its left or right edge, zoomed with the wheel, and hidden withEscor a double-click on an edge. -
Type a note. Include
#tagsto classify it, for example:看了奥本海默,9 分 #电影 买牛奶 #todo 2026-09-11 阴,下午写完了迁移脚本 #日记 -
Press
Ctrl+Enter. The note is stored, the input clears,已保存 HH:MMflashes and the main window refreshes. -
In the main window, search by keyword, click a tag chip to filter, switch the ordering, edit a note in the split pane, or export everything to Excel. The filter is a single set of conditions and survives a restart.
Tray menu (right click): open the input bar, open the main window, settings, quit. A left click on the tray icon opens the main window (same as the menu item); the input bar is still surfaced by the global hotkey, the menu item, or a second launch. A second launch of the app does not start another instance — it surfaces the input bar of the running one.
Autostart is supported through the Tauri autostart plugin; the settings page shows the real registry
state and offers a one-click repair when the registered path goes stale. Whether launching shows the
input bar or stays in the tray is decided by the 启动时显示 setting (input bar by default), so a
system-triggered launch behaves exactly like a manual one.
The application is a single Tauri process hosting two webview windows and a Rust command layer. All state lives in SQLite; the frontend never talks to the database directly.
- Frontend (
src/) — TypeScript + React, two Vite entry points:index.html(main window) andinput.html(input bar).src/main-window/— stream UI:App.tsxorchestrates;use-notes-feed.tsowns the query state machine (paging, request sequencing, error sources);use-note-created.tssubscribes to the backend event that refreshes the list after a save from the input bar;NoteStream/NoteItem/EditPanelrender, filter and edit notes.src/main-window/filter/— the single filter state:use-filter-state.tsreads and writes thesettings.filter_currentkey (throttled),filter-state.tsis the pure parse/serialise layer, andConditionBar.tsxrenders the active chips plus the summary line.src/input-bar/—InputBar.tsx(a single textarea filling the window) and its behaviour hooks:use-drag-band(move / double-click),use-width-drag(edge resize),use-auto-height(1–5 line growth),use-input-wheel+use-input-view-store(zoom, opacity and their persistence),use-input-settings,logical-size.ts.src/main-window/settings/— the settings page model and its three sections.src/shared/—api.ts(typed command wrappers),markdown.ts(Markdown-it pipeline and DOMPurify policy),links.ts(external links open in the system browser),zoom.ts,note-source.ts(shared pre-save normalisation),time.ts, and the pure input-bar models (input-geometry.ts,input-gestures.ts,input-lock.ts,input-scale.ts,input-settings.ts,input-feedback.ts).src/shared/also holds the command core:when.ts(context-key expressions),keys.ts(key declarations),commands.ts(the command registry),hotkey-match.ts(in-app hotkey matching),fuzzy-score.ts(the scorer shared with the input bar) andquickpick/(provider registry, list model, MRU).src/main-window/unified/— the unified input box (UnifiedInput/UnifiedDropdown, the prefix hint and the hit counter) that replaced the palette overlay;src/main-window/palette/keeps the provider/model layer behind it (PaletteRow,use-palette, the commands / notes / tags providers, quick-open and the MRU lists).
- Command layer (
src-tauri/src/commands/) — thin Tauri commands for notes, settings, window control and export. - Domain layer (
src-tauri/src/) —tags.rs(tag tokeniser),db/repos/(notes CRUD, search queries, tag counts, settings),exchange/(Excel export),windowing/(tray, global hotkey, input-bar geometry and zoom). - Data layer (
src-tauri/src/db/) — a sharedMutex<Connection>behind Tauri state, opened with WAL journaling, foreign keys on and a busy timeout; versioned SQL migrations indb/migrations/.
Notes are written in one transaction that also rebuilds the note's tag links; SQLite triggers keep the FTS index in sync on insert, update and delete.
- Database:
%APPDATA%\com.lifelog.app\lifelog.db(SQLite, WAL). Delete this file to start over. - Search index: an FTS5 virtual table inside the same file, kept in sync by triggers.
- Unified entities: notes and tags share one
pointstable, and a relation is alinesrow (its name is a separate "name point"). An existing v30 database is migrated to v31 automatically on first launch (single transaction); the migration is irreversible, so back up%APPDATA%\com.lifelog.app\lifelog.dbbefore upgrading. - Excel export: written wherever you point the save dialog; nothing is uploaded.
- Legacy tag names that the current syntax cannot parse (for example names with spaces) are kept as-is; they are re-parsed with the new syntax the next time you edit and save that note.
pnpm install # dependencies
pnpm tauri dev # run with hot reload
pnpm tauri build # release build (LTO enabled)
pnpm typecheck # tsc --noEmit
pnpm test # vitest
cd src-tauri && cargo testVerification status at the time of writing: 2081 frontend tests (288 files), 801 Rust tests, typecheck, build and clippy clean.
Debugging the webviews over CDP: set
WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS=--remote-debugging-port=9222 before pnpm tauri dev, then
inspect http://127.0.0.1:9222/json/list. scripts/dev-cdp.mjs is a small helper for that.
| Layer | Choice |
|---|---|
| Shell | Tauri 2 (tray icon, global shortcut, autostart, single instance, dialog, opener plugins) |
| Backend | Rust, rusqlite with bundled SQLite, rust_xlsxwriter |
| Frontend | React 19, TypeScript, Vite, Tailwind CSS 4 |
| Markdown | markdown-it + markdown-it-task-lists, DOMPurify, highlight.js |
| Tests | Vitest (jsdom) for the frontend, cargo test for Rust |
| Storage | SQLite with FTS5 (trigram tokenizer) |
These are enforced in the codebase rather than being suggestions:
- No source file longer than 200 lines; extract a module instead of growing one
- All UI copy in Chinese
- No emoji anywhere in the source, the UI or commit messages
- Every byte of rendered Markdown passes through one sanitising component
- Failures are surfaced to the user, never swallowed silently
docs/superpowers/and.superpowers/(design and task workspaces) are deliberately gitignored