Slide Transitions (#207)

pull/216/head
Josh Karpel 1 year ago committed by GitHub
parent 3d4ebec7f3
commit d0f55054a4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -6,9 +6,6 @@ source =
spiel/
tests/
omit =
spiel/demo/*
[report]
skip_empty = True

@ -17,6 +17,7 @@ jobs:
run:
shell: bash
runs-on: ${{ matrix.platform }}
timeout-minutes: 15
env:
PLATFORM: ${{ matrix.platform }}
PYTHON_VERSION: ${{ matrix.python-version }}
@ -38,6 +39,8 @@ jobs:
run: poetry run pre-commit run --all-files --show-diff-on-failure --color=always
- name: Make sure we can build the package
run: poetry build -vvv
- name: Test types
run: poetry run mypy
- name: Test code
run: poetry run pytest -v --cov --cov-report=xml --durations=20
- name: Test docs

@ -10,6 +10,14 @@
::: spiel.Triggers
## Transitions
::: spiel.Direction
::: spiel.Transition
::: spiel.Swipe
## Presenting Decks
::: spiel.present

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 136 KiB

After

Width:  |  Height:  |  Size: 132 KiB

@ -94,17 +94,17 @@
</g>
<g transform="translate(9, 41)" clip-path="url(#spieldocs-clip-terminal)">
<rect fill="#1e1e1e" x="0" y="1.5" width="854" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="25.9" width="854" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="50.3" width="854" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="74.7" width="854" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="99.1" width="170.8" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="170.8" y="99.1" width="512.4" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="683.2" y="99.1" width="170.8" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="123.5" width="170.8" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="170.8" y="123.5" width="195.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="366" y="123.5" width="122" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="488" y="123.5" width="195.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="683.2" y="123.5" width="170.8" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="147.9" width="170.8" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="170.8" y="147.9" width="195.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="366" y="147.9" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="378.2" y="147.9" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="390.4" y="147.9" width="73.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="463.6" y="147.9" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="475.8" y="147.9" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="488" y="147.9" width="195.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="683.2" y="147.9" width="170.8" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="172.3" width="170.8" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="170.8" y="172.3" width="195.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="366" y="172.3" width="122" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="488" y="172.3" width="195.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="683.2" y="172.3" width="170.8" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="196.7" width="854" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="221.1" width="854" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="245.5" width="854" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="269.9" width="854" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="294.3" width="854" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="318.7" width="854" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="343.1" width="463.6" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="463.6" y="343.1" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="475.8" y="343.1" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="488" y="343.1" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="500.2" y="343.1" width="353.8" height="24.65" shape-rendering="crispEdges"/>
<rect fill="#1e1e1e" x="0" y="1.5" width="854" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="25.9" width="854" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="50.3" width="854" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="74.7" width="854" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="99.1" width="170.8" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="170.8" y="99.1" width="24.4" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="195.2" y="99.1" width="451.4" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="646.6" y="99.1" width="36.6" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="683.2" y="99.1" width="170.8" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="123.5" width="170.8" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="170.8" y="123.5" width="512.4" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="683.2" y="123.5" width="170.8" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="147.9" width="170.8" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="170.8" y="147.9" width="195.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="366" y="147.9" width="122" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="488" y="147.9" width="195.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="683.2" y="147.9" width="170.8" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="172.3" width="170.8" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="170.8" y="172.3" width="195.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="366" y="172.3" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="378.2" y="172.3" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="390.4" y="172.3" width="73.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="463.6" y="172.3" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="475.8" y="172.3" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="488" y="172.3" width="195.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="683.2" y="172.3" width="170.8" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="196.7" width="170.8" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="170.8" y="196.7" width="195.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="366" y="196.7" width="122" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="488" y="196.7" width="195.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="683.2" y="196.7" width="170.8" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="221.1" width="854" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="245.5" width="854" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="269.9" width="854" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="294.3" width="854" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="318.7" width="854" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="343.1" width="463.6" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="463.6" y="343.1" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="475.8" y="343.1" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="488" y="343.1" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="500.2" y="343.1" width="353.8" height="24.65" shape-rendering="crispEdges"/>
<g class="spieldocs-matrix">
<text class="spieldocs-r2" x="854" y="20" textLength="12.2" clip-path="url(#spieldocs-line-0)">
</text><text class="spieldocs-r2" x="854" y="44.4" textLength="12.2" clip-path="url(#spieldocs-line-1)">
</text><text class="spieldocs-r2" x="854" y="68.8" textLength="12.2" clip-path="url(#spieldocs-line-2)">
</text><text class="spieldocs-r2" x="854" y="93.2" textLength="12.2" clip-path="url(#spieldocs-line-3)">
</text><text class="spieldocs-r1" x="170.8" y="117.6" textLength="512.4" clip-path="url(#spieldocs-line-4)">spaces_before_bang=0&#160;|&#160;spaces_after_bang=5</text><text class="spieldocs-r2" x="854" y="117.6" textLength="12.2" clip-path="url(#spieldocs-line-4)">
</text><text class="spieldocs-r1" x="366" y="142" textLength="122" clip-path="url(#spieldocs-line-5)">╭────────╮</text><text class="spieldocs-r2" x="854" y="142" textLength="12.2" clip-path="url(#spieldocs-line-5)">
</text><text class="spieldocs-r1" x="366" y="166.4" textLength="12.2" clip-path="url(#spieldocs-line-6)"></text><text class="spieldocs-r1" x="390.4" y="166.4" textLength="73.2" clip-path="url(#spieldocs-line-6)">!&#160;&#160;&#160;&#160;&#160;</text><text class="spieldocs-r1" x="475.8" y="166.4" textLength="12.2" clip-path="url(#spieldocs-line-6)"></text><text class="spieldocs-r2" x="854" y="166.4" textLength="12.2" clip-path="url(#spieldocs-line-6)">
</text><text class="spieldocs-r1" x="366" y="190.8" textLength="122" clip-path="url(#spieldocs-line-7)">╰────────╯</text><text class="spieldocs-r2" x="854" y="190.8" textLength="12.2" clip-path="url(#spieldocs-line-7)">
</text><text class="spieldocs-r2" x="854" y="215.2" textLength="12.2" clip-path="url(#spieldocs-line-8)">
</text><text class="spieldocs-r1" x="195.2" y="117.6" textLength="451.4" clip-path="url(#spieldocs-line-4)">triggers=Triggers(now=0,&#160;_times=(0,))</text><text class="spieldocs-r2" x="854" y="117.6" textLength="12.2" clip-path="url(#spieldocs-line-4)">
</text><text class="spieldocs-r1" x="170.8" y="142" textLength="512.4" clip-path="url(#spieldocs-line-5)">spaces_before_bang=0&#160;|&#160;spaces_after_bang=5</text><text class="spieldocs-r2" x="854" y="142" textLength="12.2" clip-path="url(#spieldocs-line-5)">
</text><text class="spieldocs-r1" x="366" y="166.4" textLength="122" clip-path="url(#spieldocs-line-6)">╭────────╮</text><text class="spieldocs-r2" x="854" y="166.4" textLength="12.2" clip-path="url(#spieldocs-line-6)">
</text><text class="spieldocs-r1" x="366" y="190.8" textLength="12.2" clip-path="url(#spieldocs-line-7)"></text><text class="spieldocs-r1" x="390.4" y="190.8" textLength="73.2" clip-path="url(#spieldocs-line-7)">!&#160;&#160;&#160;&#160;&#160;</text><text class="spieldocs-r1" x="475.8" y="190.8" textLength="12.2" clip-path="url(#spieldocs-line-7)"></text><text class="spieldocs-r2" x="854" y="190.8" textLength="12.2" clip-path="url(#spieldocs-line-7)">
</text><text class="spieldocs-r1" x="366" y="215.2" textLength="122" clip-path="url(#spieldocs-line-8)">╰────────╯</text><text class="spieldocs-r2" x="854" y="215.2" textLength="12.2" clip-path="url(#spieldocs-line-8)">
</text><text class="spieldocs-r2" x="854" y="239.6" textLength="12.2" clip-path="url(#spieldocs-line-9)">
</text><text class="spieldocs-r2" x="854" y="264" textLength="12.2" clip-path="url(#spieldocs-line-10)">
</text><text class="spieldocs-r2" x="854" y="288.4" textLength="12.2" clip-path="url(#spieldocs-line-11)">

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

@ -94,17 +94,17 @@
</g>
<g transform="translate(9, 41)" clip-path="url(#spieldocs-clip-terminal)">
<rect fill="#1e1e1e" x="0" y="1.5" width="854" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="25.9" width="854" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="50.3" width="854" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="74.7" width="854" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="99.1" width="170.8" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="170.8" y="99.1" width="512.4" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="683.2" y="99.1" width="170.8" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="123.5" width="170.8" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="170.8" y="123.5" width="195.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="366" y="123.5" width="122" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="488" y="123.5" width="195.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="683.2" y="123.5" width="170.8" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="147.9" width="170.8" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="170.8" y="147.9" width="195.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="366" y="147.9" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="378.2" y="147.9" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="390.4" y="147.9" width="73.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="463.6" y="147.9" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="475.8" y="147.9" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="488" y="147.9" width="195.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="683.2" y="147.9" width="170.8" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="172.3" width="170.8" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="170.8" y="172.3" width="195.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="366" y="172.3" width="122" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="488" y="172.3" width="195.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="683.2" y="172.3" width="170.8" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="196.7" width="854" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="221.1" width="854" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="245.5" width="854" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="269.9" width="854" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="294.3" width="854" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="318.7" width="854" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="343.1" width="463.6" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="463.6" y="343.1" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="475.8" y="343.1" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="488" y="343.1" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="500.2" y="343.1" width="353.8" height="24.65" shape-rendering="crispEdges"/>
<rect fill="#1e1e1e" x="0" y="1.5" width="854" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="25.9" width="854" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="50.3" width="854" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="74.7" width="854" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="99.1" width="170.8" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="170.8" y="99.1" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="183" y="99.1" width="475.8" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="658.8" y="99.1" width="24.4" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="683.2" y="99.1" width="170.8" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="123.5" width="170.8" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="170.8" y="123.5" width="512.4" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="683.2" y="123.5" width="170.8" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="147.9" width="170.8" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="170.8" y="147.9" width="195.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="366" y="147.9" width="122" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="488" y="147.9" width="195.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="683.2" y="147.9" width="170.8" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="172.3" width="170.8" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="170.8" y="172.3" width="195.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="366" y="172.3" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="378.2" y="172.3" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="390.4" y="172.3" width="73.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="463.6" y="172.3" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="475.8" y="172.3" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="488" y="172.3" width="195.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="683.2" y="172.3" width="170.8" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="196.7" width="170.8" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="170.8" y="196.7" width="195.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="366" y="196.7" width="122" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="488" y="196.7" width="195.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="683.2" y="196.7" width="170.8" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="221.1" width="854" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="245.5" width="854" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="269.9" width="854" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="294.3" width="854" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="318.7" width="854" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="343.1" width="463.6" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="463.6" y="343.1" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="475.8" y="343.1" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="488" y="343.1" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="500.2" y="343.1" width="353.8" height="24.65" shape-rendering="crispEdges"/>
<g class="spieldocs-matrix">
<text class="spieldocs-r2" x="854" y="20" textLength="12.2" clip-path="url(#spieldocs-line-0)">
</text><text class="spieldocs-r2" x="854" y="44.4" textLength="12.2" clip-path="url(#spieldocs-line-1)">
</text><text class="spieldocs-r2" x="854" y="68.8" textLength="12.2" clip-path="url(#spieldocs-line-2)">
</text><text class="spieldocs-r2" x="854" y="93.2" textLength="12.2" clip-path="url(#spieldocs-line-3)">
</text><text class="spieldocs-r1" x="170.8" y="117.6" textLength="512.4" clip-path="url(#spieldocs-line-4)">spaces_before_bang=1&#160;|&#160;spaces_after_bang=4</text><text class="spieldocs-r2" x="854" y="117.6" textLength="12.2" clip-path="url(#spieldocs-line-4)">
</text><text class="spieldocs-r1" x="366" y="142" textLength="122" clip-path="url(#spieldocs-line-5)">╭────────╮</text><text class="spieldocs-r2" x="854" y="142" textLength="12.2" clip-path="url(#spieldocs-line-5)">
</text><text class="spieldocs-r1" x="366" y="166.4" textLength="12.2" clip-path="url(#spieldocs-line-6)"></text><text class="spieldocs-r1" x="390.4" y="166.4" textLength="73.2" clip-path="url(#spieldocs-line-6)">&#160;!&#160;&#160;&#160;&#160;</text><text class="spieldocs-r1" x="475.8" y="166.4" textLength="12.2" clip-path="url(#spieldocs-line-6)"></text><text class="spieldocs-r2" x="854" y="166.4" textLength="12.2" clip-path="url(#spieldocs-line-6)">
</text><text class="spieldocs-r1" x="366" y="190.8" textLength="122" clip-path="url(#spieldocs-line-7)">╰────────╯</text><text class="spieldocs-r2" x="854" y="190.8" textLength="12.2" clip-path="url(#spieldocs-line-7)">
</text><text class="spieldocs-r2" x="854" y="215.2" textLength="12.2" clip-path="url(#spieldocs-line-8)">
</text><text class="spieldocs-r1" x="183" y="117.6" textLength="475.8" clip-path="url(#spieldocs-line-4)">triggers=Triggers(now=1.5,&#160;_times=(0,))</text><text class="spieldocs-r2" x="854" y="117.6" textLength="12.2" clip-path="url(#spieldocs-line-4)">
</text><text class="spieldocs-r1" x="170.8" y="142" textLength="512.4" clip-path="url(#spieldocs-line-5)">spaces_before_bang=1&#160;|&#160;spaces_after_bang=4</text><text class="spieldocs-r2" x="854" y="142" textLength="12.2" clip-path="url(#spieldocs-line-5)">
</text><text class="spieldocs-r1" x="366" y="166.4" textLength="122" clip-path="url(#spieldocs-line-6)">╭────────╮</text><text class="spieldocs-r2" x="854" y="166.4" textLength="12.2" clip-path="url(#spieldocs-line-6)">
</text><text class="spieldocs-r1" x="366" y="190.8" textLength="12.2" clip-path="url(#spieldocs-line-7)"></text><text class="spieldocs-r1" x="390.4" y="190.8" textLength="73.2" clip-path="url(#spieldocs-line-7)">&#160;!&#160;&#160;&#160;&#160;</text><text class="spieldocs-r1" x="475.8" y="190.8" textLength="12.2" clip-path="url(#spieldocs-line-7)"></text><text class="spieldocs-r2" x="854" y="190.8" textLength="12.2" clip-path="url(#spieldocs-line-7)">
</text><text class="spieldocs-r1" x="366" y="215.2" textLength="122" clip-path="url(#spieldocs-line-8)">╰────────╯</text><text class="spieldocs-r2" x="854" y="215.2" textLength="12.2" clip-path="url(#spieldocs-line-8)">
</text><text class="spieldocs-r2" x="854" y="239.6" textLength="12.2" clip-path="url(#spieldocs-line-9)">
</text><text class="spieldocs-r2" x="854" y="264" textLength="12.2" clip-path="url(#spieldocs-line-10)">
</text><text class="spieldocs-r2" x="854" y="288.4" textLength="12.2" clip-path="url(#spieldocs-line-11)">

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

@ -94,17 +94,17 @@
</g>
<g transform="translate(9, 41)" clip-path="url(#spieldocs-clip-terminal)">
<rect fill="#1e1e1e" x="0" y="1.5" width="854" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="25.9" width="854" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="50.3" width="854" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="74.7" width="854" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="99.1" width="170.8" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="170.8" y="99.1" width="512.4" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="683.2" y="99.1" width="170.8" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="123.5" width="170.8" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="170.8" y="123.5" width="195.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="366" y="123.5" width="122" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="488" y="123.5" width="195.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="683.2" y="123.5" width="170.8" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="147.9" width="170.8" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="170.8" y="147.9" width="195.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="366" y="147.9" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="378.2" y="147.9" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="390.4" y="147.9" width="73.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="463.6" y="147.9" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="475.8" y="147.9" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="488" y="147.9" width="195.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="683.2" y="147.9" width="170.8" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="172.3" width="170.8" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="170.8" y="172.3" width="195.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="366" y="172.3" width="122" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="488" y="172.3" width="195.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="683.2" y="172.3" width="170.8" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="196.7" width="854" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="221.1" width="854" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="245.5" width="854" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="269.9" width="854" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="294.3" width="854" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="318.7" width="854" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="343.1" width="463.6" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="463.6" y="343.1" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="475.8" y="343.1" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="488" y="343.1" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="500.2" y="343.1" width="353.8" height="24.65" shape-rendering="crispEdges"/>
<rect fill="#1e1e1e" x="0" y="1.5" width="854" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="25.9" width="854" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="50.3" width="854" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="74.7" width="854" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="99.1" width="170.8" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="170.8" y="99.1" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="183" y="99.1" width="475.8" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="658.8" y="99.1" width="24.4" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="683.2" y="99.1" width="170.8" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="123.5" width="170.8" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="170.8" y="123.5" width="512.4" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="683.2" y="123.5" width="170.8" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="147.9" width="170.8" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="170.8" y="147.9" width="195.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="366" y="147.9" width="122" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="488" y="147.9" width="195.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="683.2" y="147.9" width="170.8" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="172.3" width="170.8" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="170.8" y="172.3" width="195.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="366" y="172.3" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="378.2" y="172.3" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="390.4" y="172.3" width="73.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="463.6" y="172.3" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="475.8" y="172.3" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="488" y="172.3" width="195.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="683.2" y="172.3" width="170.8" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="196.7" width="170.8" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="170.8" y="196.7" width="195.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="366" y="196.7" width="122" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="488" y="196.7" width="195.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="683.2" y="196.7" width="170.8" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="221.1" width="854" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="245.5" width="854" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="269.9" width="854" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="294.3" width="854" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="318.7" width="854" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="343.1" width="463.6" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="463.6" y="343.1" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="475.8" y="343.1" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="488" y="343.1" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="500.2" y="343.1" width="353.8" height="24.65" shape-rendering="crispEdges"/>
<g class="spieldocs-matrix">
<text class="spieldocs-r2" x="854" y="20" textLength="12.2" clip-path="url(#spieldocs-line-0)">
</text><text class="spieldocs-r2" x="854" y="44.4" textLength="12.2" clip-path="url(#spieldocs-line-1)">
</text><text class="spieldocs-r2" x="854" y="68.8" textLength="12.2" clip-path="url(#spieldocs-line-2)">
</text><text class="spieldocs-r2" x="854" y="93.2" textLength="12.2" clip-path="url(#spieldocs-line-3)">
</text><text class="spieldocs-r1" x="170.8" y="117.6" textLength="512.4" clip-path="url(#spieldocs-line-4)">spaces_before_bang=3&#160;|&#160;spaces_after_bang=2</text><text class="spieldocs-r2" x="854" y="117.6" textLength="12.2" clip-path="url(#spieldocs-line-4)">
</text><text class="spieldocs-r1" x="366" y="142" textLength="122" clip-path="url(#spieldocs-line-5)">╭────────╮</text><text class="spieldocs-r2" x="854" y="142" textLength="12.2" clip-path="url(#spieldocs-line-5)">
</text><text class="spieldocs-r1" x="366" y="166.4" textLength="12.2" clip-path="url(#spieldocs-line-6)"></text><text class="spieldocs-r1" x="390.4" y="166.4" textLength="73.2" clip-path="url(#spieldocs-line-6)">&#160;&#160;&#160;!&#160;&#160;</text><text class="spieldocs-r1" x="475.8" y="166.4" textLength="12.2" clip-path="url(#spieldocs-line-6)"></text><text class="spieldocs-r2" x="854" y="166.4" textLength="12.2" clip-path="url(#spieldocs-line-6)">
</text><text class="spieldocs-r1" x="366" y="190.8" textLength="122" clip-path="url(#spieldocs-line-7)">╰────────╯</text><text class="spieldocs-r2" x="854" y="190.8" textLength="12.2" clip-path="url(#spieldocs-line-7)">
</text><text class="spieldocs-r2" x="854" y="215.2" textLength="12.2" clip-path="url(#spieldocs-line-8)">
</text><text class="spieldocs-r1" x="183" y="117.6" textLength="475.8" clip-path="url(#spieldocs-line-4)">triggers=Triggers(now=2.5,&#160;_times=(0,))</text><text class="spieldocs-r2" x="854" y="117.6" textLength="12.2" clip-path="url(#spieldocs-line-4)">
</text><text class="spieldocs-r1" x="170.8" y="142" textLength="512.4" clip-path="url(#spieldocs-line-5)">spaces_before_bang=2&#160;|&#160;spaces_after_bang=3</text><text class="spieldocs-r2" x="854" y="142" textLength="12.2" clip-path="url(#spieldocs-line-5)">
</text><text class="spieldocs-r1" x="366" y="166.4" textLength="122" clip-path="url(#spieldocs-line-6)">╭────────╮</text><text class="spieldocs-r2" x="854" y="166.4" textLength="12.2" clip-path="url(#spieldocs-line-6)">
</text><text class="spieldocs-r1" x="366" y="190.8" textLength="12.2" clip-path="url(#spieldocs-line-7)"></text><text class="spieldocs-r1" x="390.4" y="190.8" textLength="73.2" clip-path="url(#spieldocs-line-7)">&#160;&#160;!&#160;&#160;&#160;</text><text class="spieldocs-r1" x="475.8" y="190.8" textLength="12.2" clip-path="url(#spieldocs-line-7)"></text><text class="spieldocs-r2" x="854" y="190.8" textLength="12.2" clip-path="url(#spieldocs-line-7)">
</text><text class="spieldocs-r1" x="366" y="215.2" textLength="122" clip-path="url(#spieldocs-line-8)">╰────────╯</text><text class="spieldocs-r2" x="854" y="215.2" textLength="12.2" clip-path="url(#spieldocs-line-8)">
</text><text class="spieldocs-r2" x="854" y="239.6" textLength="12.2" clip-path="url(#spieldocs-line-9)">
</text><text class="spieldocs-r2" x="854" y="264" textLength="12.2" clip-path="url(#spieldocs-line-10)">
</text><text class="spieldocs-r2" x="854" y="288.4" textLength="12.2" clip-path="url(#spieldocs-line-11)">

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

@ -94,17 +94,17 @@
</g>
<g transform="translate(9, 41)" clip-path="url(#spieldocs-clip-terminal)">
<rect fill="#1e1e1e" x="0" y="1.5" width="854" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="25.9" width="854" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="50.3" width="854" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="74.7" width="854" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="99.1" width="170.8" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="170.8" y="99.1" width="512.4" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="683.2" y="99.1" width="170.8" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="123.5" width="170.8" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="170.8" y="123.5" width="195.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="366" y="123.5" width="122" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="488" y="123.5" width="195.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="683.2" y="123.5" width="170.8" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="147.9" width="170.8" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="170.8" y="147.9" width="195.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="366" y="147.9" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="378.2" y="147.9" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="390.4" y="147.9" width="73.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="463.6" y="147.9" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="475.8" y="147.9" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="488" y="147.9" width="195.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="683.2" y="147.9" width="170.8" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="172.3" width="170.8" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="170.8" y="172.3" width="195.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="366" y="172.3" width="122" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="488" y="172.3" width="195.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="683.2" y="172.3" width="170.8" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="196.7" width="854" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="221.1" width="854" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="245.5" width="854" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="269.9" width="854" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="294.3" width="854" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="318.7" width="854" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="343.1" width="463.6" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="463.6" y="343.1" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="475.8" y="343.1" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="488" y="343.1" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="500.2" y="343.1" width="353.8" height="24.65" shape-rendering="crispEdges"/>
<rect fill="#1e1e1e" x="0" y="1.5" width="854" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="25.9" width="854" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="50.3" width="854" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="74.7" width="854" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="99.1" width="170.8" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="170.8" y="99.1" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="183" y="99.1" width="475.8" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="658.8" y="99.1" width="24.4" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="683.2" y="99.1" width="170.8" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="123.5" width="170.8" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="170.8" y="123.5" width="512.4" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="683.2" y="123.5" width="170.8" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="147.9" width="170.8" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="170.8" y="147.9" width="195.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="366" y="147.9" width="122" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="488" y="147.9" width="195.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="683.2" y="147.9" width="170.8" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="172.3" width="170.8" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="170.8" y="172.3" width="195.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="366" y="172.3" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="378.2" y="172.3" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="390.4" y="172.3" width="73.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="463.6" y="172.3" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="475.8" y="172.3" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="488" y="172.3" width="195.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="683.2" y="172.3" width="170.8" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="196.7" width="170.8" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="170.8" y="196.7" width="195.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="366" y="196.7" width="122" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="488" y="196.7" width="195.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="683.2" y="196.7" width="170.8" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="221.1" width="854" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="245.5" width="854" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="269.9" width="854" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="294.3" width="854" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="318.7" width="854" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="343.1" width="463.6" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="463.6" y="343.1" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="475.8" y="343.1" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="488" y="343.1" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="500.2" y="343.1" width="353.8" height="24.65" shape-rendering="crispEdges"/>
<g class="spieldocs-matrix">
<text class="spieldocs-r2" x="854" y="20" textLength="12.2" clip-path="url(#spieldocs-line-0)">
</text><text class="spieldocs-r2" x="854" y="44.4" textLength="12.2" clip-path="url(#spieldocs-line-1)">
</text><text class="spieldocs-r2" x="854" y="68.8" textLength="12.2" clip-path="url(#spieldocs-line-2)">
</text><text class="spieldocs-r2" x="854" y="93.2" textLength="12.2" clip-path="url(#spieldocs-line-3)">
</text><text class="spieldocs-r1" x="170.8" y="117.6" textLength="512.4" clip-path="url(#spieldocs-line-4)">spaces_before_bang=5&#160;|&#160;spaces_after_bang=0</text><text class="spieldocs-r2" x="854" y="117.6" textLength="12.2" clip-path="url(#spieldocs-line-4)">
</text><text class="spieldocs-r1" x="366" y="142" textLength="122" clip-path="url(#spieldocs-line-5)">╭────────╮</text><text class="spieldocs-r2" x="854" y="142" textLength="12.2" clip-path="url(#spieldocs-line-5)">
</text><text class="spieldocs-r1" x="366" y="166.4" textLength="12.2" clip-path="url(#spieldocs-line-6)"></text><text class="spieldocs-r1" x="390.4" y="166.4" textLength="73.2" clip-path="url(#spieldocs-line-6)">&#160;&#160;&#160;&#160;&#160;!</text><text class="spieldocs-r1" x="475.8" y="166.4" textLength="12.2" clip-path="url(#spieldocs-line-6)"></text><text class="spieldocs-r2" x="854" y="166.4" textLength="12.2" clip-path="url(#spieldocs-line-6)">
</text><text class="spieldocs-r1" x="366" y="190.8" textLength="122" clip-path="url(#spieldocs-line-7)">╰────────╯</text><text class="spieldocs-r2" x="854" y="190.8" textLength="12.2" clip-path="url(#spieldocs-line-7)">
</text><text class="spieldocs-r2" x="854" y="215.2" textLength="12.2" clip-path="url(#spieldocs-line-8)">
</text><text class="spieldocs-r1" x="183" y="117.6" textLength="475.8" clip-path="url(#spieldocs-line-4)">triggers=Triggers(now=5.5,&#160;_times=(0,))</text><text class="spieldocs-r2" x="854" y="117.6" textLength="12.2" clip-path="url(#spieldocs-line-4)">
</text><text class="spieldocs-r1" x="170.8" y="142" textLength="512.4" clip-path="url(#spieldocs-line-5)">spaces_before_bang=5&#160;|&#160;spaces_after_bang=0</text><text class="spieldocs-r2" x="854" y="142" textLength="12.2" clip-path="url(#spieldocs-line-5)">
</text><text class="spieldocs-r1" x="366" y="166.4" textLength="122" clip-path="url(#spieldocs-line-6)">╭────────╮</text><text class="spieldocs-r2" x="854" y="166.4" textLength="12.2" clip-path="url(#spieldocs-line-6)">
</text><text class="spieldocs-r1" x="366" y="190.8" textLength="12.2" clip-path="url(#spieldocs-line-7)"></text><text class="spieldocs-r1" x="390.4" y="190.8" textLength="73.2" clip-path="url(#spieldocs-line-7)">&#160;&#160;&#160;&#160;&#160;!</text><text class="spieldocs-r1" x="475.8" y="190.8" textLength="12.2" clip-path="url(#spieldocs-line-7)"></text><text class="spieldocs-r2" x="854" y="190.8" textLength="12.2" clip-path="url(#spieldocs-line-7)">
</text><text class="spieldocs-r1" x="366" y="215.2" textLength="122" clip-path="url(#spieldocs-line-8)">╰────────╯</text><text class="spieldocs-r2" x="854" y="215.2" textLength="12.2" clip-path="url(#spieldocs-line-8)">
</text><text class="spieldocs-r2" x="854" y="239.6" textLength="12.2" clip-path="url(#spieldocs-line-9)">
</text><text class="spieldocs-r2" x="854" y="264" textLength="12.2" clip-path="url(#spieldocs-line-10)">
</text><text class="spieldocs-r2" x="854" y="288.4" textLength="12.2" clip-path="url(#spieldocs-line-11)">

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

@ -4,6 +4,10 @@
*Unreleased*
### Added
- [#207](https://github.com/JoshKarpel/spiel/pull/207) Add a default "swipe" transition between slides and support for user-defined transitions.
## `0.4.6`
Released `2023-01-19`

@ -28,10 +28,9 @@ To set up a local development environment after cloning the repository:
### Running Tests and Type-Checking
Use `pytest` to run tests.
Run `pytest` to run tests.
Types will automatically be checked as part of the test suite run;
`mypy` can also be run standalone.
Run `mypy` to check types.
### Building the Docs Locally

@ -23,6 +23,7 @@ def animate(triggers: Triggers) -> RenderableType:
return Align(
Group(
Align.center(Text(f"{triggers=}")),
Align.center(Text(f"{spaces_before_bang=} | {spaces_after_bang=}")),
Align.center(Panel(Text(bar), expand=False, height=3)),
),

@ -14,8 +14,9 @@ from rich.console import Console
from textual.app import App
from textual.pilot import Pilot
import spiel.constants
from spiel.app import SpielApp
from spiel.constants import DEMO_FILE
from spiel.triggers import Triggers
ROOT_DIR = Path(__file__).resolve().parent.parent
ASSETS_DIR = ROOT_DIR / "docs" / "assets"
@ -52,14 +53,22 @@ async def auto_pilot(pilot: Pilot[object], name: str, keys: Iterable[str]) -> No
await pilot.app.action_quit()
def take_screenshot(name: str, deck_file: Path, size: tuple[int, int], keys: Iterable[str]) -> str:
def take_screenshot(
name: str,
deck_file: Path,
size: tuple[int, int],
keys: Iterable[str],
triggers: Triggers,
) -> str:
print(f"Generating {name}")
SpielApp(
deck_path=deck_file,
watch_path=deck_file.parent,
show_messages=False,
fixed_time=datetime(year=2022, month=12, day=17, hour=15, minute=31, second=42),
_show_messages=False,
_fixed_time=datetime(year=2022, month=12, day=17, hour=15, minute=31, second=42),
_fixed_triggers=triggers,
_enable_transitions=False,
).run(
headless=True,
auto_pilot=partial(auto_pilot, name=name, keys=keys),
@ -72,13 +81,15 @@ def take_screenshot(name: str, deck_file: Path, size: tuple[int, int], keys: Ite
if __name__ == "__main__":
start_time = monotonic()
demo_deck = spiel.constants.DEMO_FILE
demo_deck = DEMO_FILE
quickstart_deck = ROOT_DIR / "docs" / "examples" / "quickstart.py"
slide_via_decorator = ROOT_DIR / "docs" / "examples" / "slide_via_decorator.py"
slide_loop = ROOT_DIR / "docs" / "examples" / "slide_loop.py"
triggers_reveal = ROOT_DIR / "docs" / "examples" / "triggers_reveal.py"
triggers_animation = ROOT_DIR / "docs" / "examples" / "triggers_animation.py"
triggers = Triggers(now=0, _times=(0,))
with ProcessPoolExecutor() as pool:
futures = [
pool.submit(
@ -87,27 +98,31 @@ if __name__ == "__main__":
deck_file=triggers_animation,
size=(70, 15),
keys=(),
triggers=Triggers(now=0, _times=(0,)),
),
pool.submit(
take_screenshot,
name="triggers_animation_2",
deck_file=triggers_animation,
size=(70, 15),
keys=("wait:1450",),
keys=(),
triggers=Triggers(now=1.5, _times=(0,)),
),
pool.submit(
take_screenshot,
name="triggers_animation_3",
deck_file=triggers_animation,
size=(70, 15),
keys=("wait:2950",),
keys=(),
triggers=Triggers(now=2.5, _times=(0,)),
),
pool.submit(
take_screenshot,
name="triggers_animation_4",
deck_file=triggers_animation,
size=(70, 15),
keys=("wait:5450",),
keys=(),
triggers=Triggers(now=5.5, _times=(0,)),
),
pool.submit(
take_screenshot,
@ -115,6 +130,7 @@ if __name__ == "__main__":
deck_file=demo_deck,
size=(130, 35),
keys=(),
triggers=triggers,
),
pool.submit(
take_screenshot,
@ -122,6 +138,7 @@ if __name__ == "__main__":
deck_file=demo_deck,
size=(135, 40),
keys=("d", "right", "down"),
triggers=triggers,
),
pool.submit(
take_screenshot,
@ -129,6 +146,7 @@ if __name__ == "__main__":
deck_file=demo_deck,
size=(110, 35),
keys=("?",),
triggers=triggers,
),
pool.submit(
take_screenshot,
@ -136,6 +154,7 @@ if __name__ == "__main__":
deck_file=quickstart_deck,
size=(70, 20),
keys=(),
triggers=triggers,
),
pool.submit(
take_screenshot,
@ -143,6 +162,7 @@ if __name__ == "__main__":
deck_file=demo_deck,
size=(140, 45),
keys=("right",),
triggers=triggers,
),
pool.submit(
take_screenshot,
@ -150,6 +170,7 @@ if __name__ == "__main__":
deck_file=slide_via_decorator,
size=(60, 15),
keys=(),
triggers=triggers,
),
pool.submit(
take_screenshot,
@ -157,6 +178,7 @@ if __name__ == "__main__":
deck_file=slide_loop,
size=(60, 15),
keys=(),
triggers=triggers,
),
pool.submit(
take_screenshot,
@ -164,6 +186,7 @@ if __name__ == "__main__":
deck_file=slide_loop,
size=(60, 15),
keys=("right",),
triggers=triggers,
),
pool.submit(
take_screenshot,
@ -171,6 +194,7 @@ if __name__ == "__main__":
deck_file=slide_loop,
size=(60, 15),
keys=("right", "right"),
triggers=triggers,
),
pool.submit(
take_screenshot,
@ -178,20 +202,23 @@ if __name__ == "__main__":
deck_file=triggers_reveal,
size=(70, 15),
keys=(),
triggers=Triggers(now=0, _times=(0,)),
),
pool.submit(
take_screenshot,
name="triggers_reveal_2",
deck_file=triggers_reveal,
size=(70, 15),
keys=("t",),
keys=(),
triggers=Triggers(now=1, _times=(0, 1)),
),
pool.submit(
take_screenshot,
name="triggers_reveal_3",
deck_file=triggers_reveal,
size=(70, 15),
keys=("t", "t"),
keys=(),
triggers=Triggers(now=2, _times=(0, 1, 2)),
),
]

@ -0,0 +1,101 @@
# Slide Transitions
!!! warning "Under construction!"
Transitions are a new and experiment feature in Spiel
and the interface might change dramatically from version to version.
If you plan on using transitions, we recommend pinning the
exact version of Spiel your presentation was developed in to ensure stability.
## Setting Transitions
To set the default transition for the entire deck,
which will be used if a slide does not override it,
set [`Deck.default_transition`][spiel.Deck.default_transition] to
a **type** that implements the [`Transition`][spiel.Transition]
protocol.
For example, the default transition is [`Swipe`][spiel.Swipe],
so not passing `default_transition` at all is equivalent to
```python
from spiel import Deck, Swipe
deck = Deck(name=f"Spiel Demo Deck", default_transition=Swipe)
```
To override the deck-wide default for an individual slide,
specify the transition type in the [`@slide`][spiel.Deck.slide] decorator:
```python
from spiel import Deck, Swipe
deck = Deck(name=f"Spiel Demo Deck")
@deck.slide(title="My Title", transition=Swipe)
def slide():
...
```
Or, in the arguments to [`Slide`][spiel.Slide]:
```python
from spiel import Slide, Swipe
slide = Slide(title="My Title", transition=Swipe)
```
In either case, the specified transition will be used when
transitioning **to** that slide.
It does not matter whether the slide is the "next" or "previous"
slide: the slide being moved to determines which transition
effect will be used.
## Disabling Transitions
In any of the above examples, you can also set `default_transition`/`transition` to `None`.
In that case, there will be no transition effect when moving to the slide;
it will just be displayed on the next render, already in-place.
## Writing Custom Transitions
To implement your own custom transition, you must write a class which implements
the [`Transition`][spiel.Transition] [protocol](https://docs.python.org/3/library/typing.html#typing.Protocol).
The protocol is:
```python title="Transition Protocol"
--8<-- "../spiel/transitions/protocol.py"
```
As an example, consider the [`Swipe`][spiel.Swipe] transition included in Spiel:
```python title="Swipe Transition"
--8<-- "../spiel/transitions/swipe.py"
```
The transition effect is implemented using
[Textual CSS styles](https://textual.textualize.io/styles/)
on the [widgets](https://textual.textualize.io/guide/widgets/)
that represent the "from" and "to" widgets.
Because the slide widgets are on [different layers](https://textual.textualize.io/styles/layers/),
they would normally both try to render in the "upper left corner" of the screen,
and since the `from` slide is on the upper layer, it would be the one that actually gets rendered.
In `Swipe.initialize`, the `to` widget is moved to either the left or the right
(depending on the transition direction) by `100%`, i.e., it's own width.
This puts the slides side-by-side, with the `to` slide fully off-screen.
As the transition progresses, the horizontal offsets of the two widgets are adjusted in lockstep
so that they appear to move across the screen.
Again, the direction of offset adjustment depends on the transition direction.
The absolute value of the horizontal offsets always sums to `100%`, which keeps the slides glued together
as they move across the screen.
When `progress=100` in the final state, the `to` widget will be at zero horizontal offset,
and the `from` widget will be at plus or minus `100%`, fully moved off-screen.
!!! tip "Contribute your transitions!"
If you have developed a cool transition, consider [contributing it to Spiel](./contributing.md)!

@ -84,6 +84,7 @@ nav:
- quickstart.md
- presenting.md
- slides.md
- transitions.md
- api.md
- gallery.md
- contributing.md

702
poetry.lock generated

File diff suppressed because it is too large Load Diff

@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"
[tool.poetry]
name = "spiel"
version = "0.4.6"
version = "0.5.0"
description = "A framework for building and presenting richly-styled presentations in your terminal using Python."
readme="README.md"
homepage="https://github.com/JoshKarpel/spiel"
@ -42,24 +42,23 @@ python = ">=3.10,<4"
rich = ">=13.2"
typer = ">=0.6"
pillow = ">=8"
textual = ">=0.10.0"
textual = ">=0.11.0"
watchfiles = ">=0.18"
more-itertools = ">=9"
[tool.poetry.group.dev.dependencies]
pre-commit = ">=2.20.0"
pre-commit = ">=3"
pytest = ">=7"
pytest-cov = ">=3"
pytest-xdist = ">=3"
mypy = ">=0.991"
pytest-mypy = ">=0.10"
pytest-asyncio = ">=0.20"
pytest-mock = ">=3"
hypothesis = ">=6"
textual = {extras = ["dev"], version = ">=0.10.0"}
mypy = ">=1"
mkdocs = ">=1.4"
mkdocs-material = ">=9"
mkdocstrings = {extras = ["python"], version = ">=0.19.0"}
pytest-asyncio = ">=0.20"
textual = {extras = ["dev"], version = ">=0.11.0"}
[tool.poetry.scripts]
spiel = 'spiel.cli:cli'
@ -76,7 +75,7 @@ line_length = 100
all = true
[tool.pytest.ini_options]
addopts = ["--strict-markers", "--mypy", "-n", "auto"]
addopts = ["--strict-markers"]
testpaths = ["tests", "spiel", "docs"]
markers = ["slow"]

@ -2,6 +2,18 @@ from spiel.app import SuspendType, present
from spiel.constants import __version__
from spiel.deck import Deck
from spiel.slide import Slide
from spiel.transitions.protocol import Direction, Transition
from spiel.transitions.swipe import Swipe
from spiel.triggers import Triggers
__all__ = ["present", "SuspendType", "__version__", "Deck", "Slide", "Triggers"]
__all__ = [
"Deck",
"Direction",
"Slide",
"SuspendType",
"Swipe",
"Transition",
"Triggers",
"__version__",
"present",
]

@ -28,6 +28,8 @@ from spiel.exceptions import NoDeckFound
from spiel.screens.deck import DeckScreen
from spiel.screens.help import HelpScreen
from spiel.screens.slide import SlideScreen
from spiel.screens.transition import SlideTransitionScreen
from spiel.transitions.protocol import Direction
from spiel.triggers import Triggers
from spiel.utils import clamp
from spiel.widgets.slide import SlideWidget
@ -80,24 +82,30 @@ class SpielApp(App[None]):
self,
deck_path: Path,
watch_path: Path | None = None,
show_messages: bool = True,
fixed_time: datetime.datetime | None = None,
_show_messages: bool = True,
_fixed_time: datetime.datetime | None = None,
_fixed_triggers: Triggers | None = None,
_enable_transitions: bool = True,
_slide_refresh_rate: float = 1 / 60,
):
super().__init__()
self.deck_path = deck_path
self.watch_path = watch_path
self.show_messages = show_messages
self.fixed_time = fixed_time
self.show_messages = _show_messages
self.fixed_time = _fixed_time
self.fixed_triggers = _fixed_triggers
self.enable_transitions = _enable_transitions
self.slide_refresh_rate = _slide_refresh_rate
async def on_mount(self) -> None:
self.deck = load_deck(self.deck_path)
self.reloader = asyncio.create_task(self.reload())
await self.install_screen(SlideScreen(), name="slide")
await self.install_screen(DeckScreen(), name="deck")
await self.install_screen(HelpScreen(), name="help")
self.install_screen(SlideScreen(), name="slide")
self.install_screen(DeckScreen(), name="deck")
self.install_screen(HelpScreen(), name="help")
await self.push_screen("slide")
async def reload(self) -> None:
@ -144,11 +152,49 @@ class SpielApp(App[None]):
self.set_timer(delay, clear)
def action_next_slide(self) -> None:
self.current_slide_idx = clamp(self.current_slide_idx + 1, 0, len(self.deck) - 1)
async def action_next_slide(self) -> None:
await self.handle_new_slide(self.current_slide_idx + 1, Direction.Next)
def action_prev_slide(self) -> None:
self.current_slide_idx = clamp(self.current_slide_idx - 1, 0, len(self.deck) - 1)
async def action_prev_slide(self) -> None:
await self.handle_new_slide(self.current_slide_idx - 1, Direction.Previous)
async def handle_new_slide(self, new_slide_idx: int, direction: Direction) -> None:
new_slide_idx = clamp(new_slide_idx, 0, len(self.deck) - 1)
current_slide = self.deck[self.current_slide_idx]
new_slide = self.deck[new_slide_idx]
transition = new_slide.transition or self.deck.default_transition
if (
self.current_slide_idx == new_slide_idx
or not isinstance(self.screen, SlideScreen)
or transition is None
or not self.enable_transitions
):
self.current_slide_idx = new_slide_idx
return
transition_screen = SlideTransitionScreen(
from_slide=current_slide,
from_triggers=self.query_one(SlideWidget).triggers,
to_slide=new_slide,
direction=direction,
transition=transition,
)
await self.switch_screen(transition_screen)
transition_screen.animate(
"progress",
value=100,
delay=0,
duration=0.75,
on_complete=lambda: self.finalize_transition(new_slide_idx),
)
async def finalize_transition(self, new_slide_idx: int) -> None:
await self.switch_screen("slide")
self.current_slide_idx = new_slide_idx
def action_next_row(self) -> None:
self.current_slide_idx = clamp(
@ -164,7 +210,7 @@ class SpielApp(App[None]):
self.title = new_deck.name
def watch_current_slide_idx(self, new_current_slide_idx: int) -> None:
self.query_one(SlideWidget).triggers = Triggers.new()
self.query_one(SlideWidget).triggers = self.fixed_triggers or Triggers.new()
self.sub_title = self.deck[new_current_slide_idx].title
def action_trigger(self) -> None:
@ -179,7 +225,10 @@ class SpielApp(App[None]):
@cached_property
def repl(self) -> Callable[[], None]:
# Lazily enable readline support
import readline # nopycln: import
try:
import readline # nopycln: import
except ImportError:
pass
self.console.clear() # clear the console the first time we go into the repl
sys.stdout.flush()
@ -203,7 +252,6 @@ class SpielApp(App[None]):
if driver is not None:
driver.stop_application_mode()
driver.exit_event.clear() # type: ignore[attr-defined]
with redirect_stdout(sys.__stdout__), redirect_stderr(sys.__stderr__):
yield
driver.start_application_mode()

@ -59,77 +59,6 @@ def _present(
present(deck_path=path, watch_path=watch)
@cli.command()
def init(
path: Path = Argument(
...,
writable=True,
resolve_path=True,
help="The path to create a new deck script at.",
)
) -> None:
"""
Create a new deck script at the given path from a basic template.
This is a good starting point if you already know what you want to do.
If you're not so sure, consider taking a look at the demo deck to see what's possible:
$ spiel demo --help
"""
console = Console()
if path.exists():
console.print(
Text(f"Error: {path} already exists, refusing to overwrite.", style=Style(color="red"))
)
raise Exit(code=1)
name = path.stem.replace("_", " ").title()
try:
path.parent.mkdir(parents=True, exist_ok=True)
except Exception as e:
console.print(
Text(
f"Error: was not able to ensure that the parent directory {path.parent} exists due to: {e}.",
style=Style(color="red"),
)
)
raise Exit(code=1)
try:
path.write_text(
dedent(
f"""\
from textwrap import dedent
from spiel import Deck
deck = Deck(name="{name}")
@deck.slide(title="Title")
def title():
markup = dedent(
\"""\\
# {name}
This is your title slide!
\"""
)
return Markdown(markup, justify="center")
"""
)
)
except Exception as e:
console.print(
Text(
f"Error: was not able to write template to {path} due to: {e}",
style=Style(color="red"),
)
)
raise Exit(code=1)
console.print(Text(f"Wrote deck template to {path}", style=Style(color="green")))
demo = Typer(
name="demo",
no_args_is_help=True,
@ -206,6 +135,6 @@ def version(
"""
if plain:
console.print(__version__, style=Style())
console.print(Text(__version__))
else:
console.print(DebugTable())

@ -1,3 +1,5 @@
from __future__ import annotations
import sys
from importlib import metadata
from pathlib import Path

@ -2,9 +2,11 @@ from __future__ import annotations
from collections.abc import Callable, Iterator, Mapping, Sequence
from dataclasses import dataclass, field
from typing import overload
from typing import Type, overload
from spiel.slide import Content, Slide
from spiel.transitions.protocol import Transition
from spiel.transitions.swipe import Swipe
@dataclass
@ -14,7 +16,15 @@ class Deck(Sequence[Slide]):
"""
name: str
"""The name of the `Deck`/presentation, which will be displayed in the footer."""
"""The name of the [`Deck`][spiel.Deck], which will be displayed in the footer."""
default_transition: Type[Transition] | None = Swipe
"""\
The default slide transition animation;
used if the slide being moved to does not specify its own transition.
Defaults to the [`Swipe`][spiel.Swipe] transition.
Set to `None` for no transition animation.
"""
_slides: list[Slide] = field(default_factory=list)
@ -22,10 +32,11 @@ class Deck(Sequence[Slide]):
self,
title: str = "",
bindings: Mapping[str, Callable[..., None]] | None = None,
transition: Type[Transition] | None = None,
) -> Callable[[Content], Content]:
"""
A decorator that creates a new slide in the deck,
with the decorated function as the `Slide`'s `content`.
with the decorated function as the [`Slide.content`][spiel.Slide.content].
Args:
title: The title to display for the slide.
@ -33,6 +44,10 @@ class Deck(Sequence[Slide]):
[keys](https://textual.textualize.io/guide/input/#key)
to callables to be executed when those keys are pressed,
when on this slide.
transition: The transition animation to use when moving to this slide.
Set to `None` to use the
[`Deck.default_transition`][spiel.Deck.default_transition]
of the deck this slide is in.
"""
def slideify(content: Content) -> Content:
@ -41,6 +56,7 @@ class Deck(Sequence[Slide]):
title=title,
content=content,
bindings=bindings or {},
transition=transition,
)
)
return content

@ -0,0 +1,85 @@
from __future__ import annotations
from typing import Type
from textual.app import ComposeResult
from textual.reactive import reactive
from spiel.screens.screen import SpielScreen
from spiel.slide import Slide
from spiel.transitions.protocol import Direction, Transition
from spiel.triggers import Triggers
from spiel.widgets.fixed_slide import FixedSlideWidget
from spiel.widgets.footer import Footer
class SlideTransitionScreen(SpielScreen):
DEFAULT_CSS = """\
SlideTransitionScreen {
layout: vertical;
overflow: hidden hidden;
layers: below above;
}
FixedSlideWidget#from {
layer: above;
}
FixedSlideWidget#to {
layer: below;
}
Footer {
layer: above;
}
Footer#dummy {
layer: below;
}
"""
progress = reactive(0, init=False, layout=True)
def __init__(
self,
from_slide: Slide,
from_triggers: Triggers,
to_slide: Slide,
transition: Type[Transition],
direction: Direction,
):
super().__init__()
self.from_slide = from_slide
self.from_triggers = from_triggers
self.to_slide = to_slide
self.transition = transition()
self.direction = direction
def compose(self) -> ComposeResult:
from_widget = FixedSlideWidget(self.from_slide, triggers=self.from_triggers, id="from")
to_widget = FixedSlideWidget(self.to_slide, id="to")
self.transition.initialize(
from_widget=from_widget,
to_widget=to_widget,
direction=self.direction,
)
yield from_widget
yield to_widget
yield Footer()
yield Footer(
id="dummy"
) # a dummy footer to hold space on the "below" layer, won't be displayed
def watch_progress(self, new_progress: float) -> None:
from_widget = self.query_one("#from")
to_widget = self.query_one("#to")
self.transition.progress(
from_widget=from_widget,
to_widget=to_widget,
direction=self.direction,
progress=new_progress,
)

@ -2,11 +2,13 @@ from __future__ import annotations
import inspect
from dataclasses import dataclass, field
from typing import Callable, Mapping
from typing import Callable, Mapping, Type
from rich.console import RenderableType
from rich.text import Text
from spiel.transitions.protocol import Transition
from spiel.transitions.swipe import Swipe
from spiel.triggers import Triggers
TRIGGERS = "triggers"
@ -24,7 +26,7 @@ class Slide:
"""The title of the `Slide`, which will be displayed in the footer."""
content: Content = lambda: Text()
"""
"""\
A callable that is invoked by Spiel to display the slide's content.
The function may optionally take arguments with these names:
@ -33,6 +35,20 @@ class Slide:
"""
bindings: Mapping[str, Callable[..., None]] = field(default_factory=dict)
"""\
A mapping of
[keys](https://textual.textualize.io/guide/input/#key)
to callables to be executed when those keys are pressed,
when on this slide.
"""
transition: Type[Transition] | None = Swipe
"""\
The transition animation to use when moving to this slide.
Set to `None` to use the
[`Deck.default_transition`][spiel.Deck.default_transition]
of the deck this slide is in.
"""
def render(self, triggers: Triggers) -> RenderableType:
signature = inspect.signature(self.content)

@ -0,0 +1,72 @@
from __future__ import annotations
from enum import Enum
from typing import Protocol, runtime_checkable
from textual.widget import Widget
class Direction(Enum):
"""
An enumeration that describes which direction a slide transition
animation should move in: whether we're going to the next slide,
or to the previous slide.
"""
Next = "next"
"""Indicates that the transition should handle going to the next slide."""
Previous = "previous"
"""Indicates that the transition should handle going to the previous slide."""
@runtime_checkable
class Transition(Protocol):
"""
A protocol that describes how to implement a transition animation.
See [Writing Custom Transitions](./transitions.md#writing-custom-transitions)
for more details on how to implement the protocol.
"""
def initialize(
self,
from_widget: Widget,
to_widget: Widget,
direction: Direction,
) -> None:
"""
A hook function to set up any CSS that should be present at the start of the transition.
Args:
from_widget: The widget showing the slide that we are leaving.
to_widget: The widget showing the slide that we are entering.
direction: The desired direction of the transition animation.
"""
...
def progress(
self,
from_widget: Widget,
to_widget: Widget,
direction: Direction,
progress: float,
) -> None:
"""
A hook function that is called each time the `progress`
of the transition animation updates.
Args:
from_widget: The widget showing the slide that we are leaving.
to_widget: The widget showing the slide that we are entering.
direction: The desired direction of the transition animation.
progress: The progress of the animation, as a percentage
(e.g., initial state is `0`, final state is `100`).
Note that this is **not necessarily** bounded between `0` and `100`,
nor is it necessarily [monotonically increasing](https://en.wikipedia.org/wiki/Monotonic_function),
depending on the underlying Textual animation easing function,
which may overshoot or bounce.
However, it will always start at `0` and end at `100`,
no matter which `direction` the transition should move in.
"""
...

@ -0,0 +1,36 @@
from __future__ import annotations
from textual.widget import Widget
from spiel.transitions.protocol import Direction, Transition
class Swipe(Transition):
"""
A transition where the current and incoming slide are placed side-by-side
and gradually slide across the screen,
with the current slide leaving and the incoming slide entering.
"""
def initialize(
self,
from_widget: Widget,
to_widget: Widget,
direction: Direction,
) -> None:
to_widget.styles.offset = ("100%" if direction is Direction.Next else "-100%", 0)
def progress(
self,
from_widget: Widget,
to_widget: Widget,
direction: Direction,
progress: float,
) -> None:
match direction:
case Direction.Next:
from_widget.styles.offset = (f"-{progress:.2f}%", 0)
to_widget.styles.offset = (f"{100 - progress:.2f}%", 0)
case Direction.Previous:
from_widget.styles.offset = (f"{progress:.2f}%", 0)
to_widget.styles.offset = (f"-{100 - progress:.2f}%", 0)

@ -0,0 +1,50 @@
from __future__ import annotations
import sys
from rich.box import HEAVY
from rich.console import RenderableType
from rich.errors import NotRenderableError
from rich.panel import Panel
from rich.protocol import is_renderable
from rich.style import Style
from rich.traceback import Traceback
import spiel
from spiel.exceptions import SpielException
from spiel.slide import Slide
from spiel.triggers import Triggers
from spiel.widgets.widget import SpielWidget
class FixedSlideWidget(SpielWidget):
def __init__(self, slide: Slide, triggers: Triggers | None = None, id: str | None = None):
super().__init__(id=id)
self.slide = slide
self.triggers = triggers or Triggers.new()
def render(self) -> RenderableType:
try:
self.remove_class("error")
r = self.slide.render(triggers=self.triggers)
if is_renderable(r):
return r
else:
raise NotRenderableError(f"object {r!r} is not renderable")
except Exception:
self.add_class("error")
et, ev, tr = sys.exc_info()
if et is None or ev is None or tr is None:
raise SpielException("Expected to be handling an exception, but wasn't.")
return Panel(
Traceback.from_exception(
exc_type=et,
exc_value=ev,
traceback=tr,
suppress=(spiel,),
),
title="Slide content failed to render",
border_style=Style(bold=True, color="red1"),
box=HEAVY,
)

@ -24,7 +24,10 @@ class SlideWidget(SpielWidget):
def on_mount(self) -> None:
super().on_mount()
self.set_interval(1 / 60, self.update_triggers)
if not self.app.fixed_triggers:
self.set_interval(self.app.slide_refresh_rate, self.update_triggers)
else:
self.triggers = self.app.fixed_triggers
def update_triggers(self) -> None:
self.triggers = Triggers(now=monotonic(), _times=self.triggers._times)
@ -40,7 +43,7 @@ class SlideWidget(SpielWidget):
except Exception:
self.add_class("error")
et, ev, tr = sys.exc_info()
if et is None or ev is None or tr is None:
if et is None or ev is None or tr is None: # pragma: unreachable
raise SpielException("Expected to be handling an exception, but wasn't.")
return Panel(
Traceback.from_exception(

@ -2,7 +2,7 @@ from __future__ import annotations
from typing import TYPE_CHECKING
from textual.reactive import watch
from textual.reactive import _watch
from textual.widget import Widget
from spiel.slide import Slide
@ -15,12 +15,9 @@ class SpielWidget(Widget):
app: "SpielApp"
def on_mount(self) -> None:
watch(self.app, "deck", self.r)
watch(self.app, "current_slide_idx", self.r)
watch(self.app, "message", self.r)
def r(self, _: object) -> None:
self.refresh()
_watch(self, self.app, "deck", self.refresh)
_watch(self, self.app, "current_slide_idx", self.refresh)
_watch(self, self.app, "message", self.refresh)
@property
def current_slide(self) -> Slide:

@ -0,0 +1,14 @@
tests:
@watch spiel/ tests/ docs/
pytest
types:
@watch spiel/ tests/ docs/
mypy
docs:
@restart
mkdocs serve --strict

@ -1,54 +1,10 @@
import subprocess
import sys
from pathlib import Path
from unittest.mock import MagicMock
import pytest
from pytest_mock import MockFixture
from typer.testing import CliRunner
from spiel.cli import cli
from spiel.constants import DEMO_FILE, PACKAGE_NAME, __version__
def test_help(runner: CliRunner) -> None:
result = runner.invoke(cli, ["--help"])
assert result.exit_code == 0
def test_help_via_main() -> None:
result = subprocess.run([sys.executable, "-m", PACKAGE_NAME, "--help"])
print(result.stdout)
assert result.returncode == 0
def test_version(runner: CliRunner) -> None:
result = runner.invoke(cli, ["version"])
assert result.exit_code == 0
assert __version__ in result.stdout
def test_plain_version(runner: CliRunner) -> None:
result = runner.invoke(cli, ["version", "--plain"])
assert result.exit_code == 0
assert __version__ in result.stdout
def test_present_deck_on_missing_file(runner: CliRunner, tmp_path: Path) -> None:
result = runner.invoke(cli, ["present", str(tmp_path / "missing.py")])
assert result.exit_code == 2
@pytest.mark.parametrize("stdin", [""])
def test_display_demo_deck(runner: CliRunner, stdin: str) -> None:
result = runner.invoke(cli, ["present", str(DEMO_FILE)], input=stdin)
assert result.exit_code == 0
def test_demo_display(runner: CliRunner) -> None:

@ -0,0 +1,20 @@
import subprocess
import sys
from typer.testing import CliRunner
from spiel.cli import cli
from spiel.constants import PACKAGE_NAME
def test_help(runner: CliRunner) -> None:
result = runner.invoke(cli, ["--help"])
assert result.exit_code == 0
def test_help_via_main() -> None:
result = subprocess.run([sys.executable, "-m", PACKAGE_NAME, "--help"])
print(result.stdout)
assert result.returncode == 0

@ -0,0 +1,20 @@
from pathlib import Path
import pytest
from typer.testing import CliRunner
from spiel.cli import cli
from spiel.constants import DEMO_FILE
def test_present_on_missing_file(runner: CliRunner, tmp_path: Path) -> None:
result = runner.invoke(cli, ["present", str(tmp_path / "missing.py")])
assert result.exit_code == 2
@pytest.mark.parametrize("stdin", [""])
def test_display_demo(runner: CliRunner, stdin: str) -> None:
result = runner.invoke(cli, ["present", str(DEMO_FILE)], input=stdin)
assert result.exit_code == 0

@ -0,0 +1,18 @@
from typer.testing import CliRunner
from spiel import __version__
from spiel.cli import cli
def test_version(runner: CliRunner) -> None:
result = runner.invoke(cli, ["version"])
assert result.exit_code == 0
assert __version__ in result.stdout
def test_plain_version(runner: CliRunner) -> None:
result = runner.invoke(cli, ["version", "--plain"])
assert result.exit_code == 0
assert __version__ in result.stdout

@ -22,7 +22,7 @@ def runner() -> CliRunner:
@pytest.fixture
def three_slide_deck() -> Deck:
deck = Deck(name="three-slides")
deck = Deck(name="three-slides", default_transition=None)
deck.add_slides(Slide(), Slide(), Slide())
return deck

@ -1,45 +1,16 @@
from collections.abc import Iterable
from itertools import combinations_with_replacement
import pytest
from hypothesis import given
from hypothesis.strategies import lists, sampled_from
import spiel.constants
from spiel.app import SpielApp
from spiel.constants import DEMO_FILE
@pytest.fixture
def app() -> SpielApp:
return SpielApp(deck_path=spiel.constants.DEMO_FILE)
KEYS = [
"right",
"left",
"d",
"t",
"enter",
"up",
"down",
"escape",
"question_mark",
]
@pytest.mark.slow
@given(keys=lists(elements=sampled_from(KEYS), max_size=100))
async def test_hammer_on_the_keyboard_long_random(keys: Iterable[str]) -> None:
app = SpielApp(deck_path=spiel.constants.DEMO_FILE)
async with app.run_test() as pilot:
await pilot.press(*keys)
@pytest.mark.slow
@pytest.mark.parametrize("keys", combinations_with_replacement(KEYS, 3))
async def test_hammer_on_the_keyboard_short_exhaustive(app: SpielApp, keys: Iterable[str]) -> None:
async with app.run_test() as pilot:
await pilot.press(*keys)
return SpielApp(
deck_path=DEMO_FILE,
_enable_transitions=False,
_slide_refresh_rate=1 / 10,
)
async def test_advance_through_demo_slides(app: SpielApp) -> None:

@ -1,34 +0,0 @@
from pathlib import Path
import pytest
from typer.testing import CliRunner
from spiel.app import load_deck
from spiel.cli import cli
def test_init_cli_command_fails_if_file_exists(runner: CliRunner, tmp_path: Path) -> None:
target = tmp_path / "foo_bar.py"
target.touch()
result = runner.invoke(cli, ["init", str(target)])
assert result.exit_code == 1
@pytest.fixture
def init_file(runner: CliRunner, tmp_path: Path) -> Path:
target = tmp_path / "foo_bar.py"
runner.invoke(cli, ["init", str(target)])
return target
def test_title_slide_header_injection(init_file: Path) -> None:
assert "# Foo Bar" in init_file.read_text()
def test_can_load_init_file(init_file: Path) -> None:
deck = load_deck(init_file)
assert deck.name == "Foo Bar"

@ -0,0 +1,12 @@
import pytest
from textual.widget import Widget
@pytest.fixture
def from_widget() -> Widget:
return Widget()
@pytest.fixture
def to_widget() -> Widget:
return Widget()

@ -0,0 +1,121 @@
import pytest
from hypothesis import HealthCheck, given, settings
from hypothesis.strategies import floats
from textual.css.scalar import Scalar, ScalarOffset, Unit
from textual.widget import Widget
from spiel import Direction, Swipe, Transition
@pytest.fixture
def transition() -> Swipe:
return Swipe()
Y = Scalar.parse("0", percent_unit=Unit.HEIGHT)
@pytest.mark.parametrize(
"direction, to_offset",
[
(Direction.Next, ScalarOffset(Scalar.parse("100%"), Y)),
],
)
def test_swipe_initialize(
from_widget: Widget,
to_widget: Widget,
direction: Direction,
to_offset: tuple[object, object],
) -> None:
Swipe().initialize(from_widget=from_widget, to_widget=to_widget, direction=direction)
assert to_widget.styles.offset == to_offset
@pytest.mark.parametrize(
"progress, direction, from_offset, to_offset",
[
(
0,
Direction.Next,
ScalarOffset(Scalar.parse("-0%"), Y),
ScalarOffset(Scalar.parse("100%"), Y),
),
(
25,
Direction.Next,
ScalarOffset(Scalar.parse("-25%"), Y),
ScalarOffset(Scalar.parse("75%"), Y),
),
(
50,
Direction.Next,
ScalarOffset(Scalar.parse("-50%"), Y),
ScalarOffset(Scalar.parse("50%"), Y),
),
(
75,
Direction.Next,
ScalarOffset(Scalar.parse("-75%"), Y),
ScalarOffset(Scalar.parse("25%"), Y),
),
(
75.123,
Direction.Next,
ScalarOffset(Scalar.parse("-75.12%"), Y),
ScalarOffset(Scalar.parse("24.88%"), Y),
),
(
75.126,
Direction.Next,
ScalarOffset(Scalar.parse("-75.13%"), Y),
ScalarOffset(Scalar.parse("24.87%"), Y),
),
(
100,
Direction.Next,
ScalarOffset(Scalar.parse("-100%"), Y),
ScalarOffset(Scalar.parse("0%"), Y),
),
],
)
def test_swipe_progress(
transition: Transition,
from_widget: Widget,
to_widget: Widget,
progress: float,
direction: Direction,
from_offset: tuple[object, object],
to_offset: tuple[object, object],
) -> None:
transition.initialize(from_widget=from_widget, to_widget=to_widget, direction=direction)
transition.progress(
from_widget=from_widget,
to_widget=to_widget,
direction=direction,
progress=progress,
)
assert from_widget.styles.offset == from_offset
assert to_widget.styles.offset == to_offset
@given(progress=floats(min_value=0, max_value=100))
@settings(suppress_health_check=[HealthCheck.function_scoped_fixture])
def test_swipe_progress_always_balances_for_right(
transition: Transition,
from_widget: Widget,
to_widget: Widget,
progress: float,
) -> None:
transition.initialize(from_widget=from_widget, to_widget=to_widget, direction=Direction.Next)
transition.progress(
from_widget=from_widget,
to_widget=to_widget,
direction=Direction.Next,
progress=progress,
)
assert abs(from_widget.styles.offset.x.value) + to_widget.styles.offset.x.value == 100
Loading…
Cancel
Save