Metadata-Version: 2.5
Name: beamboard
Version: 0.0.1
Summary: Beam your clipboard between devices: the pbd server plus the pb client
Author-email: Jonas Eschmann <jonas.eschmann@gmail.com>
Keywords: clipboard,pasteboard,pbcopy,pbpaste,self-hosted,sync
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Utilities
Requires-Python: >=3.8
Description-Content-Type: text/markdown

# beamboard — pasteboard sync

Self-hosted clipboard sync for devices on a Tailnet (or any private network).
Three small programs (the two Python ones ship as one package, `beamboard`),
one tiny HTTP+JSON protocol:

```
 macOS app  ──┐                      ┌── TrueNAS docker-compose
 (menubar,    ├── HTTP over Tailnet ─┤    pbd + SQLite
  pasteboard  │                      └── history capped at N clips
  sync)       │
 pb CLI    ───┘  (copy / paste / history / watch)
```

The server is the single source of truth; every clip gets a monotonically
increasing `seq`. Clients are stateless and reconcile against `seq`.
Everything is plain text, stdlib-only Python, and dependency-free Swift.

## Server (TrueNAS / docker-compose)

```sh
cd server
docker compose up -d --build
```

Point the volume in `docker-compose.yml` at a dataset
(e.g. `/mnt/tank/apps/pb:/data`, writable by uid 1000) for persistent history.

Or run it anywhere with Python: `pipx install beamboard` (from a checkout:
`pipx install .`), then `PB_DATA=./data pbd` (alias: `pb serve`). No dependencies.

| Env | Default | |
|---|---|---|
| `PB_PORT` | `8737` | listen port |
| `PB_DATA` | `~/.local/share/pb` (`/data` in Docker) | directory for `pb.db` |
| `PB_TOKEN` | *(unset)* | optional shared bearer token; unset = no auth (trust the Tailnet) |
| `PB_HISTORY` | `100` | clips to keep |
| `PB_MAX_BYTES` | `1048576` | max clip size |

## CLI client (Linux or anywhere)

```sh
pipx install beamboard     # from a checkout: pipx install .   (or: make install)
pb config url http://truenas:8737   # saved to ~/.config/pb/config.json
# pb config token ...               # if the server sets PB_TOKEN

echo "text" | pb copy      # copy to the shared pasteboard
pb paste > file.txt        # paste from it (byte-verbatim)
pb history [n] [--json]    # recent clips
pb watch                   # print each new clip as it arrives (pipeable)
```

The same stdlib-only package (`beamboard`) provides the `pbd` server and the
`pb` command, also installed as `beamboard` (`beamboard/server.py` and `cli.py`).
`pb copy`/`pb paste` behave like macOS `pbcopy`/`pbpaste`; a symlink or alias
named `pbcopy`/`pbpaste` pointing at `pb` restores the old spelling.
`pb config` shows the effective settings; `PB_URL`, `PB_TOKEN` and `PB_DEVICE`
override the config file (device defaults to the hostname). `make check` builds
the sdist and wheel and validates them for PyPI.

## macOS menubar app

```sh
make -C mac app        # builds BeamBoard.app (SwiftPM, no Xcode project)
make -C mac install    # copies it to /Applications
```

Open BeamBoard, click the clipboard icon in the menu bar, and set the server URL
(and token) in the settings. The app then:

- watches the system pasteboard (polls `changeCount` twice a second — the
  pasteboard has no change notifications) and pushes new local clips,
- long-polls the server and writes clips from other devices into the
  system pasteboard,
- shows the shared history in the menu bar; clicking an entry copies it.

It never overwrites your pasteboard at launch — syncing starts with the first
actual change on either side. Requires macOS 13+. The build is ad-hoc signed;
add it to Login Items for autostart.

## Protocol

Clip: `{seq: int, ts: float, mime: "text/plain", text: str, device: str}`

| Endpoint | Behavior |
|---|---|
| `GET /clip` | latest clip, `204` if empty |
| `POST /clip` `{text, device}` | store new head, returns `{seq}`; identical to current head → deduped (returns existing seq) |
| `GET /history?limit=50` | recent clips, newest first |
| `GET /watch?since=SEQ&timeout=30` | long-poll: latest clip once head `seq > SEQ`, else `204` on timeout |
| `GET /healthz` | `200 ok` (never requires auth) |

With `PB_TOKEN` set, all other endpoints require `Authorization: Bearer <token>`.
Long-polling keeps every client a dumb HTTP GET loop — no streams, no state,
self-healing across server restarts.

Echo loops are broken by three cheap layers: the server dedups identical
consecutive text, the macOS app skips pasteboard changes it caused itself,
and the `device` field lets clients ignore their own clips from `/watch`.

## Security

There is no TLS and (by default) no auth: the design assumes the server is
only reachable over a trusted private network such as a Tailnet. Set
`PB_TOKEN` for a cheap second layer. Do not expose the port to the internet.
