logo

Polylux is a package for the typesetting system Typst to create presentation slides, just like you would use the beamer package in LaTeX. (So far, it is much less advanced than beamer, obviously.)

If you haven't heard of things like LaTeX's beamer before, here is how this works: As a rule of thumb, one slide becomes one PDF page, and most PDF viewers can display PDFs in the form of a slide show (usually by hitting the F5-key).

Polylux gives you:

  • Elegant yet powerfull typesetting by the ever-improving Typst.
  • Fully customisable slides.
  • Dynamic slides (or overlays or (dis-)appearing content, or however you want to call it).
  • Decently looking themes.

If you like it, consider giving a star on GitHub!

Why the name?

A polylux is a brand of overhead projectors very common in Eastern German schools (where the main author of this package grew up). It fulfils a similar function to a projector, namely projecting visuals to a wall to aid a presentation. The German term for projector is beamer, and now you might understand how it all comes together. (The original author of the aforementioned LaTeX package is German as well.)

About this book

This book documents all features currently implemented in Polylux. Specifically, it describes the state of the package as it is pulished to the Typst package registry. The main branch of the Polylux repository may contain features not documented here.

Contributing

This package is free and open source. You can find the code on GitHub where you can also create issues or pull requests.

License

Polylux is released under the MIT license.

Getting started

You can find this package in the official Typst package repository. To use it, start your document with

#import "@preview/polylux:0.3.1": *

You now have two options:

  1. use the features of polylux but define every visual aspect yourself,
  2. use one of the provided themes.

We will first explore how you can get started without the support of themes. Later, we will see how you can employ them to get beautiful slides without having to do the design work yourself.

Do it yourself

Let's start with the absolute minimal effort. What characterises a set of slides? Well, each slide (or PDF page, as we already established) has specific dimensions. Some time ago, a 4:3 format was common, nowadays 16:9 is used more often. Typst has those built in:

#set page(paper: "presentation-16-9")

You probably don't want your audience to carry magnifying glasses, so let's set the font size to something readable from the last row:

#set text(size: 25pt)

We should be ready do go to create some actual slides now. We will use the function polylux-slide for this, which is kind of at the core of this package.

// Remember to actually import polylux before this!

#polylux-slide[
  Hello, world!
]

And here is the result (the gray border is not part of the output but it makes the slide easier to see here): helloworld Already kinda looks like a slide, but also a bit boring, maybe. We should add a title slide before that so that our audience actually knows what talk they are attending. Also, let us choose a nicer font and maybe add some colour? We modify the #set page and #set text commands for that:

#set page(paper: "presentation-16-9", fill: teal.lighten(90%))
#set text(size: 25pt, font: "Blogger Sans")

#polylux-slide[
  #set align(horizon + center)
  = My fabulous talk

  Jane Doe

  Conference on Advances in Slide Making
]

titleslide Not bad, right? Another thing that is usually a good idea is to have a title on each slide. That is also no big deal by using off-the-shelf Typst features, so let's modify our first slide:

#polylux-slide[
  == My slide title
  Hello, world!
]

This is starting to look like a real presentation: slidetitle

So what?

To be honest, everything we did so far would have been just as easy without using polylux at all. So why should you care about it?

Consider the following situation: You have a slide where parts of the content appear or disappear, or the colour of some text changes, or some other small-sized change. Would you like to duplicate the whole slide just so to create this affect? And then maintain multiple copies of the same content, making sure never to forget updating all copies when your content evolves? Of course you wouldn't and, gladly, polylux can handle this for you.

This kind of feature is called dynamic content or overlays (loosely speaking, you might also say animations but that might be a bit of a stretch, nothing actually "moves" on PDF pages).

So how does that work in polylux? As a quick example, let's add a little quiz to our slides:

#polylux-slide[
  == A quiz

  What is the capital of the Republic of Benin?

  #uncover(2)[Porto-Novo]
]

quiz Note how two more slides have been created even though we declared only one.

The next sections will explain dynamic content in polylux in all its details.

For reference, here is the full source code for the slides we developed in this section:

#import "@preview/polylux:0.3.1": *

#set page(paper: "presentation-16-9", fill: teal.lighten(90%))
#set text(size: 25pt, font: "Blogger Sans")

#polylux-slide[
  #set align(horizon + center)
  = My fabulous talk

  Jane Doe

  Conference on Advances in Slide Making
]

#polylux-slide[
  == My slide title
  Hello, world!
]

#polylux-slide[
  == A quiz

  What is the capital of the Republic of Benin?

  #uncover(2)[Porto-Novo]
]

Dynamic slides

The PDF format does not (trivially) allow to include animations, as one would be used to from, say, Powerpoint. The solution PDF-based presentation slides use is to create multiple PDF pages for one slide, each with slightly different content. This enables us to have some basic dynamic elements on our slides.

In this book, we will use the term logical slide for a section of content that was created by one call to #polylux-slide, and subslide for a resulting PDF page. Each logical side can have an arbitrary amount of subslides and every subslide is part of exactly one logical slide. Note that the same page number is displayed for all subslides of a logical slide.

As you will see soon, the commands for creating dynamic content all have some way of specifying on what subslides some content is supposed to be shown. One of those subslides has the highest index, of course. Of all those commands with their respective highest subslide to show something, the maximum is take again and that defines the number of PDF pages produced for one logical slide. For example, suppose we have a slide with the following commands:

  • show something on subslides 1 and 3
  • show someting from subslide 2 to subslide 4
  • show someting until subslide 6

This results in 6 PDF pages for this logical slide.

In the LaTeX beamer package, the functionalities described in this part are called "overlays".

Everything discussed here works just as well when you use themes. For simplicity, we will work through the material without them, though.

Reserve space or not?

When you want to specify that a certain piece of content should be displayed one some subslides but not on others, the first question should be what should happen on the subslides it is not displayed on. You could either want

  • that it is completely not existing there, or
  • that it is invisible but it still occupies the space it would need otherwise (see the docs of the #hide function)

The two different behaviours can be achieved using either #only or #uncover, respectively. The intuition behind it is that, in one case, content is only existing on some slides, and, in the other case, it is merely covered when not displayed.

General syntax for #only and #uncover

Both functions are used in the same way. They each take two positional arguments, the first is a description of the subslides the content is supposed to be shown on, the second is the content itself. Note that Typst provides some syntactic sugar for trailing content arguments, namely putting the content block behind the function call.

You could therefore write:

#polylux-slide[
  before #only(2)[*displayed only on subslide 2*] after

  before #uncover(2)[*uncovered only on subslide 2*] after
]

