Exocortex
Note taking/information storage + retrieval system inspired by https://roamresearch.com
Install / Use
/learn @neutralinsomniac/ExocortexREADME
exocortex
A note-taking/todo/information-storage system. There are two clients:
- exo — a terminal UI for desktop/laptop, written in Go with bubbletea
- tdeck-exo — a firmware port for the LilyGo T-Deck Plus, a portable device with a 320×240 display and a physical QWERTY keyboard
Concepts
exocortex tries to be as friction-free as possible when it comes to capturing information without worrying about organization upfront.
On startup, exo reopens the last tag you were on, or the inbox tag if none exists yet.
Tags are the top-level organizational unit. They are created explicitly via the new tag command, or on-the-fly by referencing them in row text using [[double brackets]]. Tags are automatically deleted when they have no rows and no rows reference them.
Rows are bullets under a tag. When a row references another tag (e.g. [[todo]] take out the trash), exocortex links that row bidirectionally — viewing the todo tag will show a reference back to the originating tag with the full row text visible and editable.
Installation
go install github.com/neutralinsomniac/exocortex/cmd/exo@latest
Or with Nix:
nix run github:neutralinsomniac/exocortex
To install into a NixOS configuration flake, add it as an input and pass it through to your NixOS module:
{
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.11";
exocortex.url = "github:neutralinsomniac/exocortex";
exocortex.inputs.nixpkgs.follows = "nixpkgs";
};
outputs = { nixpkgs, exocortex, ... } @ inputs: {
nixosConfigurations.myhostname = nixpkgs.lib.nixosSystem {
system = "x86_64-linux";
specialArgs = { inherit inputs; };
modules = [ ./configuration.nix ];
};
};
}
Then in configuration.nix:
{ pkgs, inputs, ... }:
{
environment.systemPackages = [
inputs.exocortex.packages.${pkgs.system}.exo
inputs.exocortex.packages.${pkgs.system}.exo-server
];
}
Usage
Run exo from anywhere. All data is stored in a single SQLite database at $XDG_DATA_HOME/exocortex/exocortex.db (defaulting to ~/.local/share/exocortex/exocortex.db).
Press ? inside the app to open the help screen.
Keybindings
Tags
| Key | Action |
|-----|--------|
| i | Go to inbox tag |
| n | New tag |
| r | Rename current tag |
| t | Tag selector (type to filter) |
| / | Fuzzy search all rows |
| \ | Toggle show/hide done rows |
| ctrl-t | Go back in tag history |
| 1–9 | Jump to numbered tag reference |
Rows
| Key | Action |
|-----|--------|
| g / G | Jump to first / last row |
| j / k | Move cursor down / up |
| enter | Follow tag link in selected row |
| o / O | Add row below / above cursor |
| e | Edit selected row |
| N | Add/edit note on selected row |
| d | Cut selected row (or all marked) |
| D | Mark done / un-done (or all marked) |
| y | Yank (copy) selected row |
| p / P | Paste below / above cursor |
| J / K | Move row down / up within its priority group |
| space | Toggle row in/out of multi-selection |
| ; | Clear multi-selection |
| ! @ # $ % | Set priority 1–5 (repeat to clear) |
| ) | Clear priority |
| u / U | Undo / redo |
| S | Sync with server |
| ? | Help |
| q | Quit |
T-Deck Plus firmware (tdeck-exo)
The tdeck-exo/ directory contains a PlatformIO project that builds firmware for the LilyGo T-Deck Plus. It shares the same sync protocol as exo, so a T-Deck and a desktop instance stay in sync through the same server.
Building
Open tdeck-exo/ in PlatformIO and flash the lilygo_tdeck_plus environment.
Configuration
Edit tdeck-exo/src/config.h before flashing:
| Setting | Description |
|---------|-------------|
| WIFI_SSID / WIFI_PASSWORD | Network to connect to for sync and NTP |
| SYNC_URL | Server URL — https:// for TLS, http:// for AES-256-GCM payload encryption |
| SYNC_TOKEN | Shared secret matching the server's -token flag |
| TLS_INSECURE | Set false to verify the server's TLS certificate |
| NTP_SERVER | NTP server for time sync (default: pool.ntp.org) |
| TZ_OFFSET_HOURS | UTC offset for your timezone (e.g. -5 for EST) |
| BOARD_BAT_ADC | GPIO pin for battery voltage ADC (default: 4) |
| SLEEP_TIMEOUT_MS | Deep sleep after this many ms of inactivity (default: 30000; set to 0 to disable) |
| STATIC_IP | (optional) Static IP address — bypasses DHCP, avoids repeated DHCP requests on each wake |
| STATIC_GATEWAY | (optional) Gateway address (required if STATIC_IP is set) |
| STATIC_SUBNET | (optional) Subnet mask (required if STATIC_IP is set) |
| STATIC_DNS | (optional) DNS server (required if STATIC_IP is set) |
Keybindings
Tag list
| Key | Action |
|-----|--------|
| j / k or trackball | Move cursor |
| Enter | Open tag |
| i | Go to inbox |
| n | New tag |
| Type | Filter tags |
| Backspace | Clear filter / return to row list |
Row list
| Key | Action |
|-----|--------|
| j / k or trackball | Move cursor |
| o | New row |
| e | Edit selected row |
| d | Cut selected row (copied to clipboard) |
| y | Yank (copy) selected row |
| p / P | Paste clipboard row after / before cursor |
| D | Toggle done |
| 1–5 | Set priority (press same key again to clear) |
| 0 | Clear priority |
| J / K | Move row up / down within its priority group |
| h | Toggle show/hide done rows |
| Enter / l | Follow [[tag]] link in selected row |
| b | Go back in tag history |
| t | Tag list |
| i | Go to inbox |
| s | Sync with server |
| Backspace | Sleep immediately |
Row edit
| Key | Action |
|-----|--------|
| Trackball left / right | Move cursor within text |
| Enter | Save |
| Escape | Cancel |
Sync
exo-server is a companion binary that lets you keep a hosted exocortex database and sync to it from multiple machines. Sync is bidirectional with last-write-wins conflict resolution per row.
Running the server
go install github.com/neutralinsomniac/exocortex/cmd/exo-server@latest
exo-server -db /path/to/exocortex.db -token <secret> -cert /path/to/cert.pem -key /path/to/key.pem
| Flag | Default | Description |
|------|---------|-------------|
| -db | (required) | Path to the SQLite database file |
| -token | (required) | Shared secret used to authenticate clients |
| -cert | | TLS certificate file (PEM). Must be paired with -key |
| -key | | TLS private key file (PEM). Must be paired with -cert |
| -addr | :8765 | Address to listen on |
Encryption modes
With -cert and -key (TLS): The server listens with HTTPS. All traffic is encrypted via TLS. The sync payload is transmitted as plain JSON over the encrypted connection. Configure the client with an https:// URL.
Without -cert and -key (symmetric encryption): The server listens with plain HTTP, but the sync payload itself is encrypted end-to-end using AES-256-GCM. The encryption key is derived from the shared -token via SHA-256. Both request and response bodies are opaque binary blobs (application/octet-stream). No Authorization header is sent — a successful decryption already proves knowledge of the token, so the token never appears in plaintext on the wire. Configure the client with an http:// URL.
The client automatically selects the encryption mode based on the sync_url setting: https:// URLs use TLS (no payload encryption), and http:// URLs use AES-256-GCM payload encryption.
Configuring the client
Set the server URL and token in the exocortex settings table using any SQLite client:
sqlite3 ~/.local/share/exocortex/exocortex.db \
"INSERT OR REPLACE INTO settings VALUES ('sync_url', 'https://yourserver:8765');
INSERT OR REPLACE INTO settings VALUES ('sync_token', 'yoursecret');"
Once configured, press S inside exo to sync. Status is shown in the bottom bar.
