Skip to content
Zzz210sPublic

About

A local-first personal life database: one note stream with #tags, full-text search, and Markdown plus Excel export.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

English | 简体中文

LifeLog

LifeLog

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.

Table of Contents

Background

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 #tags parsed out of the text.
  • Retrieval is filtering. Full-text search (SQLite FTS5 with the trigram tokenizer, 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).

Features

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 a 0 2px 10px soft shadow; focused: 2 px rgba(59,130,246,0.65) blue border with a 0 0 12px blue 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 it
  • Ctrl+Enter saves: the input clears, 已保存 HH:MM flashes 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/Down to choose, Enter/Tab to accept, Esc to 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 #tag chip 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/DD lives 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-name still 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 日期/2026 filters 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 +N past 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+Enter to 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+P focuses the unified input box with > (commands) and Ctrl+P with @ (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

  • #tags are 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 no done/doing tags and no per-note inline checkbox
  • The note stream shows no time at all: the only time-like information is the ordinary 时间/YYYY/MM/DD tag 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 .xlsx sheet with four columns: id, body, creation time, and reference path; creation time is the note's diary day, not the moment it was imported

Install

Prebuilt (Windows 11 x64)

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.

From source

Requirements: Node.js, pnpm, a Rust toolchain and the MSVC build tools on Windows.

pnpm install
pnpm tauri build

Artifacts:

  • src-tauri/target/release/LifeLog.exe — standalone executable
  • src-tauri/target/release/bundle/nsis/拾枝_0.1.0_x64-setup.exe — installer

Usage

  1. 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.

  2. Press Ctrl+Shift+Q anywhere 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 with Esc or a double-click on an edge.

  3. Type a note. Include #tags to classify it, for example:

    看了奥本海默,9 分 #电影
    买牛奶 #todo
    2026-09-11 阴,下午写完了迁移脚本 #日记
    
  4. Press Ctrl+Enter. The note is stored, the input clears, 已保存 HH:MM flashes and the main window refreshes.

  5. 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.

Architecture

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.

Architecture

  • Frontend (src/) — TypeScript + React, two Vite entry points: index.html (main window) and input.html (input bar).
    • src/main-window/ — stream UI: App.tsx orchestrates; use-notes-feed.ts owns the query state machine (paging, request sequencing, error sources); use-note-created.ts subscribes to the backend event that refreshes the list after a save from the input bar; NoteStream/NoteItem/EditPanel render, filter and edit notes.
    • src/main-window/filter/ — the single filter state: use-filter-state.ts reads and writes the settings.filter_current key (throttled), filter-state.ts is the pure parse/serialise layer, and ConditionBar.tsx renders 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) and quickpick/ (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 shared Mutex<Connection> behind Tauri state, opened with WAL journaling, foreign keys on and a busy timeout; versioned SQL migrations in db/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.

Data and storage

  • 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 points table, and a relation is a lines row (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.db before 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.

Development

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 test

Verification 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.

Tech stack

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)

Project conventions

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

License

MIT

About

A local-first personal life database: one note stream with #tags, full-text search, and Markdown plus Excel export.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages