GitHub - ctanas/tiles: Tagged Instant Lightweight Emacs Snippets (TILES)

16 min read Original article ↗

TILES is an Emacs package for taking quick, title-less notes.

Each note (or tile, if you will) is a single paragraph stored in its own org file, organized through tags and bold keywords rather than hierarchies. TILES tries to keep it simple: there are no dependencies (except for Emacs, version 27.1 at least), no links between notes (except the Org Mode syntax), no backlinks, no graphs, no database; every note is a paragraph in its own text file.

I created this package because I wanted a note taking system with the following features:

  • focus on one paragraph (like Logseq): one paragraph = one note;
  • offers a bird's-eye view (quick preview) of recent notes (similar to Howm);
  • quick note preview, quick note edit;
  • color coding depending on the note's age (sort of like Howm, but not really);
  • title-less, to reduce friction (why having to stop the thought process to create a title that's never used afterwards?);
  • can use the Dynamic Block features in Org Mode (like Denote and Denote Org, ideal if you want to use your notes to create other documents;
  • can stitch notes together, after applying a search filter (like Howm, ideal if you want to use your notes to create other documents;
  • uses tags for hierarchy but also uses bold keywords (extracted automatically from words that are marked as bold);
  • can have follow-up text inside a note (and undertile, if you will), a kind of a meta-content (a private content inside a note), which is a paragraph prefixed with '&&' hidden everywhere (not exported with Dynamic Blocks actions) except expanded view in the dashboard and, of course, in note editing buffer;
  • search after tags and/or keywords only (who really wants to search for anything else?);
  • no external dependencies needed except at least version 27.1 of Emacs and Org Mode (built-in);
  • uses Org Mode format for bold, italic, links, in-line footnotes;

Screenshots

Dashboard for TILES, default view: Dashboard for TILES, default view

Dashboard with Org Mode markup toggled on (notice the red & character in front of a note, meaning there's some meta content there Dashboard with Org Mode markup toggled on

A regular note, expanded to reveal the keywoards: Screenshot 2026-02-09 at 14 14 41

A note with meta-content, expanded to reveal the meta-content (meta-content is not exported, nor visible with stitching): Screenshot 2026-02-09 at 14 14 56

An example of a regular note, no title, Org Mode markup, tags on the last line (mandatory by default but can be disabled, see tiles-tag-mode below): Screenshot 2026-02-09 at 14 15 27

An example of a note with meta-content, added after main content, prefixed with &&: Screenshot 2026-02-09 at 14 16 00

The result of stitching all notes sharing the same keyword ("Falcon 9" in this case): Screenshot 2026-02-09 at 14 25 23

If you're wondering about the font I'm using inside my Emacs, it's TX-02 Berkeley Mono 18 Medium Condensed.

Note format

Each note is a file named TYYYYMMDDHHMMSS.org (T followed by a timestamp up to seconds) and stored in a predefined folder:

The Mars Sample Return (*MSR*) mission involved
a collaboration between *NASA* and *ESA* to 
retrieve samples collected by the *Perseverance* 
rover[fn:: Launched in July 2020].

space/mars
  • Content: paragraph(s) with full org-mode formatting (*bold*, /italic/, [[links]], inline footnotes);
  • Optional private paragraphs prefixed with && (see Private paragraphs below);
  • Blank line separator after the content;
  • Last non-empty line: tags separated by / (always parsed as the tag line); tags are mandatory by default, but can be disabled;
  • Bold words (*word*) double as searchable keywords (optional);
  • Multi-paragraph notes are supported but discouraged; the parser always takes the last non-empty line as tags.

Installation

From GitHub (Emacs 29+)

Emacs 29 introduced package-vc-install, which can install packages directly from GitHub:

(package-vc-install "https://github.com/ctanas/tiles")

Then add to your config:

Manual

Clone the repository and add to your load path:

(add-to-list 'load-path "/path/to/tiles")
(require 'tiles)

Set your notes directory (default ~/notes/tiles/):

(setq tiles-directory "~/notes/tiles/")

Usage

Global keybindings

All commands are under the C-c m prefix:

Key Command Description
C-c m m tiles-show-notes Open dashboard
C-c m n tiles-new Create a new note (buffer)
C-c m q tiles-quick Quick capture via minibuffer
C-c m y tiles-yank Quick capture from clipboard
C-c m t tiles-tag-search Search by tags
C-c m k tiles-keyword-search Search by keywords

Dashboard

C-c m m launches the dashboard, which displays a chronological list of all notes. Each entry shows color-coded timestamps (showing hours and minutes to save space), inline previews, tags, and keywords. Timestamps are color-coded: green for today, darker green for recent (< 2 weeks), faded grey for older notes. The selection highlight is Lufthansa yellow. While the dashboard displays truncated timestamps for brevity, the actual filenames include timestamps down to the second level, allowing you to create multiple notes within the same minute without conflicts.

  *T*agged *I*nstant *L*ightweight *E*macs *S*nippets (TILES), v0.3.5 | 42 notes | loaded in 0.023s
  ════════════════════════════════════════════════════════════════════════
  [SPC] view, [RET] open, [TAB] expand, [f] format, [d] chg date, [u] touch, [0] stitch, [D] delete, [g] refresh, [+] more, [q] quit
  [t] filter tag, [k] filter keyword, [F] exclude tags, [T] list tags, [K] list keywords, [c] clr search, [C] clr excl, [l] new tile
  7 days until New Moon: Mon, 17 February 2026
  ──────────────────────────────────────────────────────────────────────

  2026-02-06 08:12  Hello world, I'm the first tile!  meta/test
  2026-02-06 08:12  This note is ready for production  meta/prod

Dashboard keybindings:

Key Action
n/p Navigate notes
SPC Open editable preview split (follows cursor)
RET Open note file
TAB Toggle expanded view (private &&, keywords, stats)
M-up Move selected note up
M-down Move selected note down
d Change note date/timestamp (renames file)
u Touch (update timestamp to now)
D Delete note (with confirmation)
t Filter displayed notes by tag
k Filter displayed notes by keyword
T List all tags
K List all keywords
F Exclude tags (hide notes with these tags)
c Clear search filter (keeps exclusion)
C Clear tag exclusion (keeps search filter)
f Toggle raw preview (strip org formatting)
+ Load next batch of notes
0 Stitch displayed notes into flowing view
l New note (same as C-c m n)
g Refresh
q Quit

Listing all tags and keywords

M-x tiles-list-tags (or T in the dashboard) displays all unique tags with occurrence counts, sorted alphabetically. M-x tiles-list-keywords (or K) does the same for bold keywords. In both buffers, items that appear in both sets are shown in bold. Press RET to filter the dashboard by the selected item.

Sorting: a sorts alphabetically (a-z), o sorts by occurrence (high to low), d toggles ascending/descending.

In the keyword list, press R to rename a keyword across all notes. You'll be prompted for a new name, and every bold occurrence (*old*) will be replaced in all note files that contain it.

Tag exclusion

Press F in the dashboard to exclude notes by tag. Enter one or more space-separated tags and any note carrying those tags will be hidden. The exclusion filter works independently from the search filter (t/k): you can exclude some tags, then search within the remaining notes. c clears only the search filter (keeping the exclusion), while C clears only the exclusion (keeping the search filter). The dashboard title shows the active exclusion (e.g., | excluding: journal draft).

Tag search syntax

Tag queries use / for AND and SPC for OR:

Query Meaning
b218/lx2026 Notes with both b218 and lx2026
b218 misc Notes with either b218 or misc
b218/lx2026 misc (b218 AND lx2026) OR misc

This syntax applies everywhere: tiles-tag-search, dashboard filter (t), and dynamic block :tags parameter.

Keyword search syntax

Keyword queries use SPC-separated terms with OR logic — any matching term is enough:

Query Meaning
emacs Notes with emacs as a bold keyword
emacs lisp Notes with either emacs or lisp

Keywords are the *bold* words extracted from note content. Hyphens in keywords are normalized to spaces for matching and display — *Falcon-9* and *Falcon 9* are treated as the same keyword ("Falcon 9") — but the note content itself is never modified. This syntax applies to tiles-keyword-search, dashboard filter (k), and dynamic block :keywords parameter.

Search views

Tag and keyword searches (C-c m t / C-c m k) open a two-panel view: results list on top, live preview below.

Key Action
n/p Navigate results
RET Open note file
SPC Toggle to stitched view
r Refine search (new query)
t/k Switch to tag/keyword search
q Quit

Stitched view

Press SPC from the search view to enter the stitched view: all matching notes concatenated into a single flowing org buffer, stripped of tag lines and private (&&) paragraphs, in inverse chronological order. This is useful for reading related notes as continuous prose or if you want to include multiple related notes into another document, like a newsletter.

Key Action
n/p Jump between note boundaries
RET/e Open the source file at point (with focus mode)
SPC Toggle back to two-panel view
r Refine search
q Quit

Capturing notes

C-c m n opens a capture buffer. Write your paragraph, add a blank line, then your tags. Press C-c C-c to save, C-c C-k to cancel. While keywords are not mandatory, tags are (by default), so if the user forgets to add tags, it will be asked to do so. The tag line (last line) is displayed in red using the tiles-tags face, matching the tag color in the dashboard.

Inside the capture buffer, C-c m t (tiles-insert-tag) inserts a tag at point using minibuffer completion:

  • Unrestricted mode: suggests all tags found in existing notes (free input allowed).
  • Restricted mode: completes from the allowed list with require-match enforced.
  • Required-one-of mode: suggests the required tags (free input still allowed).
  • Inhibit mode: displays a message explaining that tags are disabled and how to re-enable them.

Outside a capture buffer, C-c m t continues to run tiles-tag-search as usual.

For faster capture, C-c m q prompts for content and tags directly in the minibuffer. C-c m y does the same but pre-fills the content from the clipboard (kill ring), which you can edit before confirming.

Private paragraphs

Any paragraph in a note that starts with && is treated as private. Private paragraphs are hidden from dashboard previews, stitched views, search panels, and dynamic blocks. They are only visible in two places: when expanding a note with TAB in the dashboard, and when editing the file directly.

This is useful for keeping personal annotations, reminders, or context that you don't want surfacing in exports or shared views.

The Mars Sample Return (*MSR*) mission involved
a collaboration between *NASA* and *ESA*.

&& Personal note: double-check the timeline
with the ESA press release from January.

space/mars

In the example above, the && paragraph will not appear in previews or stitched output, but pressing TAB on this note in the dashboard will reveal it in the expanded area.

When formatted preview is on (i.e., tiles-preview-raw is nil), notes containing private paragraphs display a red & indicator right before the preview text in the dashboard, so you can tell at a glance which notes have hidden content.

Focus mode

Focus mode centers the buffer content with approximately 80-character line width (using window margins, similar to olivetti-mode) and adds visual padding at the top. No hyphens or hard wraps — just soft word wrap via visual-line-mode. The padding is purely visual and is never saved to the file.

Focus mode is enabled by default when creating new notes. You can also toggle it manually with M-x tiles-focus-mode in any capture buffer.

To disable focus mode by default:

(setq tiles-focus-default nil)

Updating a note's timestamp

While editing a note, M-x tiles-touch updates the file's timestamp to the current time and renames the file accordingly. Asks for confirmation before proceeding. Useful for bumping a note to the top of the chronological list after editing.

Org dynamic blocks

TILES provides two dynamic block types for embedding note data in org files:

tiles-notes - Insert a linked list of matching notes:

#+BEGIN: tiles-notes :tags "space mars" :sort "newest" :limit 10
- [[file:~/notes/tiles/T20260206081250.org][2026-02-06 08:12:50]] The Mars Sample Return...  space/mars
- [[file:~/notes/tiles/T20260206081227.org][2026-02-06 08:12:27]] NASA announced today...  space/nasa
#+END:

tiles-files - Embed note contents directly:

#+BEGIN: tiles-files :tags "journal" :keywords "review" :separator "\n-----\n"
First matching note content...

-----
Second matching note content...
#+END:

Parameters (all optional):

Parameter Description Default
:tags Space-separated tags (OR logic)
:keywords Space-separated keywords (OR logic)
:sort "newest" or "oldest" "newest"
:limit Maximum number of notes unlimited
:separator String between notes (tiles-files only) blank line
  • C-c C-x x to insert a dynamic block from a menu
  • C-c C-x C-u to update the block under cursor

Performance

TILES uses an in-memory cache that stores parsed note data keyed by filepath. Files are only re-read from disk when their modification time changes. The first load reads all files; subsequent operations are fast. Use M-x tiles-clear-cache to force a full reload.

Customization

All settings are available via M-x customize-group RET tiles.

Variables

Variable Description Default
tiles-directory Root directory for notes (recursive) ~/notes/tiles/
tiles-preview-length Max characters for inline preview 105
tiles-line-padding Extra padding beyond preview and tags 22
tiles-preview-raw Strip all org formatting from previews t
tiles-dashboard-limit Max notes per page (nil = unlimited) 50
tiles-focus-default Enable focus mode for new notes t
tiles-fancy-separators Use Unicode box-drawing separators (═/─) t
tiles-tag-mode Controls tag behavior (see below) 'unrestricted

Tag mode

tiles-tag-mode controls how tags work across the entire package. It accepts four kinds of values:

'unrestricted (default) — tags work as described throughout this document: any string is accepted, search and filter are fully available.

'inhibit — tags are completely disabled. Capture (both buffer and quick) does not prompt for tags; notes are saved with an internal placeholder. Tag-based search (C-c m t), dashboard tag filter (t), tag exclusion (F), and tag listing (T) are all disabled and will show an error if invoked. Tags are not displayed in the dashboard, and the second keybinding help line is simplified to omit tag-related keys.

A list of strings — only those tags are accepted. Tag prompts use completing-read with the list as candidates and require-match enforced, so any completion framework (Vertico, Ivy, Helm, etc.) will show the candidates automatically. The first element of the list is used as the default when the user provides no input. Tags entered manually in the capture buffer are validated against the list at save time, and the save is rejected if any tag is not in the list. Dashboard tag filter (t), exclusion (F), and search (C-c m t) also use completion-based prompts restricted to the allowed list.

(required-one-of TAG...) — any tag is accepted, but at least one from the list must be present. Prompts suggest the required tags via completing-read with free input still allowed, so completion frameworks will surface the candidates without enforcing them. If the saved note contains no tag from the required list, the save is rejected with a clear error message. Dashboard filter, exclusion, and search prompts also suggest the required tags but accept free input.


Case 1 — Unrestricted (default). No configuration needed; this is the default. To restore it explicitly:

(setq tiles-tag-mode 'unrestricted)

Case 2 — Inhibit tags. Use this if you prefer a pure keyword-based workflow with no tagging at all:

(setq tiles-tag-mode 'inhibit)

With this setting: capture never asks for tags, the dashboard hides tag columns and tag-related keybindings, and t, F, T, and C-c m t all report an error.

Case 3 — Restricted tag list. Define an explicit vocabulary; the first element becomes the default when the user confirms without typing:

(setq tiles-tag-mode '("work" "personal" "journal" "idea" "reference"))

With this setting: C-c m n and C-c m q/C-c m y prompt for a tag using completion restricted to the list (work is offered as the default); t, F, and C-c m t in the dashboard also use completion. Any tag typed manually in the capture buffer that is not in the list will be rejected at save time with a clear error message.

Case 4 — At least one required tag. Allow any tags, but enforce that at least one comes from a required set:

(setq tiles-tag-mode '(required-one-of "work" "personal" "journal"))

With this setting: C-c m n and C-c m q/C-c m y prompt for tags with the required tags suggested via completion (free input still allowed); t, F, and C-c m t in the dashboard also suggest the required tags but accept any string. If the saved note contains none of the required tags, the save is rejected with a clear error message.

Example configuration:

(setq tiles-directory "~/Documents/tiles/")
(setq tiles-preview-length 120)

Faces (colors)

All faces can be customized via M-x customize-face or in your config:

Face Description Default
tiles-timestamp-today Today's timestamps #228b22
tiles-timestamp-recent Recent timestamps (< 2 weeks) #3a5a2a
tiles-timestamp-old Older timestamps (> 2 weeks) #999999
tiles-tags Tag display #a00000
tiles-keywords Keyword display in expanded view #006600
tiles-notes-hl-line Selection highlight #FFC700
tiles-notes-expanded Background of expanded lines #FFF8DC

Example:

(set-face-attribute 'tiles-timestamp-today nil :foreground "#008800")
(set-face-attribute 'tiles-notes-hl-line nil :background "#FFD700")

Changelog

  • 0.3.5 — Tag mode control via tiles-tag-mode: 'unrestricted (default), 'inhibit (tags disabled, tag search/filter suppressed), a list of allowed tag strings (completion-based prompts, first element is the default), or (required-one-of TAG...) (any tags accepted, but at least one from the list must be present). Fix: deleting the last note now correctly refreshes the dashboard to an empty state instead of leaving the deleted note visible.
  • 0.3.4 — Keyword rename: R in the keyword list renames a keyword across all note files.
  • 0.3.3 — Unicode box-drawing dashboard separators (tiles-fancy-separators, set to nil for ASCII fallback). Tag line shown in red (tiles-tags face) when editing notes. Focus mode when opening notes from stitched view (RET).
  • 0.3.2 — Tag exclusion filter (F to exclude, C to clear, independent from search filter). Focus mode for distraction-free editing (enabled by default, tiles-focus-default). Interactive tag/keyword lists with occurrence counts and sorting (o/a/d). Keyword hyphen normalization. Dashboard keybindings: T list tags, K list keywords, u touch. Stitch confirmation when no filter is active.
  • 0.3.1 — Red & indicator in formatted preview for notes with private paragraphs. New tiles-list-tags and tiles-list-keywords commands to browse all unique tags/keywords (with bold cross-highlighting).
  • 0.3 — Private paragraphs: paragraphs starting with && are hidden from dashboard previews, stitched views, search panels, and dynamic blocks. Only visible via TAB expansion in the dashboard or direct file editing.
  • 0.2 — Initial public release.

Acknowledgements

Many thanks to Protesilaos Stavrou for Denote and Denote Org, Kazuyuki Hiraoka for Howm, Andrei Sukhovskii for Howm Manual, Jethro Kuan for Org-roam, Jason Blevins for Deft, Zachary Schneirov for Notational Velocity, and to all the developers of Logseq and Obsidian for their inspiration into creating this package.

Disclaimer

This package was developed with the assistance of Claude, an AI assistant created by Anthropic.

License

GNU GPLv3