R3 is a personal Readwise Reader -> reMarkable -> Readwise highlight loop.
The problem it tries to solve is narrow: Readwise Reader is where saved articles arrive, but the reMarkable is where longer reading and highlighting feels best. R3 sends Reader articles to the tablet, lets reading happen there at its own pace, then pulls text-backed tablet highlights back into Readwise. Reflect and other note systems can stay downstream through Readwise's existing integrations.
This repo is an experimental spike, not a polished product. The current direction is a pragmatic "Send to reMarkable" workflow rather than full two-way highlight sync between platforms.
- Source of truth for saved articles: Readwise Reader.
- Delivery format for the tablet: EPUB, prefixed as
[RW] {title}.epub. - Transport: reMarkable USB Web at
http://10.11.99.1. - Readwise integration: the maintained
readwiseCLI, not direct HTTP calls. - Local state: SQLite in
state/r3.db. - Annotation recovery: reMarkable OS 3.26
.rmdocarchives, reading.rmv6SceneGlyphItemBlocktext highlights viarmscene. - Downstream notes: handled outside R3 by Readwise integrations.
- Lists Readwise Reader docs in inbox + later updated within the
last 6 months (configurable via
--cutoff-months). - Uploads the ones the tablet doesn't already have, named
[RW] {title}.epub. - Downloads any tablet doc that's been modified since the last sync,
extracts text-backed highlights from the OS 3.26
.rmfiles. - Writes recovered highlights back to Readwise via
readwise reader-create-highlight(with a generic-highlight fallback when the recovered text doesn't match a Reader HTML element). - Surfaces a macOS notification with the run summary; per-run details
land in
state/logs/sync-<timestamp>.log.
Ordering: new uploads are sequenced by Reader's last_moved_at
ascending, so the most recently moved/saved Reader doc ends up with
the freshest tablet ModifiedClient — i.e. on top of the tablet's
"Most recent" view, matching Reader's ordering.
Known v1 limitation: moving a doc in Reader after it's already on the tablet doesn't re-rank the existing tablet copy (its
ModifiedClientis locked at upload time, and USB Web has no delete endpoint to clear a re-upload's orphan). v2 cloud sync via rmapi will fix this — until then, re-ranking requires a manual delete on the tablet followed byr3-sync --resend <reader_id>.
Another intentional limitation: R3 does not try to mirror highlights created directly in Readwise back onto the tablet. For now, tablet highlights are the write-back source.
Dedup rules:
- Tablet delete is sticky. If r3 finds a doc it previously uploaded
is now gone from the tablet, it marks the doc
removed-by-userand will not re-upload. User3 sync --resend <reader_document_id>or--resend all-missingto re-send. - Writebacks cap at 3 attempts. Failed writebacks are persisted
with
attemptsandlast_error. After 3 attempts, they're skipped permanently. User3 sync --retry-writebacksto clear non-applied rows and retry.
Requirements:
- Python 3.11+
uv- authenticated
readwiseCLI - a reMarkable tablet with USB Web enabled
uv sync
readwise --version # Readwise CLI must be installed and authenticated
readwise config showOn the tablet: connect by USB, unlock, enable Settings -> Storage -> USB web interface.
The tablet's USB Web interface is only active while the device is awake. If
r3 syncreports "USB Web unreachable", tap the tablet screen to wake it, then re-run.
Optional fish wrapper (so you can run r3-sync from anywhere):
ln -sfn "$(pwd)/scripts/r3-sync.fish" ~/.config/fish/functions/r3-sync.fishr3-sync # full sync pass
r3-sync -v # stream all log lines to stderr while running
r3-sync --dry-run # plan without rendering, uploading, or writing
r3-sync --no-notify # suppress the macOS notification
r3-sync --resend <reader_id> # un-stick a manually-deleted tablet doc
r3-sync --resend all-missing
r3-sync --retry-writebacks # retry failed writebacks past the 3-attempt cap
r3-sync --cutoff-months 3 # narrow the source setWhile running, r3 sync prints ==> ... progress lines for each phase
boundary and per-document step (e.g. ==> [3/12] uploading: The Paper Loop). With -v, the full INFO stream from the per-run log file is
also echoed to stderr.
Equivalent without the wrapper:
uv run python -m r3 sync [flags]The first successful run creates state/r3.db, renders EPUBs under
output/sync/, uploads missing Reader articles to the tablet, and records the
tablet document IDs for future extraction/write-back passes.
uv run python -m r3 db list-documents # what r3 knows about
uv run python -m r3 db list-highlights # recovered highlights
uv run python -m r3 db writeback-stats # writeback status counts
uv run python -m r3 db reset # drop and recreate the DB
uv run python -m r3 readwise list --location new --updated-after 2026-01-01T00:00:00Z
uv run python -m r3 remarkable status
uv run python -m r3 remarkable listuv run python -m unittest discover -s testssrc/r3/ # the package
cli.py # argparse, lock acquisition, notification dispatch
sync.py # run_sync orchestration + SyncSummary
config.py # SyncConfig + default paths
storage.py # SQLite schema + upsert/state helpers
readwise.py # thin wrapper around the readwise CLI
remarkable.py # .rmdoc -> snapshot -> recovered highlights
usb.py # reMarkable USB Web client (10.11.99.1)
rendering.py # Reader HTML -> EPUB / PDF / preview HTML
html_matching.py # recovered text -> Reader html_content element
writeback.py # build readwise-create-highlight actions
article.py # Article dataclass + Reader payload normalize
notify.py # osascript notification dispatch
tests/test_r3.py # unit tests against the fixtures
fixtures/ # synthetic Reader payload + sanitized .rm snapshot
scripts/r3-sync.fish # fish wrapper (symlink into ~/.config/fish/functions)
docs/ # spike plan, snapshot contract
state/ # local state (gitignored)
r3.db # SQLite sync state
r3.lock # fcntl flock for r3 sync
logs/sync-<timestamp>.log # per-run log
output/ # rendered artifacts (gitignored)
sync/<reader_id>.epub # EPUBs uploaded by r3 sync
docs/spike-plan.mdrecords the PDF -> EPUB spike and product direction.docs/remarkable-snapshot-contract.mddocuments the OS 3.26 annotation shape this extractor currently understands.