...resulting in

only-uncover

(Note again that the gray border is not part of the slides and that the labels indicating the index of the subslide are also added afterwards.)

You can clearly see the difference in behaviour between only and uncover. In the first line, "after" moves but not in the second line.

In this example, we specified only a single subslide index, resulting in content that is shown on that exact subslide and at no other one. Let's explore more complex rules next.

Complex display rules

There are multiple options to define more complex display rules than a single number.

Array

The simplest extension is to use an array. For example

#polylux-slide[
  #uncover((1, 2, 4))[uncovered only on subslides 1, 2, and 4]
]

results in:

rule-array

The array elements can actually themselves be any kind of rule that is explained on this page.

Interval

You can also provide a (bounded or half-bounded) interval in the form of a dictionary with a beginning and/or an until key:

#polylux-slide[
  #only((beginning: 1, until: 5))[Content displayed on subslides 1, 2, 3, 4, and 5 \ ]
  #only((beginning: 2))[Content displayed on subslide 2 and every following one \ ]
  #only((until: 3))[Content displayed on subslides 1, 2, and 3 \ ]
  #only((:))[Content that is always displayed]
]

results in:

rule-interval

In the last case, you would not need to use #only anyways, obviously.

Convenient syntax as strings

In principle, you can specify every rule using numbers, arrays, and intervals. However, consider having to write

#uncover(((until: 2), 4, (beginning: 6, until: 8), (beginning: 10)))[polylux]

That's only fun the first time. Therefore, we provide a convenient alternative. You can equivalently write:

#uncover("-2, 4, 6-8, 10-")[polylux]

which results in:

rule-string

Much better, right? The spaces are optional, so just use them if you find it more readable.

Unless you are creating those function calls programmaticly, it is a good recommendation to use the single-number syntax (#only(1)[...]) if that suffices and the string syntax for any more complex use case.

Higher level helper functions

With #only and #uncover you can come a long way but there are some reoccurring situations for which helper functions are provided. We call them "higher level" because they use #only and #uncover under the hood and operate on larger pieces of content.

For the common case of succesively revealing content, there are #pause and #one-by-one and its friends. For substituting content, we have #alternatives in different variants. The following sections will describe these functions in detail.

pause to reveal content piece by piece

Consider some code like the following:

#uncover("1-")[first]

#uncover("2-")[second]

#uncover("3-")[third]

The goal here is to uncover parts of the slide one by one, so that an increasing amount of content is shown, but we don't want to specify all subslide indices manually, ideally.

If you have used the LaTeX beamer package before, you might be familiar with the \pause command. It makes everything after it on that slide appear on the next subslide. In Polylux, this works very similar with #pause, so we can equivalently write the above code as:

first #pause

second #pause

third

This results in

pause

#pause should mainly be used when you want to distribute a lot of code onto different subslides. Note that it does not affect content in the same paragraph as itself, for example. For smaller pieces of code, consider one of the functions described next.

More sophisticated piecewise revealing

#one-by-one

#pause may be considered syntactically a bit surprising by some although (or because) it is very convenient to use. If you prefer to signal the grouping of content appearing together syntactically by using scopes, you can use #one-by-one:

#one-by-one[Do you know ][$pi$ ][to a thousand decimal places?]

resulting in

one-by-one

If we still want to uncover certain elements one after the other but starting on a later subslide, we can use the optional start argument of #one-by-one:

#one-by-one(start: 3)[This ][came ][pretty late.]

resulting in

one-by-one-start

This optional start argument exists for all functions displayed on this page.

#line-by-line

#one-by-one is especially useful for arbitrary contents that you want to display in that manner. Sometimes, it produces a bit too much syntactical noise again with all the brackets between content, though. That is especially true if each piece fits into a single line, as for example for a simple bullet list. Instead of

#one-by-one[
  - first
][
  - second
][
  - third
]

you can also write

#line-by-line[
  - first
  - second
  - third
]

resulting in

line-by-line

The content provided as an argument to #line-by-line is parsed as a sequence by Typst with one element per line (hence the name of this function). We then simply iterate over that sequence as if it were given to #one-by-one.

#list-one-by-one

What if you want a more customized bullet list, though? The code above produces a tight list, for example, and maybe you do not want that. All your needs are covered by the #list-one-by-one function:

#list-one-by-one(marker: [--], tight: false)[first][second][third]

resulting in

list-one-by-one

As you can see, you can provide any arguments that the list function accepts.

#enum-one-by-one

Analogously, there is the same thing for enums, accepting the same arguments as enum:

#enum-one-by-one(numbering: "i)", number-align: start)[first][second][third]

resulting in

enum-one-by-one

#terms-one-by-one

And finally we have a function to produce a term list:

#terms-one-by-one(separator: [~---~])[/ first: 1st][/ second: 2nd][/ third: 3rd]

resulting in

terms-one-by-one

Note that #list-one-by-one and #enum-one-by-one expect only the body of the individual items while you need to provide an actual term item (using the / term: description syntax) to #terms-one-by-one.

Also, you will realise that the bullet markers, the numbers, and the terms in the lists, enums, and term lists are not hidden for technical reasons, respectively. You can truly consider this either a bug or a feature... (This could be "fixed" for enums and term lists, so file an issue on GitHub if this bothers you a lot!)

#alternatives to substitute content

The so far discussed helpers #pause, #one-by-one etc. all build upon #uncover. There is an analogon to #one-by-one that is based on #only, namely #alternatives. You can use it to show some content on one subslide, then substitute it by something else, then by something else, etc.

Consider this example:

#only(1)[Ann] #only(2)[Bob] #only(3)[Christopher]
likes
#only(1)[chocolate] #only(2)[strawberry] #only(3)[vanilla]
ice cream.

This sentence is a visual reference.

Here, we want to display three different sentences with the same structure: Some person likes some sort of ice cream.

poor-alternatives

As you can see, the positioning of likes and ice cream moves around in the produced slide because, for example, Ann takes much less space than Christopher when using #only for that job. This somewhat disturbs the perception of the constant structure of the sentence and that only the names and kinds of ice cream change.

To avoid such movement and only subsitute certain parts of content, you can use the #alternatives function. With it, our example becomes:

#alternatives[Ann][Bob][Christopher]
likes
#alternatives[chocolate][strawberry][vanilla]
ice cream.

This sentence is a visual reference.

resulting in

alternatives

#alternatives will put enough empty space around, for example, Ann such that it uses the same amount of space as Christopher. In a sense, it is like a mix of #only and #uncover with some reserving of space.

Repeat last content

In case you have other dynamic content on a slide that happens after the contents of #alternatives are exhausted, you might want to not have the #alternatives element disappear but instead continue to show its last content argument. To achieve this, you can use the repeat-last parameter:

#alternatives(repeat-last: true)[temporary][transitory][ephemeral][permanent!]

#uncover(5)[Did I miss something?]
]

resulting in

alternatives-repeat-last

Positioning

By default, all elements that enter an #alternatives command are aligned at the bottom left corner. This might not always be the desired or the most pleasant way to position it, so you can provide an optional position argument to #alternatives that takes an alignment or 2d alignment. For example:

We know that
#alternatives(position: center + horizon)[$pi$][$sqrt(2)^2 + 1/3$]
is
#alternatives[irrational][rational].

makes the mathematical terms look better positioned:

alternatives-position

All functions described on this page have such a position argument.

Similar to #one-by-one, #alternatives also has an optional start argument that works just the same.

#alternatives-match

#alternatives has a couple of "cousins" that might be more convenient in some situations. The first one is #alternatives-match that has a name inspired by match-statements in many functional programming languages. The idea is that you give it a dictionary mapping from subslides to content:

#alternatives-match((
  "1, 3-5": [this text has the majority],
  "2, 6": [this is shown less often]
))

resulting in

alternatives-match

Note that it is your responsibility to make sure that the subslide sets are mutually disjoint.

#alternatives-cases

You can use this function if you want to have one piece of content that changes only slightly depending of what "case" of subslides you are in. So instead of

#alternatives-match((
  "1, 3" : [
    Some text
  ],
  "2" : [
    #set text(fill: teal)
    Some text
  ],
))

you can avoid duplication and write

#alternatives-cases(("1, 3", "2"), case => [
  #set text(fill: teal) if case == 1
  Some text
])

using a function that maps the current "case" to content, resulting in

alternatives-cases

Note that the cases are 0-indexed (as are Typst arrays).

#alternatives-fn

Finally, you can have very fine-grained control over the content depending on the current subslide by using #alternatives-fn. It accepts a function (hence the name) that maps the current subslide index to some content.

Similar to #alternatives, it accepts an optional start parameter that has a default of 1. #alternatives-fn only knows for how long to display something, though, if you provide either the number of subslides (count parameter) or the last subslide index (end parameter). So exactly one of them is necessary.

For example:

#alternatives-fn(start: 2, count: 7, subslide => {
  numbering("(i)", subslide)
})

resulting in

alternatives-fn

Cover mode

Covered content (using #uncover, #one-by-one, #line-by-line, or #{list|enum|terms}-one-by-one) is completely invisible, by default. You can decide to make it visible but less prominent using the optional mode argument to each of those functions. The mode argument takes two different values: "invisible" (the default) and "transparent". (This terminology is taken from LaTeX beamer as well.) With mode: "transparent", text is printed in a light gray.

Use it as follows:

#uncover(3, mode: "transparent")[abc]

#one-by-one(start: 2, mode: "transparent")[def ][ghi]

#line-by-line(mode: "transparent")[
  - jkl
  - mno
]

#enum-one-by-one(mode: "transparent", tight: false)[pqr][stu][vwx]

resulting in

cover

Warning! The transparent mode really only wraps the covered content in a

#text(fill: gray.lighten(50%)[...]

so it has only limited control over the actual display. Especially

  • text that defines its own color (e.g. syntax highlighting),
  • visualisations,
  • images

will not be affected by that. This makes the transparent mode only somewhat useful today. (Relevant GitHub issue)

Handout mode

If you distribute your slides after your talk for further reference, you might not want to keep in all the dynamic content. Imagine using one-by-one on a bullet point list and readers having to scroll through endless pages when they just want to see the full list.

You can use #enable-handout-mode(true) at the top of your code to achieve this:

It has the effect that all dynamic visibility of elements that reserve space is switched off. For example,

#enable-handout-mode(true)
// ...
#polylux-slide[
  Some text.
  #uncover("3-")[You cannot always see this.]
  ...Or can you?
]

becomes:

handout

Note that only and alternatives are not affected as there is no obvious way to unify their content to one slide.

Internals

Here, topics regarding the internal implementation of dynamic content in polylux is discussed. Usually, you can completely ignore this section.

Internal number of repetitions

TL;DR: For slides with more than ten subslides, you need to set the max-repetitions argument of the #polylux-slide function to a higher number.

For technical reasons (this might change in the future when we find a better solution), producing PDF pages for subslides is implemented in the following way: Each dynamic element, such as #only or #beginning "knows" how many subslides a logical slide must have for it to "make sense". For example, a #beginning(5)[...] only makes sense if at least 5 subslides are produced.

Internally, when typesetting a slide, we now look at all the dynamic elements in it and find the maximum of those individual "required" subslide counts. So if a slide contains a #only(2)[...], a #until(4)[...], and nothing else, we know that exactly 4 subslides are necessary.

However, we only acquire this knowledge after the first subslide has been produced, i.e. when all of the slide's content has been "looked at" once. This is why we cannot simply implement something like "produce 4 pages by iterating this loop 4 times". Instead, the (admittedly hacky) solution is to iterate "very often" and check in each iteration if we still need to produce another page. This works because we always need to produce at least one page for a slide, so we can unhurriedly inspect all dynamic elements and find the maximum subslide count at the first iteration. After that, we have the information we need.

Now, the question arises how often "very often" should be. This requires a trade-off: Iterating too few times (say, twice) will lead to frequent situations where we ignore dynamic behaviour that was supposed to happen in later subslides (say, in the third). Iterating, say, a thousand times means that we will practically never encounter such situations but we now perform a thousand iterations per slide. Especially when you want to see a live update of your produced PDF as you type, this leads to severe lagging that somewhat defeats the purpose of Typst's speed. (Still faster than LaTeX, though...)

It appears reasonable that occasions are rare where one needs more than ten subslides for one slide. Therefore, ten is the default value for how often we try to create a new subslide. This should not produce noticable lag. (If it does for you, consider creating an issue so we can discuss this.)

For those hopefully rare occasions where you do, in fact, need more than ten subslides, you can manually increase this number using the max-repetitions argument of the #slide function:

#polylux-slide(max-repetitions: 20)[
  This is gonna take a while:
  #uncover(20)[I can wait...]
]

Again, use this feature sparingly, as it decreases typesetting performance.

Themes

As we have already discussed, you can use polylux completely without using themes. For most users, themes will simplify their preparation of decent slides quite a bit, however. So let's take a look at how they work.

It is important to note that polylux does not define a specific way a theme has to work. Similarly, if you define your own slide layout, don't feel obliged to do it in any way related to how the provided themes do it. To improve the user experience and avoid having to learn everything anew when switching to another theme, the existing themes all follow a certain convention, though.

The theme convention

First of all, all themes reside in the themes module inside polylux. That means, if you want to employ, say, the simple theme, you add the following to your regular #import line at the top:

#import "@preview/polylux:0.3.1": *
#import themes.simple: *

Next, a theme usually provides some initialisation function that has a name ending in -theme. It is supposed to be used in a #show: ... rule, i.e. (again for the simple theme):

#show: simple-theme.with(...)

Inside the with(), you can set some theme-specific configuration. Here you find options that concern the whole presentation, such as the aspect ratio of your slides.

Speaking of which, when you use a theme, you do not have to set the paper size yourself anymore, such things are handled by the theme (the convention is that every theme has an aspect-ratio keyword for its initialisation function that can be set to "16-9" or "4-3").

The other major feature of themes is that they usually come with custom slide functions. That means that you will not use the #polylux-slide function! It is called under the hood by the wrapper functions from the theme.

To be more accurate: Nothing stops you from still calling #polylux-slide and you can always build something custom along the "regular" theme-slides if you are not satisfied with what a theme offers you. It's just that you usually will not have to do this.

The range of theme-specific slide functions varies from theme to theme but there is again one convention: A theme usually has a #title-slide function for, well, the title slide and a #slide function that you will use for "normal" slides.

Each of these functions might accept some keyword arguments and/or one or multiple content blocks to define what is supposed to be seen on the slide.

Theme gallery

If you decide to use one of the themes shipped with polylux, you can use this gallery to get to know how each theme works and if you like how it looks. All themes follow the convention introduced on the previous page if not stated otherwise. For many things, most themes are interchangable, meaning that you can simply change what theme you import and its initialisation and everything still works. However, this is not guaranteed and might break at any point.

Simple theme

simple

This theme is rather unobstrusive and might still be considered bare-bones. It uses a minimal amount of colour and lets you define your slides' content very freely.

Use it via

#import "@preview/polylux:0.3.1": *
#import themes.simple: *

#show: simple-theme.with(...)

simple uses regular headings for sections. Unless specified otherwise, first level headings create new sections. The regular #outline is configured such that it lists the names of all sections.

Second level headings are supposed to be used as slide titles and introduce some spacing below them.

Text is configured to have a base font size of 25 pt.

Options for initialisation

simple-theme accepts the following optional keyword arguments:

  • aspect-ratio: the aspect ratio of the slides, either "16-9" or "4-3", default is "16-9"
  • footer: text to display in the footer of every slide, default is []
  • background: background colour, default is white
  • foreground: text colour, default is black

Slide functions

simple provides the following custom slide functions:

#centered-slide[
  ...
]

A slide where the content is positioned in the center of the slide. Not suitable for content that exceeds one page.


#title-slide[
  ...
]

Same as centered-slide but makes heading of level 1 not outlined, so that the presentation title does not show up in the outline. Not suitable for content that exceeds one page.


#slide[
  ...
]

Decorates the provided content with a header containing the current section (if any) and a footer containing some custom text and the slide number.


#focus-slide(foreground: ..., background: ...)[
  ...
]

Draw attention with this variant where the content is displayed centered and text is enlarged. Optionally accepts a foreground colour (default: white) and background color (default: aqua.darken(50%)). Not suitable for content that exceeds one page.

Example code

The image at the top is created by the following code:

#import "@preview/polylux:0.3.1": *
#import themes.simple: *

#set text(font: "Inria Sans")

#show: simple-theme.with(
  footer: [Simple slides],
)

#title-slide[
  = Keep it simple!
  #v(2em)

  Alpha #footnote[Uni Augsburg] #h(1em)
  Bravo #footnote[Uni Bayreuth] #h(1em)
  Charlie #footnote[Uni Chemnitz] #h(1em)

  July 23
]

#slide[
  == First slide

  #lorem(20)
]

#focus-slide[
  _Focus!_

  This is very important.
]

#centered-slide[
  = Let's start a new section!
]

#slide[
  == Dynamic slide
  Did you know that...

  #pause
  ...you can see the current section at the top of the slide?
]

Clean theme

clean

This theme is a bit more opinionated than the simple theme but still supposed to be an off-the-shelf solution that fits many use cases.

Use it via

#import "@preview/polylux:0.3.1": *
#import themes.clean: *

#show: clean-theme.with(...)

clean uses polylux' section handling, the regular #outline() will not work properly, use #polylux-outline() instead.

Text is configured to have a base font size of 25 pt.

Options for initialisation

clean-theme accepts the following optional keyword arguments:

  • aspect-ratio: the aspect ratio of the slides, either "16-9" or "4-3", default is "16-9"
  • footer: text to display in the footer of every slide, default is []
  • short-title: short form of the presentation title, to be displayed on every slide, default: none
  • logo: some content (most likely an image) used as a logo on every slide, default: none
  • color: colour of decorative lines, default: teal

Slide functions

clean provides the following custom slide functions:

#title-slide(...)

Creates a title slide where title and subtitle are put between decorative lines, along with logos and author and date infos. Accepts the following keyword arguments:

  • title: title of the presentation, default: none
  • subtitle: subtitle of the presentation, default: none
  • authors: authors of presentation, can be an array of contents or a single content, will be displayed in a grid, default: ()
  • date: date of the presentation, default: none
  • watermark: some content (most likely an image) used as a watermark behind the titlepage, default: none
  • secondlogo: some content (most likely an image) used as a second logo opposite to the regular logo on the title page, default: none

Does not accept additional content.


#slide(...)[
  ...
]

Decorates the provided content with a header containing the current section (if any), the short title of the presentation, and the logo; and a footer containing some custom text and the slide number.

Pass the slide title as a keyword argument title.

Accepts the following keyword arguments:

  • title: title of the slide, default: none,

#focus-slide(foreground: ..., background: ...)[
  ...
]

Draw attention with this variant where the content is displayed centered and text is enlarged. Optionally accepts a foreground colour (default: white) and background color (default: teal). Not suitable for content that exceeds one page.


#new-section-slide(name)

Start a new section with the given name (string or content, positional argument). Creates a slide with this name in the center, a decorative line below, and nothing else. Use #polylux-outline() to display all sections, similarly to how you would use #outline() otherwise.

Example code

The image at the top is created by the following code:

#import "@preview/polylux:0.3.1": *
#import themes.clean: *

#set text(font: "Inria Sans")

#show: clean-theme.with(
  footer: [Author, institution],
  short-title: [Short title],
  logo: image("dummy-logo.png"),
)

#title-slide(
  title: [Presentation title],
  subtitle: [Presentation subtitle],
  authors: ([Author A], [Author B], [Author C]),
  date: [January 1, 1970],
  watermark: image("dummy-watermark.png"),
)

#slide(title: [First slide title])[
  #lorem(20)
]

#new-section-slide("The new section")

#slide(title: "Another slide")[
  Note that you can see the section title at the top.

  The rest of this slide will fill more than one page!

  #lorem(100)
]

#focus-slide[
  _Focus!_

  This is very important.
]

Metropolis theme

metropolis

This theme is inspired by the Metropolis beamer theme, created by Matthias Vogelgesang.

Use it via

#import "@preview/polylux:0.3.1": *
#import themes.metropolis: *

#show: metropolis-theme.with(...)

metropolis uses polylux' section handling, the regular #outline() will not work properly, use #metropolis-outline instead.

Options for initialisation

metropolis-theme accepts the following optional keyword arguments:

  • aspect-ratio: the aspect ratio of the slides, either "16-9" or "4-3", default is "16-9"
  • footer: text to display in the footer of every slide, default is []

Slide functions

metropolis provides the following custom slide functions:

#title-slide(...)

Creates a title slide where title and subtitle are separated by additional information by a bright line. Accepts the following keyword arguments:

  • title: title of the presentation, default: []
  • subtitle: subtitle of the presentation, default: none
  • author: author of presentation, can be arbitrary contet, default: none
  • date: date of the presentation, default: none
  • extra: some extra information, can be used for affiliation, instiution, logos, etc., default: none

Does not accept additional content.


#slide(...)[
  ...
]

Decorates the provided content with a header containing the slide title and a footer containing some custom text and the slide number.

Pass the slide title as a keyword argument title (default: none).


#focus-slide[
  ...
]

Draw attention with this variant where the content is displayed centered and text is enlarged and bright. Uses the background colour of the title on regular slides as the background colour for the whole slide. Not suitable for content that exceeds one page.


#new-section-slide(name)

Start a new section with the given name (string or content, positional argument). Creates a slide with this name in the center, a progress bar indicating the current progress of the presentation below, and nothing else. Use #metropolis-outline to display all sections, similarly to how you would use #outline() otherwise.

Additional features

metropolis provides a further way to highlight text besids #emph and #strong, namely #alert (as known from LaTex's beamer).

#alert[very important]

prints its content in a bright colour.

There is also #metropolis-outline which customises #polylux-outline and displays a table of contents with all sections.

Example code

The image at the top is created by the following code:

#import "@preview/polylux:0.3.1": *
#import themes.metropolis: *

#show: metropolis-theme.with(
  footer: [Custom footer]
)

#set text(font: "Fira Sans", weight: "light", size: 20pt)
#show math.equation: set text(font: "Fira Math")
#set strong(delta: 100)
#set par(justify: true)

#title-slide(
  author: [Authors],
  title: "Title",
  subtitle: "Subtitle",
  date: "Date",
  extra: "Extra"
)

#slide(title: "Table of contents")[
  #metropolis-outline
]

#slide(title: "Slide title")[
  A slide with some maths:
  $ x_(n+1) = (x_n + a/x_n) / 2 $

  #lorem(200)
]

#new-section-slide("First section")

#slide[
  A slide without a title but with #alert[important] infos
]

#new-section-slide([Second section])

#focus-slide[
  Wake up!
]

University theme

university

This theme offers a simple yet versatile design, allowing for easy customization and flexibility. Additionally, it incorporates a progress bar at the top, which displays the current status of the presentation.

Use it via

#import "@preview/polylux:0.3.1": *
#import themes.university: *

#show: university-theme.with(...)

university uses polylux' section handling, the regular #outline() will not work properly, use #polylux-outline instead. Starting a new section is done by the corresponding keyword argument of #slide.

Text is configured to have a base font size of 25 pt.

Options for initialisation

university-theme accepts the following optional keyword arguments:

  • aspect-ratio: the aspect ratio of the slides, either "16-9" or "4-3", default is "16-9"
  • short-title: short title of the presentation to display on every slide, default: none
  • short-author: short author of the presentation to display on every slide, default: none
  • short-date: short date of presentation to display on every slide, default: none
  • color-a: main colour of decorations, default: rgb("#0C6291")
  • color-b: accent colour, default: rgb("#A63446")
  • color-c: second accent colour, default: rgb("#FBFEF9")
  • progress-bar: boolean value whether or not to display a progress bar on regular sides, default: true

Slide functions

university provides the following custom slide functions:

#title-slide(...)

Creates a title slide where title and subtitle are separated by additional information by a bright line. Accepts the following keyword arguments:

  • title: title of the presentation, default: []
  • subtitle: subtitle of the presentation, default: none
  • authors: authors of presentation, can be an array of contents or a single content, will be displayed in a grid, default: ()
  • date: date of the presentation, default: none
  • institution-name: name of the institution, default: "University"
  • logo: some content (most likely an image) used as a logo on the title slide, default: none

Does not accept additional content.


#slide(...)[
  ...
]

Decorates the provided content with a header containing a progress bar (optionally), the slide title, and the current section (if any); and a footer containing short forms of authors, title, and date, and the slide number. Header and footer can also be overwritten by respective keyword arguments.

Pass the slide title as a keyword argument title.

Accepts the following keyword arguments:

  • title: title of the slide, default: none,
  • header: custom content to overwrite default header
  • footer: custom content to overwrite default footer
  • new-section: name of the new section that starts here if not none, default: none

#focus-slide(background-img: ..., background-color: ...)[
  ...
]

Draw attention with this variant where the content is displayed centered and text is enlarged and bright. You can either specify a background image or a background colour as keyword arguments. If you specify none of them, a background colour of rgb("#0C6291") is used as a default.

Not suitable for content that exceeds one page.


#matrix-slide(columns: ..., rows: ...)[
  ...
][
  ...
]

Create a slide where the provided content blocks are displayed in a grid and coloured in a checkerboard pattern without further decoration. You can configure the grid using the rows and columns keyword arguments (both default to none). It is determined in the following way:

  1. If colmuns is an integer, create that many columns of width 1fr.
  2. If columns is none, create as many columns of width 1fr as there are content blocks.
  3. Otherwise assume that columns is an array of widths already, use that.
  4. If rows is an integer, create that many rows of height 1fr.
  5. If rows is none create that many rows of height 1fr as are needed given the number of content blocks and columns.
  6. Otherwise assume that rows is an array of heights already, use that.
  7. Check that there are enough rows and columns to fit in all the content blocks.

That means that #matrix-slide[...][...] stacks horizontally and #matrix-slide(columns: 1)[...][...] stacks vertically.

Not suitable for content that exceeds one page.

Example code

The image at the top is created by the following code:

#import "@preview/polylux:0.3.1": *
#import themes.university: *

#show: university-theme.with(
  short-author: "Short author",
  short-title: "Short title",
  short-date: "Short date",
)

#title-slide(
  authors: ("Author A", "Author B"),
  title: "Title",
  subtitle: "Subtitle",
  date: "Date",
  institution-name: "University Name",
  logo: image("dummy-logo.png", width: 60mm)
)

#slide(title: [Slide title], new-section: [The section])[
  #lorem(40)
]

#focus-slide(background-img: image("background.svg"))[
  *Another variant with an image in background...*
]

#matrix-slide[
  left
][
  middle
][
  right
]

#matrix-slide(columns: 1)[
  top
][
  bottom
]

#matrix-slide(columns: (1fr, 2fr, 1fr), ..(lorem(8),) * 9)

Bipartite theme

bipartite

This theme is inspired by Modern Annual Report. and a bit more opinionated. It features a dominant partition of space into a bright and a dark side and is rather on the "artsy" than functional side.

Use it via

#import "@preview/polylux:0.3.1": *
#import themes.bipartite: *

#show: bipartite-theme.with(...)

The bipartite theme cannot display content that exceeds one page, in general. Note that, against the convention, bipartite offers no #slide function. Use either #west-slide or #east-slide for regular content. Also, this theme features no sections or slide numbers.

Options for initialisation

bipartite-theme accepts the following optional keyword arguments:

  • aspect-ratio: the aspect ratio of the slides, either "16-9" or "4-3", default is "16-9"

Slide functions

bipartite provides the following custom slide functions:

#title-slide(...)

Displays presentation title on a large bright portion above the subtitle, the author and the date. If a date was given, separates it from the author by a central dot. Accepts the following keyword arguments:

  • title: title of the presentation, default: []
  • subtitle: subtitle of the presentation, default: none
  • author: author of presentation, arbitrary content, default: []
  • date: date of the presentation, default: none

Does not accept additional content.


#west-slide(title: ...)[
  ...
]

Splits the slide into a larger bright section on the right where the content goes and a smaller, darker, left section where the title is displayed. Everything is left aligned.


#east-slide(title: ...)[
  ...
]

Same as #west-slide but with the title and the content switching places, and everything being right aligned.


#split-slide[
  ...
][
  ...
]

Splits the slide into two equal sections on the left and the right that both contain content (#split-slide requires exactly two content blocks to be passed). The left half is dark text on a bright background and right aligned, the right half is bright text on dark background and left aligned. Does not display a slide title.

Example code

The image at the top is created by the following code:

#import "@preview/polylux:0.3.1": *
#import themes.bipartite: *

#show: bipartite-theme

#set text(size: 25pt)

#title-slide(
  author: [Author],
  title: [Title],
  subtitle: [Subtitle],
  date: [Date],
)

#west-slide(title: "A longer slide title")[
  #lorem(40)
]

#east-slide(title: "On the right!")[
  #lorem(40)
]

#split-slide[
  #lorem(40)
][
  #lorem(40)
]

Build your own theme

Again, there is no right or wrong when it comes to how a Polylux theme works. If you consider building a theme that you would like to contribute to the package (which you are cordially invited to do!), we kindly ask you to follow the convention presented before. In any case, it is probably a good idea to take that as an orientation.

To demonstrate how one would go about defining a custom theme, let us create one together! How about we make one for science slams? If you have ever been to one, you might have noticed that the presenters there love sparse dark-mode slides with huge font sizes.

Imports

Depending on whether this is a theme for yourself or supposed to be part of polylux, you do one of two things: For yourself, you simply import polylux as always:

#import "@preview/polylux:0.3.1": *

A theme that is shipped with polylux doesn't have to do that, and it shouldn't! Otherwise circular imports can occur. Instead, you depend on the two files logic.typ and utils/utils.typ. As your theme file science-slam.typ will be inside the themes directory, the imports will be:

#import "../logic.typ"
#import "../utils/utils.typ"

Additionally, you have to make polylux know about your theme which you do by adding

#import "science-slam.typ"

to themes/themes.typ.

The initialisation function

With that out of the way, we start with the function that sets the scene for everything else to come. By convention, we call it science-slam-theme and it can accept some keyword arguments along a single content argument. The keyword arguments are for configuration options, the content is for the rest of the document (read here if you are unfamiliar with this kind of function, this feature is rather unique to Typst).

#let science-slam-theme(aspect-ratio: "16-9", darkness: "dark", body) = {
  let background-color = if darkness == "dark" {
    navy
  } else if darkness == "very dark" {
    navy.darken(50%)
  } else if darkness == "ultra dark" {
    black
  } else {
    panic("illegal darkness, must be one of dark, very dark, ultra dark")
  }

  set page(
    paper: "presentation-" + aspect-ratio,
    fill: background-color,
  )
  set text(fill: white.darken(10%), size: 40pt, font: "Fira Sans")

  body
}

As you can see, we have two configuration options: One for the aspect ratio (as is convention) and one to determine the background colour — the more serious you are, the darker your background colour is, of course. After we have set the page parameters accordingly, we also define the text to be huge, bright and in a sans serif font.

Using it looks like this:

#show: science-slam-theme.with(darkness: "very dark")

Title slide

