# 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 ```sh ./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: ```sh 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 `` 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 `` 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//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`.