deckz
deckz is a tool to manage a large number of slide decks (Markdown compiled with Typst), shared by several
people, with slides ("sections") shared across decks. It is not meant to be
usable out of the box by people who stumble upon this repository: it enforces
strong conventions on the layout of the repository it operates on. Please
open an issue if you want more details or want to discuss this approach.
Installation
With pip:
pip install deckz
Shell completion
Run deckz --help and look for the --install-completion /
--show-completion flags to set up completion for your shell
interactively, or run deckz generate-completion {bash,zsh,fish} to print
the completion script yourself (e.g. to source from your own dotfiles).
Repository layout
deckz expects to run inside a git repository organized like this:
root (git repository)
├── deckz.yml
├── variables.yml
├── templates
│ ├── assets_builders.py
│ └── jinja2
│ ├── main.typ
│ └── env.py
├── content
│ └── some-section
│ ├── some-section.yml
│ ├── intro.md
│ └── advanced.md
├── assets
│ ├── img
│ ├── plt
│ └── pltly
├── company1
│ ├── variables.yml
│ └── deck1
│ ├── deck.yml
│ ├── variables.yml
│ └── content
└── company2
└── deck2
├── deck.yml
└── content
content: reusable sections (Markdown content files), shared across decks.assets: everything else shared across decks (images, generated figures, or any other kind of asset your owntemplates/assets_builders.pyproduces).deckzdoesn't know or care what subdirectories live underassets-- it symlinks every one of them into each build directory, sointro.mdcan referenceplt/some-figure.svgthe same way regardless of what else is in there. Theplt/pltlynames above are just this repo's own convention, set up by itstemplates/assets_builders.py-- see Assets builders.templates/jinja2/main.typ: the Jinja2 template used to render every deck's main Typst file.templates/jinja2/env.py: the Python module that configures the Jinja2 environment(s) used to render content files -- see Content files and the Jinja2 environment.templates/assets_builders.py: the Python module that builds this repo's assets (generated figures, or anything else) -- see Assets builders.- Each deck is a directory containing a
deck.yml(its definition) and optionally acontentdirectory for files local to that deck. variables.ymlfiles can be placed at any level of the directory hierarchy between the git root and a deck to avoid repeating values (e.g. one per client/company, shared by all of that client's decks).
Configuration
deckz.yml
At the root of the repository, deckz.yml holds settings, not content, e.g.:
pandoc_command:
- pandoc
- -f
- markdown-auto_identifiers
- -t
- typst
- --lua-filter={templates_dir}/pandoc/filters/boxes.lua
typst_parallel_compilations: 1
typst_parallel_compilations: how many PDFs compile at once (default 1).deckzcompiles the main Typst file (paths.jinja2_main_template) with thetypstPython bindings, each PDF in its own child process, which gives its memory back when done (Typst can take a few GB on a large deck, and already uses every core within one compilation). Underdeckz run --watch, each PDF's process stays alive instead, so rebuilds are incremental: on a 145-page deck, an edit re-renders every PDF in about a second.pandoc_command: howdeckzinvokespandocto convert a rendered Markdown content file to the.typfragment that gets#included, including any--lua-filter=...your own conventions need (fenced divs for admonition boxes, external code-file inclusion, etc.) -- entirely up to you,deckzhas no opinion here.pandocis invoked with a working directory matching the content fragment's own (possibly nested) position under the build directory, so any path an argument needs (e.g. a--lua-filter) should be written as{git_dir}or{templates_dir}, substituted with the resolved absolute path -- a bare relative path would not resolve consistently.file_extensions: the extensions tried, in order, when resolving a file include. Defaults to[".md"].
deckz.yml files are merged, in order, from the git root, from the user's
config directory (XDG-compliant, e.g.
$HOME/.config/deckz/deckz.yml on GNU/Linux), and from the current
directory (and its ancestors up to the git root). Run
deckz show settings to inspect the resolved result.
variables.yml
variables.yml files hold the values injected into the Jinja2 templates,
merged the same way (git root → user config directory → current directory
and its ancestors), so a value set closer to a deck overrides one set
higher up. Run deckz show variables to inspect the resolved result for
the current deck.
Example:
company_name: Company
company_logo: img/logo.png
company_logo_height: 1cm
deck_title: Machine Learning and COVID-19
presentation_size: 10pt
These variables are passed to templates/jinja2/main.typ, and to every
content file, as a variables mapping; what the templates do with them is up to
your own template and Jinja2 environment, not something deckz imposes --
see Content files and the Jinja2 environment.
deck.yml
Defines a deck's parts and, for each part, the sections and files it includes:
name: ABC
parts:
- name: p1
title: Part 1
sections:
- $first-section@standard
- about
- name: p2
title:
fr: Partie 2
en: Part 2
sections:
- $first-section@light
Includes can point to a file (path/to/file) or to a shared section with a
given flavor ($path/to/section@flavor); either form can be given a custom
title with path: My title / $path@flavor: My title (or, as with p2's
title above, a {fr: ..., en: ...} map instead of a plain string, when the
text actually differs by language -- see Titles, variables and
--en).
Shared sections
A shared section lives under content (or a deck's local content
directory) and has a sibling .yml file describing its flavors, e.g.
content/first-section/first-section.yml:
title: First section
default_titles:
intro: Introduction
advanced:
fr: Approfondissement
en: Advanced
flavors:
- name: standard
includes:
- intro
- advanced
- name: light
includes:
- intro
Flavor-level variables
A flavor can also set its own variables, merged into the rendering
context of its content (and, cascading down, of every section it includes)
on top of variables.yml -- the way to give two flavors of the same
section genuinely different content instead of duplicating the section
itself:
title: Keras
variables_to_define:
- depth: [shallow, deep]
flavors:
- name: light
variables: { depth: shallow }
includes:
- body
- name: full
variables: { depth: deep }
includes:
- body
body.md then reads {{ variables.depth }} the same way it would read any
deck-wide variable. variables_to_define is a contract, not a default: it
never supplies a value itself, it just declares that every flavor below
must set that key itself (optionally restricted to the given list of
values) -- a flavor missing one, or setting one out of range, fails to
parse. deckz check variables statically surveys every shared flavor and
every real deck for two kinds of drift this contract doesn't catch by
itself: a fragment reading a variables.xxx name nothing resolved at that
point ever sets, and a declared name no fragment anywhere under its section
ever reads.
Titles, variables and --en
There is only ever one deck.yml/section .yml -- English is never a
separate deck or a duplicated section, only a --en flag on deckz run
(and deckz check variables):
- Any title (
deck.ymlpart/include titles, a section'stitle,default_titlesvalues, a flavor's title) and anyvariables.ymlvalue can be a plain string, used as-is in every language -- the right choice whenever the text is language-neutral (a proper noun, a number, code, or simply not translated yet) -- or an explicit{fr: ..., en: ...}map when it actually needs to differ by language, as inintro/advancedabove. - A translated body file lives at
<parent-dir>/en/<filename>, a sibling of the French file, whateverparent-diris -- a shared section directory, a deck's localcontentdirectory, or any nested subdirectory of either. Section/flavor structure itself is never duplicated for English. --ennever silently falls back to French for a{fr: ..., en: ...}map that's missing itsenkey, or for a resolved file with noen/sibling: both fail the build immediately, so a translation that was started but left incomplete can't accidentally ship untranslated. A plain string, on the other hand, is never a gap -- it means "the same in every language" by design.deckz i18n missing-enaudits a deck for both kinds of gap (blocking and merely informational) without needing to actually compile it.- fr and
--enbuilds write to separate output paths (anen/subdirectory under.build/pdf), so both can be built from the same checkout without clobbering each other.
Content files and the Jinja2 environment
Content files (a section's or a deck's included files) are plain text,
templated with Jinja2, using whatever delimiters and filters
templates/jinja2/env.py configures -- deckz itself has no opinion on
this beyond providing the mechanism. That module must expose:
from jinja2 import Environment
def environment_for(suffix: str) -> Environment:
...
called once per content-file suffix deckz renders (typically once for
the .typ main template, once for .md content), so different sources can
use different delimiters/filters. This is also where you define any
macro/filter your content relies on, e.g. an image filter emitting
.
Whatever filter you use to reference an asset file (an image, a generated
plot, ...) should call the assets_metadata_retriever context
variable deckz injects into every render, e.g. from a filter function:
from jinja2 import pass_context
from jinja2.runtime import Context
@pass_context
def image(context: Context, path: str) -> str:
metadata = context["assets_metadata_retriever"](path)
...
This is the hook that keeps deckz asset search/deckz asset deps (and
the i18n tooling) accurate: skip it, and asset usage/licensing detection
silently misses whatever your filter references.
Assets builders
deckz itself has no opinion on what assets a deck needs (generated
figures, or anything else) or how to build them. Your repo supplies a
Python module (templates/assets_builders.py) exposing:
from collections.abc import Iterable
from pathlib import Path
from deckz.components.protocols import AssetsBuilderProtocol, CompilerProtocol
def assets_builders(
assets_dir: Path, compiler: CompilerProtocol
) -> Iterable[AssetsBuilderProtocol]:
...
called once (by deckz run/deckz run shared/deckz run all/deckz run assets) to obtain every builder to run. Each
builder implements AssetsBuilderProtocol:
from collections.abc import Iterable
from pathlib import Path
class MyAssetsBuilder:
def build_assets(self) -> None:
... # write output files somewhere under assets_dir
def watched_dirs(self) -> Iterable[Path]:
... # source directories `deckz run assets --watch` should watch
build_assets should write its output somewhere under assets_dir --
deckz symlinks every top-level directory found there into every build
directory, without needing to know their names (see Repository
layout). watched_dirs only matters for deckz run assets --watch: it tells the watch loop which source directories should
trigger a rebuild.
Markdown content and pandoc
Content files are authored in Markdown: deckz renders each one through
its own Jinja2 environment (see above) and then converts the result to the
.typ fragment the main template #includes, by shelling out to
pandoc_command. Slide conventions your content relies on
(admonition boxes, external code-file inclusion, columns, math, ...) are
entirely up to how you write your Markdown and configure pandoc_command
(typically pandoc's own built-ins plus one or more --lua-filter=...
pointing at Lua filters you maintain in this repository, e.g. under
templates/pandoc/) -- deckz only orchestrates the conversion.
Upgrading from 28.x (LaTeX removal)
deckz no longer compiles LaTeX. In a repository still laid out for 28.x:
git mv latex contentat the root, and in every deck directory.- Remove
compilerandbuild_commandfromdeckz.yml(they're now ignored), andfile_extensionsif it only lists.md. - The default main template is now
templates/jinja2/main.typ: apaths.jinja2_main_templateoverride pointing there can go. deckz clean latexis nowdeckz clean content.
Usage
Run deckz --help for the full list of commands, or deckz <command> --help for a specific command. The main ones:
deckz run(alias fordeckz run deck, optionally restricted with--parts) /deckz run file PATH/deckz run section SECTION FLAVOR/deckz run assets: compile the deck in the current directory, a single content file, a specific section flavor, or the project's assets. Add--ento compile the English variant (see Titles, variables and--en), or--watchto recompile on file changes instead of once.run file/run sectionwrite their output under<git_dir>/.run/, not the current deck's ownpdf/.build, and open it once done (add--no-openfor agentic/headless use, where only the printed path matters).deckz run decks/deckz run shared/deckz run all: validate the repository by compiling.run deckscompiles every real deck end to end (by far the slowest for a repo with many decks, since every shared section gets recompiled once per deck that includes it).run sharedcompiles one throwaway deck containing every shared section expanded to every file in its directory (not just the ones some named flavor lists) -- fast enough to run while editing shared content.run alldoes the same plus one extra copy of a section for every deck that locally overrides one of its files.--enis strict: the first gap in translation coverage aborts the run.run shared/run allwrite their throwaway output under<git_dir>/.run/.deckz check variables: statically survey every shared section's every named flavor (not just the ones some deck currently uses) plus every real deck, resolvingvariablesthe same way a real build would. Reports a fragment reading avariables.xxx/variables['xxx']name nothing resolved at that point sets (UNDEFINED), avariables_to_definename no fragment under its section ever reads (UNUSED), a fragment that fails to parse (UNPARSABLE), and a flavor/deck that fails to parse at all, e.g. avariables_to_definecontract violation (STRUCTURAL). Nothing is compiled.deckz show(alias fordeckz show tree): show the resolved tree of sections and files for the current deck.deckz show settings/deckz show variables/deckz show paths: print the resolved settings/variables/file paths for the current directory.deckz deps [SECTION] [FLAVOR]: show shared sections/flavors usage across the repository, including unused ones.deckz search-sections KEYWORDS...: search shared sections by title or Markdown heading.deckz section-flavors SECTION: list a section's flavor names.deckz section-files SECTION FLAVOR: list the files a section+flavor resolves to.deckz flavor rename SECTION OLD NEW: rename a flavor and rewrite all its usages.deckz flavor deduplicate: deduplicate section flavors that are identical up to their name.deckz asset search ASSET/deckz asset deps: find where an asset is used, or find assets missing license metadata.deckz clean/deckz clean all/deckz clean content: remove build directories, or unused shared/local content files.deckz clean allalso removes the whole<git_dir>/.run/scratch tree (run shared/run all/run file/run section), and<git_dir>/.check/(check variables).deckz i18n missing-en [--all]: report fr content/titles/variables with no English counterpart, i.e. what adeckz run --enwould currently fail on.deckz generate-agent-notes: print onboarding notes for an AI coding agent working in the repo (conventions not already covered by--help, e.g. the section/flavor syntax and local-override resolution). Prints to stdout, e.g.deckz generate-agent-notes > CLAUDE.md.deckz generate-completion {bash,zsh,fish}: print a shell completion script (see Shell completion).deckz upload: upload built PDFs to Google Drive.deckz extras issue TITLE [BODY]: create a GitHub issue.deckz extras random REASON: roll a dice and email the result (handy for arbitrary decision-making, e.g. picking who does a task).deckz extras labs NOTEBOOKS...: normalize Jupyter notebooks (files, or directories searched recursively for*.ipynb) to this project's Colab conventions -- collapse every "Solution" markdown heading cell by default, and disable Colab's generative AI features.--dry-runlists what would change without writing.
Documentation
A partial code reference, generated from the docstrings, is published via
mkdocs (see mkdocs.yml and the docs directory).
Metadata
Release files for deckz 29.0.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| deckz-29.0.0.tar.gz | 75.6 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| deckz-29.0.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 176.8 kB
Release files / deckz-29.0.0.tar.gz
| Download URL | deckz-29.0.0.tar.gz |
|---|---|
| Size | 75.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
b1fbebec51a2c796133bf26fdf372a21ad27a526774a4ec926b0cbe732712ef3
|
|
BLAKE2b-256 checksum How to use checksums |
7f25105c42b00472baf722783756a94865d52209f029d44d73f2dd82e5fbc60f
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.12.19 {"installer":{"name":"uv","version":"0.12.19","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / deckz-29.0.0-py3-none-any.whl
| Download URL | deckz-29.0.0-py3-none-any.whl |
|---|---|
| Size | 101.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
333319d28a194aadb5bbfeb8256d82dd0bba1a30203ba1f9ee7119fbe49abd7b
|
|
BLAKE2b-256 checksum How to use checksums |
dfd551739341209b4c232017065ab22316234de2e69c4d46909b14e6d866364d
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.12.19 {"installer":{"name":"uv","version":"0.12.19","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|