Bidirectional sync between a Slidev markdown deck and Google Slides as native, editable objects
Project description
slidesync
Bidirectional sync between a Slidev markdown deck and Google Slides — as native, editable objects (title/body/bullets/tables/positioned images, brand-styled text boxes), not pasted screenshots.
Version: 0.3.0
uvx slidesync --help # run without installing
pip install slidesync # or install the CLI + library
Why
Exporting a deck to images gives you something you can't edit; pasting markdown
by hand gives you something you can't version. slidesync keeps a .slidev.md
file as the source of truth and renders it into real Slides objects, so the
result stays fully editable in Google Slides — and pull reconstructs the
markdown back from those objects, so the loop is reversible.
push— markdown → Slides (idempotent upsert, never a blind append).pull— Slides → markdown (handles multi-text-box and externally-authored decks, bullet nesting, tables, images, and speaker notes).roundtrip— push a sample to a scratch deck, pull it back, assert the two are semantically identical, delete the scratch deck.
Auth (no setup)
Auth is borrowed from the gog CLI — no separate
OAuth client. slidesync reads the client id/secret from
~/Library/Application Support/gogcli/credentials.json and the refresh token via
gog auth tokens export, then mints a short-lived access token. The stored token
already carries the slides + drive scopes; the Slides API must be enabled on
the gog Cloud project. Override the account with --account or
$SLIDESYNC_ACCOUNT. (Currently macOS-only — it reads gog's macOS Application
Support path.)
Commands
| Command | Purpose |
|---|---|
slidesync push <file.slidev.md> [--deck ID] [--new "Title"] [--anchor SLIDE] [--prune] [--force] |
markdown → Slides |
slidesync pull <deckId> --out <file.md> [--all] |
Slides → markdown (--all includes non-managed slides) |
slidesync roundtrip [--keep] |
self-test: push a sample, pull, assert identical |
slidesync layouts <deckId> |
list a deck's theme layouts + placeholders |
slidesync make-templates <deckId> |
inject branded {{token}} template slides |
slidesync comments <deckId> |
list comment threads as JSON (page anchor, author, content, replies) |
slidesync sync <file.slidev.md> [--deck ID] |
report drift vs the live deck — comments, live edits, conflicts (exit 1 on drift) |
push resolves the target deck from (in order) --deck, --new, or a top-level
deck: frontmatter key. Relative image paths resolve against the markdown file's
directory.
slidesync push deck.slidev.md # targets `deck:` frontmatter
slidesync push deck.slidev.md --new "Talk"
slidesync pull <id> --out deck.slidev.md
slidesync roundtrip
Idempotent sync (upsert)
Each managed slide is created with objectId = s2g_<keyHash>_<contentHash>.
keyHash = per-slide id: frontmatter, else title slug, else index (survives
edits/reorders); contentHash is over a canonical render, so push → pull → push
is a no-op. Diff per run: identical hash → skip; same key, new content → replace;
new key → create. Removed slides are kept unless --prune. Only s2g_
slides are ever touched — hand-authored slides are invisible to the sync. A
hidden <!-- s2g {...} --> marker in speaker notes carries the human id, image
path, template vars — and, for template slides, the authored body markdown
(base64) — so pull recovers the source verbatim.
Sync & drift (detection, not resolution)
The marker's last-pushed source is a true per-slide merge base, so sync
classifies each slide three-way without timestamps (the Slides API has no
per-slide edit times — only file-level modifiedTime; the marker's at stamp
records when we last pushed each slide):
| status | meaning | action |
|---|---|---|
clean / converged |
nothing changed, or both sides made the same change | — |
local-edit |
markdown changed, deck untouched | push (normal) |
live-drift |
slide edited in Google Slides | fold the printed diff into the markdown, or push --force to clobber |
conflict |
both changed since last push | resolve the two printed diffs by hand/LLM, then push |
Unresolved comment threads print as ready-to-paste <!-- @Author: text -->
blocks on their slide (replies as extra @Author: lines) — paste them into the
source, where they round-trip from then on. Threads anchor to slide objectIds,
so a re-render orphans them: sync reports those too. Capture before you push.
Markdown dialect
Top-level frontmatter: theme:, deck:. Slides separated by ---; each slide
may have its own frontmatter (id:, template:, layout:).
# h1= headline,## h2above an# h1= kicker; a lone##is the title.- Bullets
-/*; ordered1.(nest with 2-space indent). Inline**bold**/*italic*/`code`/[link](url). GFM tables.images (uploaded to Drive;altbecomes the accessibility description, round-tripped on pull). Blank lines preserved as spacing.<!-- notes -->become speaker notes — and round-trip as comments, in place: template slides carry their authored source in the marker, sopullre-emits each comment where it was written instead of one merged trailing blob. Speaker notes edited live in Slides come back as one extra trailing comment. - Internal links:
[text](#slide-id)becomes a native Slides link to the slide whoseid:(or title slug) isslide-id.
Built-in brand kit (IBM Plex; red #C0392B kicker)
Select per slide via template: — native styled boxes, no in-deck templates:
dark/title, appendix, question/label, topic, content,
graph/full (single full-bleed image), prompt/code (verbatim monospace).
Title cards (dark/title/appendix) render body lines as a small dimmed
byline beneath the headline (e.g. Project · Presenter) — they still have
no linkable body region.
Slides with no template: fall back to a generative path (section /
title+body / table / image) that also brands the background + IBM Plex.
Custom slides (diagrams) — pull-authoritative
Give a slide a fenced ```gslides block holding literal Slides API
requests (use __PAGE__ for the slide page id). Sync is pull-authoritative /
push-if-missing: the Slides copy is the source of truth — push only creates
the slide when missing, pull captures the live drawing back into the block.
Development
uv sync
uv run pytest -q # offline tests (no network/auth)
Releases publish to PyPI via Trusted Publishing (OIDC) on a v*.*.* tag — see
.github/workflows/release.yml. Bump with uvx bumpver update --patch.
Caveats
- Slidev-only constructs (
<v-clicks>,<div grid>, CSS) are flattened/stripped — this is a content mapper, not a CSS renderer. - On
pull, the slide model holds a single image, so a slide with multiple images keeps the first; imagecontentUrls from foreign decks are ephemeral. - Verbatim-source markers are seeded at push time, so comment preservation
applies from the first push with v0.2+ (older slides re-render once: the
content hash is now over the authored source). Generative-path slides (no
template:) still merge comments into a single trailing comment on pull, since their live Slides edits — not the marker — are the source of truth.
License
MIT © Daniel Hails
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file slidesync-0.3.0.tar.gz.
File metadata
- Download URL: slidesync-0.3.0.tar.gz
- Upload date:
- Size: 38.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
6eb384c30345d7dfac06754f5af1063fa44f4551c1cce4e44ffafb39c3d229c4
|
|
| MD5 |
4baf9f9a6d77c8d14087709b68b07dd4
|
|
| BLAKE2b-256 |
1ac7e0f1e28b8344bbbe727e8581e59083835f645c5e035c0b8362db952b7d96
|
Provenance
The following attestation bundles were made for slidesync-0.3.0.tar.gz:
Publisher:
release.yml on DJRHails/slidesync
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
slidesync-0.3.0.tar.gz -
Subject digest:
6eb384c30345d7dfac06754f5af1063fa44f4551c1cce4e44ffafb39c3d229c4 - Sigstore transparency entry: 1803881416
- Sigstore integration time:
-
Permalink:
DJRHails/slidesync@16253e24b8a090189a6b95ceb64a495a2cc83aae -
Branch / Tag:
refs/tags/0.3.0 - Owner: https://github.com/DJRHails
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@16253e24b8a090189a6b95ceb64a495a2cc83aae -
Trigger Event:
push
-
Statement type:
File details
Details for the file slidesync-0.3.0-py3-none-any.whl.
File metadata
- Download URL: slidesync-0.3.0-py3-none-any.whl
- Upload date:
- Size: 31.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d35ce7f8a743be8ef1444648b1848a4c1ca1633586716cbf88ba5d38b932113a
|
|
| MD5 |
8651cc5340ae15b1eff95e6c154ede0c
|
|
| BLAKE2b-256 |
7a8b40c6b62ec0fefbdc45095d8764161749e3304d62d7ac757ffce39db32f99
|
Provenance
The following attestation bundles were made for slidesync-0.3.0-py3-none-any.whl:
Publisher:
release.yml on DJRHails/slidesync
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
slidesync-0.3.0-py3-none-any.whl -
Subject digest:
d35ce7f8a743be8ef1444648b1848a4c1ca1633586716cbf88ba5d38b932113a - Sigstore transparency entry: 1803881512
- Sigstore integration time:
-
Permalink:
DJRHails/slidesync@16253e24b8a090189a6b95ceb64a495a2cc83aae -
Branch / Tag:
refs/tags/0.3.0 - Owner: https://github.com/DJRHails
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@16253e24b8a090189a6b95ceb64a495a2cc83aae -
Trigger Event:
push
-
Statement type: