Go to file
monster 09972ccde1 TextPlus: native macOS plaintext editor with sync, scraping, and a freeform board
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>
2026-06-14 16:08:59 +10:00
docs TextPlus: native macOS plaintext editor with sync, scraping, and a freeform board 2026-06-14 16:08:59 +10:00
Resources TextPlus: native macOS plaintext editor with sync, scraping, and a freeform board 2026-06-14 16:08:59 +10:00
Sources TextPlus: native macOS plaintext editor with sync, scraping, and a freeform board 2026-06-14 16:08:59 +10:00
.gitignore TextPlus: native macOS plaintext editor with sync, scraping, and a freeform board 2026-06-14 16:08:59 +10:00
build.sh TextPlus: native macOS plaintext editor with sync, scraping, and a freeform board 2026-06-14 16:08:59 +10:00
demo.md TextPlus: native macOS plaintext editor with sync, scraping, and a freeform board 2026-06-14 16:08:59 +10:00
Info.plist TextPlus: native macOS plaintext editor with sync, scraping, and a freeform board 2026-06-14 16:08:59 +10:00
README.md TextPlus: native macOS plaintext editor with sync, scraping, and a freeform board 2026-06-14 16:08:59 +10:00

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

  1. Open TextPlus → Settings… (⌘,)
  2. Remote: user@host of 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 the user@ part).
  3. Folder: the directory on the target Mac to drop files into, e.g. /Users/you/Documents/inbox. Leave empty for the remote home folder.
  4. Hit Test Connection — it checks both SSH auth and that the folder exists.
  5. 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):

  • @attr at 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-new is 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.