A no-Xcode AppKit app (swiftc + build.sh): - Plaintext editor: 5 themes, line numbers, soft wrap, font controls, find, live Markdown preview (split mode), selection-aware Markdown toolbar. - Freeform Board mode: zoomable/pannable canvas of page cards (portrait/landscape, US Letter/A4/Legal/Square/Wide/Note), clean boxes, images (paste/drop/panel), freehand pen/shapes, screen-grab eyedropper, nested right-click menu, rich status bar. The Editor follows the selected page; layout persists to a foo.txt.board sidecar; the .txt stays pure plaintext. - scp-on-save to a Tailscale Mac (key auth, in-flight/quit drain). - HTML scraping pipeline: custom selector engine + Discogs profile, ingests to Postgres (psql) into a marketplace_snapshots schema. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
8.5 KiB
TextPlus
A TextEdit-style plaintext-only editor for macOS that can automatically
scp every file you save to another Mac on your Tailscale network — and that
grew a Markdown preview, themes, an HTML→Postgres scraping pipeline, and a
Freeform-style spatial board.
No rich text, no smart quotes, no autocorrect — just text, plus a lot of hooks.
Three view modes
The toolbar segmented control (top-left) and ⌃⌘1 / ⌃⌘2 / ⌃⌘3 switch between:
- Editor — the plain text editor: line-number gutter, 5 themes, soft wrap, font controls, find.
- Split — editor on the left, a live Markdown preview on the right.
- Board — a Freeform-style infinite, zoomable canvas. Wheel shortcuts: ⌃-scroll zooms (centered on the pointer), ⌥-scroll pans horizontally, ⌘-scroll pans vertically; a plain wheel scrolls normally and pinch zooms. Your document lives here as a page card you can place anywhere; arrange clean boxes, drop images, and sketch with the pen. Double-click or press Return on the page to focus it — the board zooms in and the card becomes a normal text editor bound to the document. Click empty space or press ⎋ to zoom back out.
Board objects:
- Page card (the main one) — is the
.txt; editing it edits the document. Set its size with the ruler button (US Letter / A4 / Legal / Square / Wide 16:9 / Note) and the P/L orientation toggle in the palette. - Text pages ("New text page" button) — extra pages created at the current
page size, freely resizable; saved in the sidecar, not the
.txt. Select a text page and switch to the Editor tab to edit it full-screen — the Editor follows whichever page is selected on the board (the status bar shows "Board note" when you're editing one of these instead of the main page). - Boxes (Box tool, drag to draw) — clean translucent rectangles for diagrams, draggable / resizable / grid-snapped.
- Images — the Add Image button, paste (⌘V), or drag a file onto the board.
- Freehand — pen / marker / line / arrow / oval / eraser, drawn behind the cards.
One color drives the pen, shapes, and boxes together — click the color well for the full picker, or the eyedropper to grab a color from anywhere on screen. Zoom to Fit frames everything; the board exports to PNG or sends to your tailnet like a saved file.
Right-click anywhere on the board for a full nested menu — tools, new page/image, page size & orientation, zoom, color (swatches / screen-pick / picker), view mode, theme, undo/redo, export/send, plus per-card actions (edit, bring to front / send to back, duplicate, delete) when you click a card. Everything is reachable from the mouse.
The status bar is mode-aware: in the editor it shows line/column, selection, words/chars/lines and reading time; in board mode it shows the active tool, page size/orientation (or the selected card's dimensions), zoom %, and card count.
Settings → Board (⌘,) sets the default page size, orientation, and grid snapping for new documents.
The board layout persists in a sidecar bundle next to the document
(foo.txt.board/: board.json + image assets + a preview.png). The .txt
itself stays pure plaintext — open it in any other editor and you just see the
page's prose. Delete the foo.txt.board/ folder to reset the board.
Build
./build.sh
Produces TextPlus.app in this folder. Drag it to /Applications if you want
it in Launchpad/Spotlight, or just open TextPlus.app.
Requires only the Xcode Command Line Tools (xcode-select --install) — no Xcode.
Set up sync
- Open TextPlus → Settings… (⌘,)
- Remote:
user@hostof the target Mac. Tailscale MagicDNS names work (mini@studio), as do raw Tailscale IPs (mini@100.x.y.z) and any alias from your~/.ssh/config(then you can leave out theuser@part). - Folder: the directory on the target Mac to drop files into,
e.g.
/Users/you/Documents/inbox. Leave empty for the remote home folder. - Hit Test Connection — it checks both SSH auth and that the folder exists.
- Turn on Copy file to remote Mac on every save.
Now every ⌘S also fires an scp in the background. The window subtitle shows
Syncing… → Synced ✓ 10:42 or the error if it failed. File → Sync Now
(⌥⌘S) re-pushes the current file any time, even with the toggle off.
One-time SSH key setup (if ssh user@host still asks for a password)
On the target Mac: System Settings → General → Sharing → Remote Login on.
On this Mac:
ssh-keygen -t ed25519 # skip if ~/.ssh/id_ed25519 already exists
ssh-copy-id user@host # copies your key to the target Mac
ssh user@host # should now log in with no password prompt
TextPlus runs scp with BatchMode=yes, so it will never pop a password
prompt — if key auth isn't working, the sync just fails with the error shown
in the window subtitle.
HTML scraping tools
Paste <body> HTML copied from the browser inspector into a document, then:
- Tools → List Div & Class Selectors (⌥⌘L) — opens a new document listing
every unique selector (
tag#id.class) with a count and a sample of its text, sorted by count so repeating listing rows float to the top. - Tools → Extract Text for Selector… (⌥⌘E) — type a selector and get every
match's text in a new document, one match per line, table cells tab-separated.
Supports
tag,.class,#id, combos (tr.shortcut_navigable), and space-separated descendant chains (table.table_block td). No child combinator (>) — use a space instead.
Selector extras (work in the Extract dialog and in scrape profiles):
@attrat the end pulls an attribute instead of text, as the first column:a.item_description_title@href,span.price@data-pricevalue.[attr]requires the attribute,[!attr]requires its absence,[attr=value]exact match,[attr^=value]prefix:.item_condition span[!class],a[href^=/seller/].
Workflow: paste → ⌥⌘L → spot the selector with the right count/sample → ⌥⌘E.
Postgres ingest (the scraper pipeline)
This turns the editor into the front end of your Discogs snapshot database.
Copy a marketplace page's <body> from the inspector, paste it into a
document with the page URL on the first line, and either:
- Tools → Send Page to Postgres (⌥⌘P) — parse and store right now, or
- turn on auto-ingest in Settings so every save stores automatically.
What gets stored (tables auto-created on first connect):
| Table | Contents |
|---|---|
tp_pages |
one row per captured page (url, source, content hash, counts, timestamp) |
tp_selectors |
the full selector summary for that page |
tp_rows |
every extracted row as JSONB, keyed to the page |
discogs_sellers |
seller_id, username — upserted by data-seller-id |
marketplace_snapshots |
one row per listing, upserted by data-item-id |
marketplace_snapshots matches the schema from your scraper notes:
item_id, release_id (your bridge to the local XML dump), seller_id,
title, listing URL, media/sleeve condition, currency, price_value,
shipping_value, and first_seen / last_seen / is_active for tracking
state over time. Re-ingesting a page upserts: existing listings get
last_seen and price refreshed (first_seen preserved); new ones inserted.
Setup: Settings → Postgres. The connection URI defaults to
postgresql://localhost:5433/scraperrr (port 5433 because a Tailscale ssh
tunnel already owns 5432 on this Mac). Hit Test & Create Tables, then flip
auto-ingest if you want it. Ingest shells out to psql
(brew install postgresql@17); it does not fetch pages — you paste the
DOM, which sidesteps Cloudflare entirely, exactly as your notes planned.
Recognized pages (the discogs_marketplace profile): sell/list,
sell/mywants, and seller/<name>/profile. Any other HTML still gets its
page + selector summary stored; only the structured listing tables are
Discogs-specific.
Notes
- Sync fires only on explicit saves (⌘S / Save As), never on autosave — half-typed files don't get pushed.
- Files are written as UTF-8. Opening files in other encodings works (encoding is auto-detected); they're converted to UTF-8 on save.
StrictHostKeyChecking=accept-newis used: first connection to a new host is trusted automatically (fine on a tailnet), but a changed host key is still rejected.- Settings live in
defaults read local.textplus.