Next up, let us define a cool title slide. The only thing you have to keep in mind when defining your own slide functions is that you need to put the content you produce into the #polylux-slide function in the end. It might look as if it works without that as well but it actually breaks when you use #uncover or similar polylux features. If you build a theme as part of polylux and you have followed the import instructions from above, you will qualify the function as logic.polylux-slide.

Our title slide here is very simple, it just makes very sure to let the audience know what the topic is and who is speaking:

#let title-slide(title: [], author: []) = {
  polylux-slide({
    set align(center + horizon)
    smallcaps(strong(title))
    parbreak()
    text(size: .7em, author)
  })
}

You can use it like this:

#title-slide(title: [My awesome topic], author: [A really funny guy])

Note that the user does not actually provide any content themselves to this function. That is a common thing for title slides because their structure is so well-defined that a theme can produce all the content by just asking for a few pieces of information (like the title etc.).

Regular slides

The principle is the same as with the title slide. Define a function, create some content, pass it to polylux-slide. By convention, you should name the function for regular slides slide because it will be used most often. Here you will typically accept arbitrary content as a positional parameter that will make up the main content of the slide.

For example:

#let slide(title: [], body) = {
  polylux-slide({
    strong(title)
    set align(horizon)
    body
  })
}

And you can use it like this:

#slide(title: "A hilarious slide")[
  You didn't expect that!
]

In case you wondered, this is how the theme and the slides we just put together look like:

science-slam

Any number of further variants

Be creative! There are no limits but your own to the slide functions your theme can contain once you grasped the simple structure. For "serious" themes (other than this demo) you will probably want to think about adding headers, footers, slide numbers etc. Why not look into the source code of existing themes to get some inspiration?

The next page also lists some small tools that polylux provides to make common tasks simpler when creating a slide.

Utility features

Let us have a look at some common use cases you run into as either a theme author or as an end user producing slides and what solutions are provided by Polylux.

Specifically, the functions discussed here reside in the utils and logic modules. They are exported by Polylux so if you (as someone making slides) imported it using

#import "@preview/polylux:0.3.1": *

you can direcly invoke them using utils and logic. As a theme author, you have access to them due to the imports

#import "../logic.typ"
#import "../utils/utils.typ"

(see previous page).

Side by side

To make good use of the space on a slide, you will often want to place content next to each other. For convenience, Polylux provides the function #side-by-side for this purpose. If you used

#import "@preview/polylux:0.3.1": *

you have it directly available. Otherwise you can get if from the utils module.

It is basically a thin wrapper around the Typst function #grid but tailored towards this specific usecase. In its simplest form, you can use it as

#side-by-side[
  #lorem(7)
][
  #lorem(10)
][
  #lorem(5)
]

resulting in

side-by-side

As you can see, the content arguments you provide will be placed next to each other with equal proportions of width. A spacing (gutter) of 1em will also be put between them.

The widths and gutter can be configured using the columns and gutter optional arguments, respectively. They are propagated to #grid directly so you can look up possible values in its documentation (gutter and columns arguments). If not specified, they fall back to these defaults:

  • gutter: 1em
  • columns: (1fr,) * n if you provided n content arguments, that means an array with the value 1fr repeated n times.

A more complex example would therefore be:

#side-by-side(gutter: 3mm, columns: (1fr, 2fr, 1fr))[
  #rect(width: 100%, stroke: none, fill: aqua)
][
  #rect(width: 100%, stroke: none, fill: teal)
][
  #rect(width: 100%, stroke: none, fill: eastern)
]

resulting in

side-by-side-kwargs

Fit to height

Suppose you have some content and some size constraints for it but the two don't match, i.e. the content does not have the size that you want. The function #fit-to-height can help you with that.

It expects a height and some content and will try to scale the content such that it takes on the given height:

#fit-to-height(1fr)[BIG]

resulting in

fill-remaining

Using 1fr as the height is probably also the prime use case for this function, as it fills the remaining space of the slide with the given content. Anything else (like 5pt, 3cm, 4em etc.) is possible as well, of course.

Adjusting the width

To finetune the result of #fit-to-height, you have two optional parameters:

  • width: If specified, this determines the width of the content after scaling. So, if you want the scaled content to fill half the slide's width for example, you can use width: 50%. By default, the scaled content will be constrained by the slide's width and will have less than the requested height if necessary.
  • prescale-width: This parameter allows you to make Typst's layouting assume the given content is to be layouted in a container of a certain width before scaling. You can pretend the slide is twice as wide using prescale-width: 200%, for example.

We can illustrate that using the following example:

#fit-to-height(5cm, prescale-width: 300%, width: 50%)[
  #set par(justify: true)
  #lorem(200)
]

resulting in

fit-to-height-width

How much longer? 🥱

There are a handfull of features that let you display the progress of the presentation.

The most simple one is directly displaying the current slide number. Remember that each slide might produce an arbitrary amount of subslides, i.e. PDF pages, so we cannot rely on the builtin page counter. Instead, there is the logical-slide counter in the logic module. Therefore, you can use

#logic.logical-slide.display()

to see what the current slide number is.

If you want to put that into relation to how many slides there are in total, you can also display

#utils.last-slide-number

which is a short-hand way of getting the final value of logic.logical-slide.

Note that both these things are content, though, so you can only display them and not calculate with the numbers. A common calculation you might want do to is finding their ratio, i.e. current slide number divided by total number of slides. To that end, you can use the function utils.polylux-progress. You can pass a function to it that turns the current ratio into some content. For example:

#utils.polylux-progress( ratio => [
  You already made it through #calc.round(ratio * 100) #sym.percent of the presentation!
])

Some themes utilise this to display a little progress bar, for example.

Sections

Another way of expressing where we are in a presentation is working with sections. Usually, this is a topic that a theme will/should handle so this page is addressed more towards theme authors.

In your theme, you can incorporate the following features from the utils module:

First, whenever a user wants to start a new section, you can call

#utils.register-section(the-section-name)

with whatever name they specify. It is up to you to decide what kind of interface you provide for the user and how/if you visualise a new section, of course.

Based on that, you can then display what section the presenter is currently in by using:

#utils.current-section

If no section has been registered so far, this is empty content ([]).

And finally, you might want to display some kind of overview over all the sections. This is achieved by:

#utils.polylux-outline()

Unfortunately, it is hard to get the Typst-builtin #outline to work properly with slides, partly again due to how page numbers are kind of meaningless. polylux-outline is a good alternative to that and will return an enum with all the registered sections (ever, not only so far, so you can safely use it at the beginning of a presentation).

