Skip to content

ganten7/navi

Repository files navigation

Navi

An Obsidian-style interactive graph viewer for org-roam, running as a native desktop window. No Emacs package required — it reads your org-roam.db directly and opens nodes in your existing Emacs process via emacsclient.

Designed for macOS with optional borderless mode and first-class AeroSpace + SketchyBar integration. Also runs on Linux from source.


Screenshots

Navi graph overview — nodes, edges, and local connections alongside Emacs

Navi local graph mode — focused neighbourhood view with node labels


Quick start

./navi

On first run the launcher creates a Python venv at ~/.local/share/navi/venv, installs dependencies (pygame, Pillow, numpy, moderngl), and writes ~/.config/navi/config.json with auto-detected paths. Subsequent runs start immediately.

The legacy launcher name org-roam-graph-window still works and forwards to navi.

macOS app (no Python required):

./build/build-macos.sh
open dist/Navi.app

Requirements

  • Python 3.8+ (source install only; not needed for Navi.app)
  • org-roam v2 — reads nodes, files, links, tags, and aliases tables
  • emacsclient + running Emacs server — for open-in-Emacs ((server-start) in init.el)
  • numpy — auto-installed by the launcher; provides ~30x physics speedup over pure Python (falls back gracefully if unavailable)

Configuration

Config file: ~/.config/navi/config.json (created on first run).

{
  "db": "~/.emacs.d/org-roam.db",
  "emacsclient": "/opt/homebrew/bin/emacsclient",
  "server_name": "server",
  "show_fps": true,
  "borderless": true
}
Key Description
db Path to org-roam.db
emacsclient Path to emacsclient binary. Auto-resolved from Homebrew, /usr/local, and Emacs.app if omitted or bare name
server_name Emacs server name (default server)
show_fps Show FPS counter (F toggles at runtime)
borderless Edge-to-edge window with no title bar (see below)

Legacy config at ~/.config/org-roam-graph/config.json is still read; the next save writes to the Navi path.

Overrides (take precedence over config):

Method Example
Config file ~/.config/navi/config.json
--db ./navi --db ~/notes/org-roam.db
--server ./navi --server work
--borderless Force borderless on this launch
--check Validate config + Emacs connectivity, then exit
ORG_ROAM_DB ORG_ROAM_DB=~/notes/org-roam.db ./navi

DB auto-detection probes the following paths in order:

Path Config
$ORG_ROAM_DB env override (highest priority)
$XDG_DATA_HOME/emacs/org-roam.db XDG-strict Linux
~/.emacs.d/org-roam.db vanilla Emacs
~/.config/emacs/org-roam.db XDG-style Emacs
~/.config/doom/.local/etc/org-roam.db Doom 3.x (new user-dir)
~/.config/doom/org-roam.db Doom 3.x fallback
~/.doom.d/.local/etc/org-roam.db Doom 2.x (legacy user-dir)
~/.doom.d/org-roam.db Doom 2.x fallback
~/.spacemacs.d/org-roam.db Spacemacs

If your setup uses a custom org-roam-db-location, set db in config.json or pass --db.


Opening nodes in Emacs

Double-click a node (or select it and press Enter / Space) to open it in Emacs. File nodes open the file; headline nodes jump to the heading (goto-char).

emacsclient from Navi.app

GUI apps on macOS get a minimal PATH, so Navi resolves emacsclient to an absolute path at startup (Homebrew, /usr/local, /Applications/Emacs.app/...). It also searches for the Emacs server socket under:

  • $EMACS_SERVER_SOCKET / $EMACS_SERVER_FILE
  • $TMPDIR/emacs{uid}/ (and server_name / server)
  • /tmp, /private/tmp
  • /var/folders/*/*/T/emacs{uid}/ (macOS Terminal vs GUI TMPDIR mismatch)

If open fails, an error appears in the status bar for 6 seconds. Ensure Emacs is running with (server-start) in your init.

Recommended config when using the .app:

"emacsclient": "/opt/homebrew/bin/emacsclient"

Borderless mode (macOS + AeroSpace)

"borderless": true removes the title bar and traffic lights for a flush, edge-to-edge graph.

Implementation:

  1. pygame NOFRAME + OpenGL — undecorated SDL window with ModernGL rendering
  2. Cocoa via SDL — resolves the real NSWindow* through SDL_GetWindowWMInfo (required because pygame.OPENGL no longer exposes cocoa_window in get_wm_info())
  3. Managed window flagsNSWindowCollectionBehaviorManaged so AeroSpace can tile (retried for ~1s after launch)
  4. Move windowdrag the status bar at the bottom of the window (SDL window position, works independently of AeroSpace tiling)
  5. AeroSpace ruleslayout tiling on Navi window detect + navi-tile.sh on focus

If borderless tiling or focus fails after an update, run with debug logging:

# from source
NAVI_DEBUG=1 ./navi

# built bundle (without copying to /Applications)
NAVI_DEBUG=1 ./dist/Navi.app/Contents/MacOS/Navi
# or
NAVI_DEBUG=1 open ./dist/Navi.app

Look for NSWindow via SDL on stderr — if you see NSWindow not found, report the full line.

Example ~/.aerospace.toml snippets (also in your dotfiles if you use this stack):

[gaps]
inner.horizontal = 0
inner.vertical   = 0
# outer.top may reserve space for SketchyBar

[[on-window-detected]]
if.app-id = 'com.navi.graph'
run = ['layout tiling', 'exec-and-forget sketchybar --trigger aerospace_workspace_change']

on-focus-changed = [
    'exec-and-forget ~/.config/aerospace/navi-tile.sh',
    'exec-and-forget sketchybar --trigger aerospace_workspace_change',
]

Helper: ~/.config/aerospace/navi-tile.sh re-applies layout tiling when Navi is focused.


SketchyBar + AeroSpace

Workspace pills (1–9) show the front app’s icon on each workspace (first window — no special “Navi wins” override).

App Icon
Navi 󰎚 (nerd-font note; same glyph family as Obsidian)
Cursor / VS Code 󰨞
Safari / Arc 󰖟
see ~/.config/sketchybar/plugins/aerospace_all.sh

Navi is detected by app name Navi, bundle id com.navi.graph, or window title containing navi / org-roam / graph.

Files:

Path Role
~/.config/sketchybar/plugins/aerospace_all.sh Single dispatcher; one sketchybar call updates all 9 spaces
~/.config/sketchybar/plugins/aerospace.sh Per-item fallback
~/.config/sketchybar/sketchybarrc Subscribes space_dispatcher to aerospace_workspace_change

Reload bar: sketchybar --trigger aerospace_workspace_change

To change the Navi icon, edit NAVI_ICON / icon_for in aerospace_all.sh (use glyphs present in MesloLGLDZ Nerd Font).


Controls

Input Action
Drag background Pan view
Swipe + release Kinetic pan (momentum)
Drag node Move node
Swipe node + release Launch node with momentum
Scroll / trackpad Zoom toward cursor
Click node Select — highlights connections
Double-click node Open in Emacs
Tab / Shift-Tab Cycle through nodes
Enter / Space Open selected node in Emacs
T Cycle colour theme (5 themes)
G Toggle tag colouring
A Toggle age / weathering heatmap
E Toggle particle effects
D Toggle daily notes filter
O Toggle orphan node filter
L Cycle local graph mode (1 → 2 → 3 hops → off)
/ Search nodes by title or alias
W Reload graph from database
F Toggle FPS counter
P Pause / resume physics
R Reset view (pan + zoom)
H Hold to show controls panel
Q / Escape Quit

Filters

Key Filter What it hides
D Daily notes Bare date titles (2024-01-15) or files in daily / dailies / journal / journals
O Orphans Nodes with no links in or out

Local graph mode

Press L to focus on the selected node's neighbourhood: 1 hop → 2 hops → 3 hops → off. Nodes outside the neighbourhood fade; edges between faded nodes are hidden.


Search

Press / to search by title or alias. Enter jumps to the best match. Escape cancels.


Tag colouring (G)

Tags come from org-roam's tags table. Press G to colour nodes by first tag (golden-ratio hue spacing).

#+FILETAGS: :project:

Run M-x org-roam-db-sync in Emacs, then W in Navi to reload.


Age / weathering heatmap (A)

Stage Age Visual
Fresh ≤ 7 days Warm tint
Recent 8–30 days Normal
Aging 31–90 days Grey shift, rust specks
Worn 91–270 days Greyer, rust + lichen
Old 271–540 days Heavy grey
Ancient > 540 days Near-grey, cracks

Particle effects (E)

Ambient particle clouds per node; comet trails when flinging nodes. Toggle with E.


Node types

File-level nodes — solid gradient spheres. Headline-level nodes — inner ring; open jumps to heading position.


Themes

Press T to cycle: Obsidian → Forest → Ocean (default) → Ember → Mono.


Font

Monospace: Meslo LG Nerd Font (macOS ~/Library/Fonts/), JetBrains/Meslo on Linux, system fallbacks. CJK titles use Noto Sans JP / PingFang when available.


Linux / cross-platform (source install)

The shell launcher (navi) and navi.py run on any OS with Python 3.8+ and a display. The .app bundle is macOS arm64 only; everything else uses the script path.

emacsclient is auto-detected from: Homebrew, MacPorts, /usr/local/bin, /usr/bin, Snap, ~/.local/bin, and ~/.nix-profile/bin.

Emacs server socket is probed from $EMACS_SERVER_SOCKET, $XDG_RUNTIME_DIR/emacs/ (standard on systemd Linux), $TMPDIR/emacs{uid}/, and the macOS /var/folders tree.

git clone <repo> ~/navi
cd ~/navi
./navi --check        # validates paths, emacsclient, and server before first launch
./navi

Nix / NixOS

# run once to ensure deps
nix-shell -p python3 python3Packages.pygame python3Packages.pillow
./navi

Or point nix-env/home-manager at the repo — emacsclient from ~/.nix-profile/bin is auto-detected.

Common config for non-standard setups

{
  "db": "~/notes/org-roam.db",
  "emacsclient": "~/.local/bin/emacsclient",
  "server_name": "server"
}

macOS app (Apple Silicon)

./build/build-macos.sh
Output dist/Navi.app (~40 MB)
Bundle ID com.navi.graph
Minimum OS macOS 11+ arm64
Config ~/.config/navi/config.json
Gatekeeper Unsigned — xattr -cr Navi.app if blocked
open dist/Navi.app
cp -R dist/Navi.app /Applications/

After code changes, rebuild and quit/reopen the app.


App icon

python3 tools/make-icon.py assets/logo-source.png

Writes assets/icon.png and assets/icon.icns for the macOS bundle.


Dependencies

Installed automatically into an isolated venv (source install) or bundled in Navi.app.

Package License Purpose
pygame LGPL 2.1 Window, input, OpenGL context
moderngl MIT GPU graph rendering
numpy BSD Vectorised physics
Pillow HPND Label/status rasterisation

Testing with a synthetic database

To stress-test Navi without a real org-roam setup:

python3 tools/make-test-db.py --tiers   # generates 300, 800, and 1500-node DBs
./navi --db /tmp/navi-test-1500.db      # launch against the large one
Flag Description
-n N Number of nodes (default 500)
-o PATH Output path (default /tmp/navi-test.db)
--tiers Generate all three tier DBs and print launch commands
--seed N Random seed for reproducibility

The generated graph has a power-law degree distribution (realistic hubs), 8 topic tags, ~10% headline nodes, and a mix of fresh/old modification times to exercise the age heatmap.


Performance

Rendering uses ModernGL (OpenGL 3.3 core / Metal backend on macOS) with a single instanced draw call per layer (nodes, glow, particles, edges). Measured on Apple M-series:

Nodes Navi CPU WindowServer
~10 (typical small graph) ~26 % ~24 %
physics settling ~45–55 % ~30 %

Physics uses numpy vectorised broadcasting (auto-installed). Physics ms/frame:

Nodes numpy pure Python
300 0.2 ms ~6 ms
800 1.4 ms
1500 5 ms

Without numpy, the soft limit is ~250 nodes at 60 fps. The GPU renderer scales to large graphs without additional CPU cost — rendering 1 500 nodes costs the same as rendering 10.


Project layout

navi                         Shell launcher (self-bootstrapping venv)
navi.py                      Main application
org-roam-graph-window        Legacy launcher (forwards to navi)
build/build-macos.sh         PyInstaller → dist/Navi.app
build/package-dmg.sh         Package dist/Navi.app → distributable DMG
build/navi.spec              macOS bundle spec
tools/make-test-db.py        Generate synthetic org-roam DB for testing
assets/icon.icns             macOS app icon
LICENSES/                    Dependency license texts
~/.config/navi/config.json   Per-user config (created on first run)
CHANGELOG.md                 Version history

Comparison with org-roam-ui

Navi org-roam-ui
Emacs package needed No Yes
Emacs server needed Only for open-in-Emacs Yes (always)
Runs in Native OS window Browser
Setup Script or .app Package install + config
Daily notes filter Yes No
Local graph (N-hop) Yes Yes
Search by title/alias Yes Partial
Tag-based colouring Yes Yes
Age heatmap Yes No
Headline node jump Yes Yes
Live DB reload Manual (W) Yes
Particle effects Yes No
3D graph No Yes
AeroSpace / borderless Yes (macOS) N/A

License

Navi application code: see repository license. Bundled dependencies: pygame (LGPL 2.1), Pillow (HPND). Ship LICENSES/ with binary releases.

About

Obsidian-style interactive graph viewer for org-roam — native window, no Emacs package required

Resources

Stars

Watchers

Forks

Packages

 
 
 

Contributors