mirror of https://github.com/JoshKarpel/spiel
You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
531 lines
16 KiB
Python
531 lines
16 KiB
Python
import inspect
|
|
import os
|
|
import shutil
|
|
import socket
|
|
from datetime import datetime
|
|
from math import cos, floor, pi
|
|
from pathlib import Path
|
|
from textwrap import dedent
|
|
|
|
import numpy as np
|
|
from rich.align import Align
|
|
from rich.box import SQUARE
|
|
from rich.color import Color, blend_rgb
|
|
from rich.console import RenderGroup
|
|
from rich.layout import Layout
|
|
from rich.markdown import Markdown
|
|
from rich.padding import Padding
|
|
from rich.panel import Panel
|
|
from rich.style import Style
|
|
from rich.syntax import Syntax
|
|
from rich.text import Text
|
|
|
|
from spiel import Deck, Image, Options, Slide, __version__, example_panels
|
|
from spiel.plot import Plot
|
|
|
|
deck = Deck(name=f"Spiel Demo Deck (v{__version__})")
|
|
options = Options()
|
|
|
|
SPIEL = "[Spiel](https://github.com/JoshKarpel/spiel)"
|
|
RICH = "[Rich](https://rich.readthedocs.io/)"
|
|
NBTERM = "[nbterm](https://github.com/davidbrochart/nbterm)"
|
|
UNIPLOT = "[Uniplot](https://github.com/olavolav/uniplot)"
|
|
IPYTHON = "[IPython](https://ipython.readthedocs.io)"
|
|
WSL = "[Windows Subsystem for Linux](https://docs.microsoft.com/en-us/windows/wsl/)"
|
|
|
|
THIS_DIR = Path(__file__).resolve().parent
|
|
|
|
|
|
@deck.slide(title="What is Spiel?")
|
|
def what():
|
|
upper_left_markup = dedent(
|
|
f"""\
|
|
## What is Spiel?
|
|
|
|
{SPIEL} is a framework for building and presenting richly-styled presentations in your terminal using Python.
|
|
|
|
Spiel uses {RICH} to render slide content.
|
|
Anything you can display with Rich, you can display with Spiel (plus some other things)!
|
|
|
|
Use your right `→` and left `←` arrows keys (or `f` and `b`) to go forwards and backwards through the deck.
|
|
|
|
Press `ctrl-c` or `ctrl-k` to exit.
|
|
|
|
Press `h` at any time to see the help screen, which describes all of the actions you can take.
|
|
|
|
To get a copy of the source code for this deck, use the `spiel demo copy` command.
|
|
"""
|
|
)
|
|
|
|
upper_right_markup = dedent(
|
|
"""\
|
|
## Why use Spiel?
|
|
|
|
It's fun!
|
|
|
|
It's weird!
|
|
|
|
Why not?
|
|
|
|
Maybe you shouldn't.
|
|
|
|
Honestly, it's unclear whether it's a good idea.
|
|
|
|
There's always [Powerpoint](https://youtu.be/uNjxe8ShM-8)!
|
|
"""
|
|
)
|
|
|
|
lower_left_markup = dedent(
|
|
f"""\
|
|
## Contributing
|
|
|
|
Please report bugs via [GitHub Issues](https://github.com/JoshKarpel/spiel/issues).
|
|
|
|
If you have ideas about how Spiel can be improved,
|
|
or you have a cool deck to show off,
|
|
please post to [GitHub Discussions](https://github.com/JoshKarpel/spiel/discussions).
|
|
"""
|
|
)
|
|
|
|
lower_right_markup = dedent(
|
|
f"""\
|
|
## Inspirations
|
|
|
|
Brandon Rhodes' [PyCon 2017](https://youtu.be/66P5FMkWoVU) and [North Bay Python 2017](https://youtu.be/rrMnmLyYjU8) talks.
|
|
|
|
David Beazley's [Lambda Calculus from the Ground Up](https://youtu.be/pkCLMl0e_0k) tutorial at PyCon 2019.
|
|
|
|
LaTeX's [Beamer](https://ctan.org/pkg/beamer) document class.
|
|
"""
|
|
)
|
|
|
|
def pad_markdown(markup):
|
|
return Padding(Markdown(markup, justify="center"), pad=(0, 5))
|
|
|
|
root = Layout()
|
|
upper = Layout()
|
|
lower = Layout()
|
|
|
|
upper.split_row(
|
|
Layout(pad_markdown(upper_left_markup)),
|
|
Layout(pad_markdown(upper_right_markup)),
|
|
)
|
|
lower.split_row(
|
|
Layout(pad_markdown(lower_left_markup)),
|
|
Layout(pad_markdown(lower_right_markup)),
|
|
)
|
|
root.split_column(upper, lower)
|
|
|
|
return root
|
|
|
|
|
|
@deck.slide(title="Decks and Slides")
|
|
def code():
|
|
markup = dedent(
|
|
f"""\
|
|
## Decks are made of Slides
|
|
|
|
Here's the code for `Deck` and `Slide`!
|
|
|
|
The source code is pulled directly from the definitions via [`inspect.getsource`](https://docs.python.org/3/library/inspect.html#inspect.getsource).
|
|
|
|
({RICH} supports syntax highlighting, so {SPIEL} does too!)
|
|
"""
|
|
)
|
|
root = Layout()
|
|
upper = Layout(Markdown(markup, justify="center"), size=len(markup.split("\n")) + 1)
|
|
lower = Layout()
|
|
root.split_column(upper, lower)
|
|
|
|
def make_code_panel(obj):
|
|
lines, line_number = inspect.getsourcelines(obj)
|
|
return Panel(
|
|
Syntax(
|
|
"".join(lines),
|
|
lexer_name="python",
|
|
line_numbers=True,
|
|
start_line=line_number,
|
|
),
|
|
box=SQUARE,
|
|
border_style=Style(dim=True),
|
|
height=len(lines) + 2,
|
|
)
|
|
|
|
lower.split_row(
|
|
Layout(make_code_panel(Deck)),
|
|
Layout(make_code_panel(Slide)),
|
|
)
|
|
|
|
return root
|
|
|
|
|
|
@deck.slide(title="Dynamic Content")
|
|
def dynamic():
|
|
home = Path.home()
|
|
width = shutil.get_terminal_size().columns
|
|
width_limit = 80
|
|
return RenderGroup(
|
|
Align(
|
|
Text(
|
|
f"Slides can have dynamic content!",
|
|
style=Style(color="bright_magenta", bold=True, italic=True),
|
|
justify="center",
|
|
),
|
|
align="center",
|
|
),
|
|
Align(
|
|
Panel(
|
|
Text(
|
|
f"Your terminal is {width} cells wide."
|
|
if width > width_limit
|
|
else f"Your terminal is only {width} cells wide! Get a bigger monitor!",
|
|
style=Style(color="green1" if width > width_limit else "red"),
|
|
justify="center",
|
|
)
|
|
),
|
|
align="center",
|
|
),
|
|
Align(
|
|
Panel(
|
|
Text(
|
|
f"The time on this computer, {socket.gethostname()}, is {datetime.now()}",
|
|
style=Style(color="bright_cyan", bold=True, italic=True),
|
|
justify="center",
|
|
)
|
|
),
|
|
align="center",
|
|
),
|
|
Align(
|
|
Panel(
|
|
Text(
|
|
f"There are {len([f for f in home.iterdir() if f.is_file()])} files in {home} right now.",
|
|
style=Style(color="yellow"),
|
|
justify="center",
|
|
)
|
|
),
|
|
align="center",
|
|
),
|
|
)
|
|
|
|
|
|
@deck.slide(title="Triggers")
|
|
def triggers(triggers):
|
|
info = Markdown(
|
|
dedent(
|
|
f"""\
|
|
## Triggers
|
|
|
|
Triggers are a mechanism for making dynamic content that depends on *relative* time.
|
|
|
|
Triggers can be used to implement effects like fades, motion, and other "animations".
|
|
|
|
Each slide is triggered once when it starts being displayed.
|
|
You can trigger it again (as many times as you'd like) by pressing `t`.
|
|
You can reset the trigger state by pressing `r`.
|
|
|
|
This slide has been triggered {len(triggers)} times.
|
|
It was last triggered {triggers.time_since_last_trigger:.2f} seconds ago.
|
|
"""
|
|
),
|
|
justify="center",
|
|
)
|
|
|
|
bounce_period = 10
|
|
width = 50
|
|
half_width = width // 2
|
|
|
|
bounce_time = triggers.time_since_first_trigger % bounce_period
|
|
bounce_character = "⁍" if bounce_time < (1 / 2) * bounce_period else "⁌"
|
|
bounce_position = floor(half_width * cos(2 * pi * bounce_time / bounce_period))
|
|
before = half_width + bounce_position
|
|
ball = Align.center(
|
|
Panel(
|
|
Padding(
|
|
bounce_character,
|
|
pad=(0, before, 0, (half_width - bounce_position - 1)),
|
|
),
|
|
title="Bouncing Bullet",
|
|
padding=0,
|
|
)
|
|
)
|
|
|
|
white = Color.parse("bright_white")
|
|
black = Color.parse("black")
|
|
red = Color.parse("bright_red")
|
|
green = Color.parse("bright_green")
|
|
|
|
fade_time = 3
|
|
|
|
lines = [
|
|
Text(
|
|
"Triggered!",
|
|
style=Style(
|
|
color=(
|
|
Color.from_triplet(
|
|
blend_rgb(
|
|
black.get_truecolor(),
|
|
white.get_truecolor(),
|
|
cross_fade=min((triggers.now - time) / fade_time, 1),
|
|
)
|
|
)
|
|
)
|
|
),
|
|
)
|
|
for time in triggers.times
|
|
]
|
|
|
|
fun = Align.center(
|
|
Panel(
|
|
Text("\n", justify="center").join(lines),
|
|
border_style=Style(
|
|
color=Color.from_triplet(
|
|
blend_rgb(
|
|
green.get_truecolor(),
|
|
red.get_truecolor(),
|
|
cross_fade=min(triggers.time_since_last_trigger / fade_time, 1),
|
|
)
|
|
),
|
|
),
|
|
title="Trigger Tracker",
|
|
)
|
|
)
|
|
return RenderGroup(info, fun, ball if len(triggers) > 2 else Text(""))
|
|
|
|
|
|
@deck.slide(title="Views")
|
|
def grid():
|
|
markup = dedent(
|
|
"""\
|
|
## Multiple Views
|
|
|
|
Try pressing `d` to go into "deck" view.
|
|
Press `s` to go back to "slide" view.
|
|
|
|
Press `j`, then enter a slide number (like `3`) to jump to a slide.
|
|
"""
|
|
)
|
|
return Markdown(markup, justify="center")
|
|
|
|
|
|
@deck.slide(title="Watch Mode")
|
|
def watch():
|
|
markup = dedent(
|
|
f"""\
|
|
## Developing a Deck
|
|
|
|
{SPIEL} can reload your deck as you edit it if you add the `--watch` option to `present`:
|
|
|
|
`$ spiel present path/to/deck.py --watch`
|
|
|
|
If you're on a system without inotify support (e.g., {WSL}), you should use the `--poll` option instead.
|
|
"""
|
|
)
|
|
return Markdown(markup, justify="center")
|
|
|
|
|
|
@deck.slide(title="Displaying Images")
|
|
def image():
|
|
markup = dedent(
|
|
f"""\
|
|
## Images
|
|
|
|
{SPIEL} can display images... sort of!
|
|
|
|
Spiel includes an `Image` widget that can render images by interpolating pixel values.
|
|
|
|
If you see big chunks of constant color instead of smooth gradients, your terminal is probably not configured for "truecolor" mode.
|
|
If your terminal supports truecolor (it probably does), try setting the environment variable `COLORTERM` to `truecolor`.
|
|
For example, for `bash`, you could add
|
|
|
|
`export COLORTERM=truecolor`
|
|
|
|
to your `.bashrc` file, then restart your shell.
|
|
"""
|
|
)
|
|
root = Layout()
|
|
root.split_row(
|
|
Layout(Padding(Markdown(markup, justify="center"), pad=(0, 2))),
|
|
Layout(Image.from_file(THIS_DIR / "img.jpg")),
|
|
)
|
|
|
|
return root
|
|
|
|
|
|
@deck.slide(title="Plots")
|
|
def plots(triggers):
|
|
markup = dedent(
|
|
f"""\
|
|
## Plots
|
|
|
|
{SPIEL} can display plots... sort of!
|
|
|
|
Spiel includes a `Plot` widget that uses {UNIPLOT} to render plots.
|
|
|
|
You can even make animated plots by using triggers! Try triggering this slide.
|
|
"""
|
|
)
|
|
|
|
x = np.linspace(-3, 3, 1000)
|
|
coefficients = np.array([0, 2, -1, 3, -1, 1])
|
|
dither = 0.1 * np.sin(triggers.time_since_last_trigger) if triggers.triggered else 0
|
|
hermite = 0.9 * np.polynomial.hermite.hermval(x, c=coefficients + dither)
|
|
upper_plot = Plot(xs=x, ys=hermite, title="A Hermite Polynomial", y_min=-100, y_max=100)
|
|
|
|
theta = np.linspace(-3 * np.pi, 3 * np.pi, 1000)
|
|
phase = (1 if triggers.triggered else 0) * triggers.time_since_last_trigger
|
|
cos = np.cos(theta + phase)
|
|
sin = 1.3 * np.sin(theta + phase)
|
|
lower_plot = Plot(xs=[theta, theta], ys=[cos, sin], title="[cos(x), 1.3 * sin(x)]")
|
|
|
|
lower = Layout()
|
|
lower.split_column(
|
|
Layout(upper_plot),
|
|
Layout(lower_plot),
|
|
)
|
|
|
|
root = Layout()
|
|
root.split_row(
|
|
Layout(Padding(Markdown(markup, justify="center"), pad=(0, 2))),
|
|
lower,
|
|
)
|
|
|
|
return root
|
|
|
|
|
|
@deck.example(title="Examples")
|
|
def examples():
|
|
# This is an example that shows how to use random.choice from the standard library.
|
|
|
|
# The source code is embedded directly into the demo deck file,
|
|
# but you could load it from another file if you wanted to.
|
|
|
|
import random
|
|
|
|
directions = ["North", "South", "East", "West"]
|
|
|
|
print("Which way should we go?")
|
|
print(random.choice(directions))
|
|
|
|
|
|
@examples.layout
|
|
def _(example, triggers):
|
|
root = Layout()
|
|
|
|
extra = (
|
|
f"""
|
|
## Example Execution is Cached
|
|
|
|
Now that you've triggered the slide, {SPIEL} will execute the example once and display the output.
|
|
The result is cached, so the example is not executed on every frame, like code in normal slide content
|
|
functions is.
|
|
|
|
## Editing Examples
|
|
|
|
Examples can be modified during the talk.
|
|
Press `e` to open your `$EDITOR` (`{os.getenv("EDITOR", "not set")}`) on the example code.
|
|
Save your changes and exit to come back to the presentation with your updated code.
|
|
You can then trigger the example again to run it with the new code.
|
|
|
|
## Layout Customization
|
|
|
|
You can customize the example slide's content by providing a custom `layout` function.
|
|
If you don't, you'll get the default layout, which looks like just the right half of this slide.
|
|
"""
|
|
if triggers.triggered
|
|
else ""
|
|
)
|
|
|
|
markup = dedent(
|
|
f"""\
|
|
## Examples
|
|
|
|
{SPIEL} can display and execute chunks of example code.
|
|
|
|
Example slides are driven by the trigger system.
|
|
Press `t` to execute the example code and display the output.
|
|
|
|
{extra}
|
|
"""
|
|
)
|
|
markdown = Markdown(markup, justify="center")
|
|
|
|
root.split_row(
|
|
Layout(Padding(markdown, pad=(0, 2))),
|
|
example_panels(example),
|
|
)
|
|
|
|
return root
|
|
|
|
|
|
@deck.slide(title="Live Coding with the REPL")
|
|
def repl():
|
|
markup = dedent(
|
|
f"""\
|
|
## Live Coding: REPL
|
|
|
|
Sometimes an static example,
|
|
or even an example that you're editing and running multiple times,
|
|
just isn't interactive enough.
|
|
|
|
To provide a more interactive experience,
|
|
{SPIEL} lets you open a REPL on any slide by pressing `i`.
|
|
|
|
There are two REPLs available by default: the [builtin Python REPL](https://docs.python.org/3/tutorial/interpreter.html#interactive-mode) and {IPYTHON}.
|
|
You can change which REPL to use via `Options`, which will be discussed later.
|
|
|
|
When you exit the REPL (by pressing `ctrl-d` or executing `exit`),
|
|
you'll be back at the same point in your presentation.
|
|
|
|
The state of the REPL is not persistent between invocations
|
|
(it will be completely fresh every time you enter it).
|
|
"""
|
|
)
|
|
return Markdown(markup, justify="center")
|
|
|
|
|
|
@deck.slide(title="Live Coding with Notebooks", notebook=THIS_DIR / "notebook.ipynb")
|
|
def notebooks():
|
|
markup = dedent(
|
|
f"""\
|
|
## Live Coding: Notebooks
|
|
|
|
For a more persistent live-coding experience than a REPL,
|
|
you can open a Jupyter Notebook via {NBTERM}
|
|
by pressing `n`.
|
|
|
|
Each slide has a notebook attached to it.
|
|
The notebook kernel will be restarted between invocations,
|
|
but changes made to the notebook contents
|
|
will persist throughout your presentation.
|
|
|
|
By default, the notebook will initially be blank,
|
|
but you can also provide a path to a notebook on disk to initialize from.
|
|
|
|
This slide has a small example notebook attached to it - try it out!
|
|
|
|
(In theory, {SPIEL} will be able to support different kinds of terminal notebook viewers in the future,
|
|
but right now, only {NBTERM} is packaged by default, and the internal API is not stable or very generic.
|
|
Stay tuned!)
|
|
"""
|
|
)
|
|
return Markdown(markup, justify="center")
|
|
|
|
|
|
@deck.slide(title="Options")
|
|
def options_():
|
|
markup = dedent(
|
|
f"""\
|
|
## Options
|
|
|
|
{SPIEL} has a variety of options that can be adjusted at runtime.
|
|
For example,
|
|
profiling information can be displayed in the footer to help you debug a slide that is rendering too slowly.
|
|
|
|
To see your current options, press `p`.
|
|
From that mode you can edit your options by pressing `e`.
|
|
|
|
Note that your `Options` are *not* reloaded when running with `--watch`.
|
|
"""
|
|
)
|
|
return Markdown(markup, justify="center")
|