polylux-outline has two optional keyword arguments:

External tools

Most users will only come across Typst itself and some off-the-shelf PDF viewer. However, there are some additional tools that might come in handy and Polylux supports some of their special needs.

So far, this support is limited to the pdfpc presentation tool.

pdfpc

pdfpc is a "presenter console with multi-monitor support for PDF-files". That means, you can use it to display slides in the form of PDF-pages and also have some of the nice features known from, for example, PowerPoint. Check out their website to learn more.

When pdfpc is provided a special .pdfpc file containing some JSON data, it can use that to enhance the user experience by correctly handling overlay slides, displaying speaker notes, setting up a specific timer, and more. While you can write this file by hand or use the pdfpc-internal features to edit it, some might find it more convenient to have all data about their presentation in one place, i.e. the Typst source file. Polylux allows you to do that.

Adding metadata to the Typst source

Polylux exports the pdfpc module that comes with a bunch of useful functions that do not actually add any content to the produced PDF but instead insert metadata that can later be extracted from the document.

Speaker notes

This is possibly the most useful feature of pdfpc. Using the function #pdfpc.speaker-note inside a slide, you can add a note to that slide that will only be visible to the speaker in pdfpc. It accepts either a string:

#pdfpc.speaker-note("This is a note that only the speaker will see.")

or a raw block:

#pdfpc.speaker-note(
  ```md
  # My notes
  Did you know that pdfpc supports Markdown notes? _So cool!_
  ```
)

Note that you can only specify one note per slide (only the first one will survive if you use more than one.)

End slide

Sometimes the last slide in your presentation is not really the one you want to end with. Say, you have some bibliography or appendix for the sake of completeness after your "I thank my mom and everyone who believed in me"-slide.

With a simple pdfpc.end-slide inside any slide you can tell pdfpc that this is the last slide you usually want to show and hitting the End key will jump there.

Save a slide

Similarly, there is a feature in pdfpc to bookmark a specific slide (and you can jump to it using Shift + M). In your Typst source, you can choose that slide by putting pdfpc.save-slide inside it.

Hide slides

If you want to keep a certain slide in your presentation (just in case) but don't normally intend to show it, you can hide it inside pdfpc. It will be skipped during the presentation but it is still available in the overview. You can use pdfpc.hidden-slide in your Typst source to mark a slide as hidden.

Configure pdfpc

The previous commands are all supposed to be used inside a slide. To perform some additional global configuration, you can use pdfpc.config() before any of the slides (it will not be recognised otherwise).

It accepts the following optional keyword arguments:

  • duration-minutes: how many minutes (a number) the presentation is supposed to take, affects the timer in pdfpc

  • start-time: wall-clock time when the presentation is supposed to start, either as a datetime(hour: ..., minute: ..., second: ...) or as a string in the HH:MM format

  • end-time: same as start-time but when the presentation is supposed to end

  • last-minutes: how many minutes (a number) before the time runs out the timer is supposed to change its colour as a warning

  • note-font-size: the font size (a number) the speaker notes are displayed in

  • disable-markdown: whether or not to disable rendering the notes as markdown (a bool), default false

  • default-transition: the transition to use between subsequent slides, must be given as a dictionary with (potentially) the following keys:

    • type: one of "replace" (default), "push", "blinds", "box", "cover", "dissolve", "fade", "glitter", "split", "uncover", "wipe"
    • duration-seconds: the duration of the transition in seconds (a number)
    • angle: in which angle the transition moves, one of ltr, rtl, ttb, and btt (see the #stack function)
    • alignment: whether the transition is performed horizontally or vertically, one of "horizontal" and "vertical"
    • direction: whether the transition is performed inward or outward, one of "inward" and "outward"

    Not all combinations of values are necessary or make sense for all transitions, of course.

Extracting the data: polylux2pdfpc

As mentioned above, the functions from the pdfpc module don't alter the produced PDF itself. Instead, we need some other way to extract their data. You could, in principle, do that by hand using the typst query CLI and then assemble the correct .pdfpc file yourself. However, this tedious task is better solved by the polylux2pdfpc tool.

Installation

If you have Rust installed, you can simply run

cargo install --git https://github.com/andreasKroepelin/polylux/ --branch release

If you use Arch Linux btw, you can also install polylux2pdfpc from the AUR package polylux2pdfpc-git (thank you to Julius Freudenberger!)

Usage

You invoke polylux2pdfpc with the same arguments you would also give to typst compile when you wanted to build your slides. For example, say you have a file called talk.typ in the folder thesis that has some global utility files or so, you would compile it using

typst compile --root .. thesis/talk.typ

and extract the pdfpc data using

polylux2pdfpc --root .. thesis/talk.typ

Internally, polylux2pdfpc runs typst query, collects all the pdfpc-related metadata and then writes a .pdfpc file that equals the input file up to the suffix. In our example with thesis/talk.typ, we obtain thesis/talk.pdfpc. Since typst compile produced thesis/talk.pdf, you can now simply open the PDF in pdpfc:

pdfpc thesis/talk.pdf

and it will automatically recognise the .pdfpc file.

Changelog

v0.3.0

  • The previously existing module helpers was transformed to utils and now contains many more useful features.
  • The modules logic and utils are now directly accesible when importing Polylux (it was a bug that it did not work previously).
  • We finally have an ergonomic #pause function that does not expect the user to keep track of some counter themselves.
  • The #alternatives function has gained lots of friends that make specific situations a bit more convenient, namely #alternatives-match, alternatives-cases, and alternatives-fn. Also, there is a parameter repeat-last for #alternatives now.
  • Bullet lists, enumerations, and term lists now have custom functions to display them dynamically: #list-one-by-one, #enum-one-by-one, and #terms-one-by-one.
  • There is a new function #fit-to-height that allows you to resize content to a given height (especially make it fill the remaining space on a slide!) Thank you to @ntjess for the initial implementation!
  • Previously, certain themes allowed you to easily put multiple content elements next to each other. This is now a commonly available function: #side-by-side. You can use it regardless of any theme and the functionality was removed from the previously implementing themes.
  • Polylux now has special support for the pdfpc presentation viewer. You can add speaker notes, hide slides, configure the timer, and more all from within your Typst source file. Thank you to @JuliusFreudenberger for the inspiration and for creating the polylux2pdfpc AUR package.