prezo 2026.1.3__tar.gz → 2026.2.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {prezo-2026.1.3 → prezo-2026.2.1}/PKG-INFO +6 -1
- {prezo-2026.1.3 → prezo-2026.2.1}/README.md +5 -0
- {prezo-2026.1.3 → prezo-2026.2.1}/pyproject.toml +1 -1
- {prezo-2026.1.3 → prezo-2026.2.1}/src/prezo/__init__.py +35 -1
- {prezo-2026.1.3 → prezo-2026.2.1}/src/prezo/app.py +303 -16
- {prezo-2026.1.3 → prezo-2026.2.1}/src/prezo/config.py +2 -0
- {prezo-2026.1.3 → prezo-2026.2.1}/src/prezo/export/svg.py +74 -71
- {prezo-2026.1.3 → prezo-2026.2.1}/src/prezo/layout.py +96 -5
- {prezo-2026.1.3 → prezo-2026.2.1}/src/prezo/parser.py +37 -0
- {prezo-2026.1.3 → prezo-2026.2.1}/src/prezo/screens/help.py +4 -0
- {prezo-2026.1.3 → prezo-2026.2.1}/src/prezo/widgets/status_bar.py +126 -12
- {prezo-2026.1.3 → prezo-2026.2.1}/src/prezo/export/__init__.py +0 -0
- {prezo-2026.1.3 → prezo-2026.2.1}/src/prezo/export/common.py +0 -0
- {prezo-2026.1.3 → prezo-2026.2.1}/src/prezo/export/html.py +0 -0
- {prezo-2026.1.3 → prezo-2026.2.1}/src/prezo/export/images.py +0 -0
- {prezo-2026.1.3 → prezo-2026.2.1}/src/prezo/export/pdf.py +0 -0
- {prezo-2026.1.3 → prezo-2026.2.1}/src/prezo/images/__init__.py +0 -0
- {prezo-2026.1.3 → prezo-2026.2.1}/src/prezo/images/ascii.py +0 -0
- {prezo-2026.1.3 → prezo-2026.2.1}/src/prezo/images/base.py +0 -0
- {prezo-2026.1.3 → prezo-2026.2.1}/src/prezo/images/chafa.py +0 -0
- {prezo-2026.1.3 → prezo-2026.2.1}/src/prezo/images/iterm.py +0 -0
- {prezo-2026.1.3 → prezo-2026.2.1}/src/prezo/images/kitty.py +0 -0
- {prezo-2026.1.3 → prezo-2026.2.1}/src/prezo/images/overlay.py +0 -0
- {prezo-2026.1.3 → prezo-2026.2.1}/src/prezo/images/processor.py +0 -0
- {prezo-2026.1.3 → prezo-2026.2.1}/src/prezo/images/sixel.py +0 -0
- {prezo-2026.1.3 → prezo-2026.2.1}/src/prezo/screens/__init__.py +0 -0
- {prezo-2026.1.3 → prezo-2026.2.1}/src/prezo/screens/base.py +0 -0
- {prezo-2026.1.3 → prezo-2026.2.1}/src/prezo/screens/blackout.py +0 -0
- {prezo-2026.1.3 → prezo-2026.2.1}/src/prezo/screens/goto.py +0 -0
- {prezo-2026.1.3 → prezo-2026.2.1}/src/prezo/screens/overview.py +0 -0
- {prezo-2026.1.3 → prezo-2026.2.1}/src/prezo/screens/search.py +0 -0
- {prezo-2026.1.3 → prezo-2026.2.1}/src/prezo/screens/toc.py +0 -0
- {prezo-2026.1.3 → prezo-2026.2.1}/src/prezo/terminal.py +0 -0
- {prezo-2026.1.3 → prezo-2026.2.1}/src/prezo/themes.py +0 -0
- {prezo-2026.1.3 → prezo-2026.2.1}/src/prezo/widgets/__init__.py +0 -0
- {prezo-2026.1.3 → prezo-2026.2.1}/src/prezo/widgets/image_display.py +0 -0
- {prezo-2026.1.3 → prezo-2026.2.1}/src/prezo/widgets/slide_button.py +0 -0
- {prezo-2026.1.3 → prezo-2026.2.1}/src/prezo/widgets/slide_content.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: prezo
|
|
3
|
-
Version: 2026.1
|
|
3
|
+
Version: 2026.2.1
|
|
4
4
|
Summary: A TUI-based presentation tool for the terminal, built with Textual.
|
|
5
5
|
Author: Stefane Fermigier
|
|
6
6
|
Author-email: Stefane Fermigier <sf@fermigier.com>
|
|
@@ -20,6 +20,7 @@ Display presentations written in Markdown using conventions similar to those of
|
|
|
20
20
|
|
|
21
21
|
- **Markdown presentations** - MARP/Deckset format with `---` slide separators
|
|
22
22
|
- **Column layouts** - Pandoc-style fenced divs for multi-column slides (`::: columns`)
|
|
23
|
+
- **Incremental lists** - Reveal list items one at a time (`-I` flag, like Pandoc)
|
|
23
24
|
- **Live reload** - Auto-refresh when file changes (1s polling)
|
|
24
25
|
- **Keyboard navigation** - Vim-style keys, arrow keys, and more
|
|
25
26
|
- **Slide overview** - Grid view for quick navigation (`o`)
|
|
@@ -71,6 +72,9 @@ prezo -c myconfig.toml presentation.md
|
|
|
71
72
|
# Set image rendering mode
|
|
72
73
|
prezo --image-mode ascii presentation.md # Options: auto, kitty, sixel, iterm, ascii, none
|
|
73
74
|
|
|
75
|
+
# Incremental list reveal (reveal items one at a time)
|
|
76
|
+
prezo -I presentation.md
|
|
77
|
+
|
|
74
78
|
# Export to PDF
|
|
75
79
|
prezo -e pdf presentation.md
|
|
76
80
|
|
|
@@ -92,6 +96,7 @@ prezo -e pdf presentation.md --theme light --size 100x30 --no-chrome
|
|
|
92
96
|
| `t` | Table of contents |
|
|
93
97
|
| `p` | Toggle notes panel |
|
|
94
98
|
| `c` | Cycle clock display |
|
|
99
|
+
| `s` | Start/stop timer |
|
|
95
100
|
| `T` | Cycle theme |
|
|
96
101
|
| `b` | Blackout screen |
|
|
97
102
|
| `w` | Whiteout screen |
|
|
@@ -8,6 +8,7 @@ Display presentations written in Markdown using conventions similar to those of
|
|
|
8
8
|
|
|
9
9
|
- **Markdown presentations** - MARP/Deckset format with `---` slide separators
|
|
10
10
|
- **Column layouts** - Pandoc-style fenced divs for multi-column slides (`::: columns`)
|
|
11
|
+
- **Incremental lists** - Reveal list items one at a time (`-I` flag, like Pandoc)
|
|
11
12
|
- **Live reload** - Auto-refresh when file changes (1s polling)
|
|
12
13
|
- **Keyboard navigation** - Vim-style keys, arrow keys, and more
|
|
13
14
|
- **Slide overview** - Grid view for quick navigation (`o`)
|
|
@@ -59,6 +60,9 @@ prezo -c myconfig.toml presentation.md
|
|
|
59
60
|
# Set image rendering mode
|
|
60
61
|
prezo --image-mode ascii presentation.md # Options: auto, kitty, sixel, iterm, ascii, none
|
|
61
62
|
|
|
63
|
+
# Incremental list reveal (reveal items one at a time)
|
|
64
|
+
prezo -I presentation.md
|
|
65
|
+
|
|
62
66
|
# Export to PDF
|
|
63
67
|
prezo -e pdf presentation.md
|
|
64
68
|
|
|
@@ -80,6 +84,7 @@ prezo -e pdf presentation.md --theme light --size 100x30 --no-chrome
|
|
|
80
84
|
| `t` | Table of contents |
|
|
81
85
|
| `p` | Toggle notes panel |
|
|
82
86
|
| `c` | Cycle clock display |
|
|
87
|
+
| `s` | Start/stop timer |
|
|
83
88
|
| `T` | Cycle theme |
|
|
84
89
|
| `b` | Blackout screen |
|
|
85
90
|
| `w` | Whiteout screen |
|
|
@@ -78,6 +78,16 @@ def _validate_file(path: Path, must_exist: bool = True) -> Path:
|
|
|
78
78
|
return resolved
|
|
79
79
|
|
|
80
80
|
|
|
81
|
+
def _get_version() -> str:
|
|
82
|
+
"""Get the package version from metadata."""
|
|
83
|
+
from importlib.metadata import version # noqa: PLC0415
|
|
84
|
+
|
|
85
|
+
try:
|
|
86
|
+
return version("prezo")
|
|
87
|
+
except Exception:
|
|
88
|
+
return "unknown"
|
|
89
|
+
|
|
90
|
+
|
|
81
91
|
def main() -> None:
|
|
82
92
|
"""Entry point for Prezo."""
|
|
83
93
|
parser = argparse.ArgumentParser(
|
|
@@ -85,6 +95,12 @@ def main() -> None:
|
|
|
85
95
|
description="TUI-based presentation tool for Markdown slides",
|
|
86
96
|
epilog="For more information, visit: https://github.com/abilian/prezo",
|
|
87
97
|
)
|
|
98
|
+
parser.add_argument(
|
|
99
|
+
"-v",
|
|
100
|
+
"--version",
|
|
101
|
+
action="version",
|
|
102
|
+
version=f"%(prog)s {_get_version()}",
|
|
103
|
+
)
|
|
88
104
|
parser.add_argument(
|
|
89
105
|
"file",
|
|
90
106
|
nargs="?",
|
|
@@ -159,6 +175,18 @@ def main() -> None:
|
|
|
159
175
|
choices=["auto", "kitty", "sixel", "iterm", "ascii", "none"],
|
|
160
176
|
help="Image rendering mode (auto, kitty, sixel, iterm, ascii, none)",
|
|
161
177
|
)
|
|
178
|
+
parser.add_argument(
|
|
179
|
+
"-I",
|
|
180
|
+
"--incremental",
|
|
181
|
+
action="store_true",
|
|
182
|
+
help="Display lists incrementally, one item at a time (like Pandoc)",
|
|
183
|
+
)
|
|
184
|
+
parser.add_argument(
|
|
185
|
+
"--time-budget",
|
|
186
|
+
metavar="MINUTES",
|
|
187
|
+
type=int,
|
|
188
|
+
help="Time budget in minutes for pacing indicator (shows if ahead/behind)",
|
|
189
|
+
)
|
|
162
190
|
|
|
163
191
|
args = parser.parse_args()
|
|
164
192
|
|
|
@@ -236,4 +264,10 @@ def main() -> None:
|
|
|
236
264
|
if args.file:
|
|
237
265
|
file_path = _validate_file(Path(args.file))
|
|
238
266
|
|
|
239
|
-
run_app(
|
|
267
|
+
run_app(
|
|
268
|
+
file_path,
|
|
269
|
+
watch=not args.no_watch,
|
|
270
|
+
config=config,
|
|
271
|
+
incremental=args.incremental,
|
|
272
|
+
time_budget=args.time_budget,
|
|
273
|
+
)
|
|
@@ -5,6 +5,7 @@ from __future__ import annotations
|
|
|
5
5
|
import base64
|
|
6
6
|
import contextlib
|
|
7
7
|
import os
|
|
8
|
+
import re
|
|
8
9
|
import subprocess
|
|
9
10
|
import sys
|
|
10
11
|
import tempfile
|
|
@@ -66,6 +67,7 @@ prezo <presentation.md>
|
|
|
66
67
|
| **t** | Table of contents |
|
|
67
68
|
| **p** | Toggle notes |
|
|
68
69
|
| **c** | Toggle clock |
|
|
70
|
+
| **s** | Start/stop timer |
|
|
69
71
|
| **b** | Blackout screen |
|
|
70
72
|
| **e** | Edit current slide |
|
|
71
73
|
| **r** | Reload file |
|
|
@@ -109,6 +111,121 @@ def _format_recent_files(recent_files: list[str], max_files: int = 5) -> str:
|
|
|
109
111
|
return "\n".join(lines)
|
|
110
112
|
|
|
111
113
|
|
|
114
|
+
# -----------------------------------------------------------------------------
|
|
115
|
+
# Incremental List Helpers
|
|
116
|
+
# -----------------------------------------------------------------------------
|
|
117
|
+
|
|
118
|
+
# Pattern matching markdown list items (unordered and ordered)
|
|
119
|
+
_LIST_ITEM_PATTERN = re.compile(r"^(\s*)([-*+]|\d+\.)\s+")
|
|
120
|
+
|
|
121
|
+
# Pattern matching layout directive markers (:::)
|
|
122
|
+
_LAYOUT_MARKER_PATTERN = re.compile(r"^\s*:::")
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def count_list_items(content: str) -> int:
|
|
126
|
+
"""Count the number of top-level list items in markdown content.
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
content: Markdown content to analyze.
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
Number of top-level list items found.
|
|
133
|
+
|
|
134
|
+
"""
|
|
135
|
+
count = 0
|
|
136
|
+
for line in content.split("\n"):
|
|
137
|
+
match = _LIST_ITEM_PATTERN.match(line)
|
|
138
|
+
if match:
|
|
139
|
+
# Only count top-level items (no leading whitespace)
|
|
140
|
+
indent = match.group(1)
|
|
141
|
+
if not indent:
|
|
142
|
+
count += 1
|
|
143
|
+
return count
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
# Braille Pattern Blank - invisible character with width, behaves like text for layout
|
|
147
|
+
_INVISIBLE_CHAR = "\u2800"
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _make_placeholder(text: str) -> str:
|
|
151
|
+
"""Create an invisible placeholder that matches the visual width of text.
|
|
152
|
+
|
|
153
|
+
Uses Braille Pattern Blank characters which are invisible but have
|
|
154
|
+
width and wrap like normal text.
|
|
155
|
+
"""
|
|
156
|
+
return _INVISIBLE_CHAR * len(text) if text else _INVISIBLE_CHAR
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def filter_list_items(content: str, max_items: int) -> str:
|
|
160
|
+
"""Filter content to show only the first N list items.
|
|
161
|
+
|
|
162
|
+
Preserves layout directive markers (:::) and other structural elements.
|
|
163
|
+
Hidden items are replaced with placeholder text of the same length
|
|
164
|
+
to maintain visual height when text wraps.
|
|
165
|
+
|
|
166
|
+
Args:
|
|
167
|
+
content: Markdown content to filter.
|
|
168
|
+
max_items: Maximum number of top-level list items to show.
|
|
169
|
+
|
|
170
|
+
Returns:
|
|
171
|
+
Filtered content with only the first N list items visible.
|
|
172
|
+
|
|
173
|
+
"""
|
|
174
|
+
if max_items < 0:
|
|
175
|
+
return content # Show all
|
|
176
|
+
|
|
177
|
+
lines = content.split("\n")
|
|
178
|
+
result_lines = []
|
|
179
|
+
item_count = 0
|
|
180
|
+
in_hidden_item = False
|
|
181
|
+
|
|
182
|
+
for line in lines:
|
|
183
|
+
# Always preserve layout markers (:::)
|
|
184
|
+
if _LAYOUT_MARKER_PATTERN.match(line):
|
|
185
|
+
result_lines.append(line)
|
|
186
|
+
# Reset hidden state when entering/exiting a block
|
|
187
|
+
in_hidden_item = False
|
|
188
|
+
continue
|
|
189
|
+
|
|
190
|
+
match = _LIST_ITEM_PATTERN.match(line)
|
|
191
|
+
|
|
192
|
+
if match:
|
|
193
|
+
indent = match.group(1) # Leading whitespace
|
|
194
|
+
marker = match.group(2) # List marker (-, *, +, 1.)
|
|
195
|
+
text_start = match.end()
|
|
196
|
+
text = line[text_start:] # The actual text content
|
|
197
|
+
|
|
198
|
+
if len(indent) == 0:
|
|
199
|
+
# Top-level item
|
|
200
|
+
item_count += 1
|
|
201
|
+
if item_count <= max_items:
|
|
202
|
+
result_lines.append(line)
|
|
203
|
+
in_hidden_item = False
|
|
204
|
+
else:
|
|
205
|
+
# Replace with same-length placeholder
|
|
206
|
+
placeholder = _make_placeholder(text)
|
|
207
|
+
result_lines.append(f"{indent}{marker} {placeholder}")
|
|
208
|
+
in_hidden_item = True
|
|
209
|
+
elif in_hidden_item:
|
|
210
|
+
# Nested item under hidden parent - also hide
|
|
211
|
+
placeholder = _make_placeholder(text)
|
|
212
|
+
result_lines.append(f"{indent}{marker} {placeholder}")
|
|
213
|
+
else:
|
|
214
|
+
# Nested item - show if parent is visible
|
|
215
|
+
result_lines.append(line)
|
|
216
|
+
elif in_hidden_item:
|
|
217
|
+
# Content continuation of hidden item - preserve length
|
|
218
|
+
stripped = line.lstrip()
|
|
219
|
+
leading = line[: len(line) - len(stripped)]
|
|
220
|
+
placeholder = _make_placeholder(stripped)
|
|
221
|
+
result_lines.append(f"{leading}{placeholder}")
|
|
222
|
+
else:
|
|
223
|
+
# Non-list line (could be continuation or other content)
|
|
224
|
+
result_lines.append(line)
|
|
225
|
+
|
|
226
|
+
return "\n".join(result_lines)
|
|
227
|
+
|
|
228
|
+
|
|
112
229
|
class PrezoCommands(Provider):
|
|
113
230
|
"""Command provider for Prezo actions."""
|
|
114
231
|
|
|
@@ -142,6 +259,7 @@ class PrezoCommands(Provider):
|
|
|
142
259
|
("Search Slides", "search", "Search slides by content (/)"),
|
|
143
260
|
("Toggle Notes", "toggle_notes", "Show/hide presenter notes (p)"),
|
|
144
261
|
("Toggle Clock", "toggle_clock", "Cycle clock display mode (c)"),
|
|
262
|
+
("Start/Stop Timer", "toggle_timer", "Start or stop elapsed timer (S)"),
|
|
145
263
|
("Help", "show_help", "Show keyboard shortcuts (?)"),
|
|
146
264
|
]
|
|
147
265
|
)
|
|
@@ -330,6 +448,8 @@ class PrezoApp(App):
|
|
|
330
448
|
Binding("t", "show_toc", "TOC", show=True),
|
|
331
449
|
Binding("p", "toggle_notes", "Notes", show=True),
|
|
332
450
|
Binding("c", "toggle_clock", "Clock", show=False),
|
|
451
|
+
Binding("s", "toggle_timer", "Timer", show=False),
|
|
452
|
+
Binding("S", "toggle_timer", "Timer", show=False),
|
|
333
453
|
Binding("T", "cycle_theme", "Theme", show=False),
|
|
334
454
|
Binding("b", "blackout", "Blackout", show=False),
|
|
335
455
|
Binding("w", "whiteout", "Whiteout", show=False),
|
|
@@ -342,6 +462,7 @@ class PrezoApp(App):
|
|
|
342
462
|
current_slide: reactive[int] = reactive(0)
|
|
343
463
|
notes_visible: reactive[bool] = reactive(False)
|
|
344
464
|
app_theme: reactive[str] = reactive("dark")
|
|
465
|
+
reveal_index: reactive[int] = reactive(-1) # -1 = show all, 0+ = show up to index
|
|
345
466
|
|
|
346
467
|
TITLE = "Prezo"
|
|
347
468
|
|
|
@@ -351,6 +472,8 @@ class PrezoApp(App):
|
|
|
351
472
|
*,
|
|
352
473
|
watch: bool | None = None,
|
|
353
474
|
config: Config | None = None,
|
|
475
|
+
incremental: bool = False,
|
|
476
|
+
time_budget: int | None = None,
|
|
354
477
|
) -> None:
|
|
355
478
|
"""Initialize the Prezo application.
|
|
356
479
|
|
|
@@ -358,6 +481,8 @@ class PrezoApp(App):
|
|
|
358
481
|
presentation_path: Path to the Markdown presentation file.
|
|
359
482
|
watch: Whether to enable file watching for live reload.
|
|
360
483
|
config: Optional config override. Uses global config if None.
|
|
484
|
+
incremental: Whether to display lists incrementally (-I flag).
|
|
485
|
+
time_budget: Time budget in minutes for pacing indicator.
|
|
361
486
|
|
|
362
487
|
"""
|
|
363
488
|
super().__init__()
|
|
@@ -367,6 +492,12 @@ class PrezoApp(App):
|
|
|
367
492
|
self.presentation_path = Path(presentation_path) if presentation_path else None
|
|
368
493
|
self.presentation: Presentation | None = None
|
|
369
494
|
|
|
495
|
+
# Incremental lists: CLI flag overrides config
|
|
496
|
+
self.incremental_cli = incremental
|
|
497
|
+
|
|
498
|
+
# Time budget: CLI flag overrides config/presentation directives
|
|
499
|
+
self.time_budget_cli = time_budget
|
|
500
|
+
|
|
370
501
|
# Use config for watch if not explicitly set
|
|
371
502
|
if watch is None:
|
|
372
503
|
self.watch_enabled = self.config.behavior.auto_reload
|
|
@@ -436,11 +567,20 @@ class PrezoApp(App):
|
|
|
436
567
|
return
|
|
437
568
|
|
|
438
569
|
old_slide = self.current_slide
|
|
570
|
+
old_reveal = self.reveal_index
|
|
439
571
|
self.presentation = parse_presentation(self.presentation_path)
|
|
440
572
|
|
|
441
573
|
if old_slide >= self.presentation.total_slides:
|
|
442
|
-
|
|
574
|
+
target_slide = max(0, self.presentation.total_slides - 1)
|
|
575
|
+
self._init_reveal_for_slide(target_slide, show_all=False)
|
|
576
|
+
self.current_slide = target_slide
|
|
443
577
|
else:
|
|
578
|
+
# Preserve reveal position if still valid
|
|
579
|
+
list_count = self._get_list_count(old_slide)
|
|
580
|
+
if self._is_incremental_enabled(old_slide) and list_count > 0:
|
|
581
|
+
self.reveal_index = max(0, min(old_reveal, list_count - 1))
|
|
582
|
+
else:
|
|
583
|
+
self.reveal_index = -1
|
|
444
584
|
self._update_display()
|
|
445
585
|
|
|
446
586
|
self.notify("Presentation reloaded", timeout=2)
|
|
@@ -453,12 +593,16 @@ class PrezoApp(App):
|
|
|
453
593
|
# Restore last position or start at 0
|
|
454
594
|
abs_path = str(self.presentation_path.absolute())
|
|
455
595
|
last_pos = self.state.get_position(abs_path)
|
|
456
|
-
if last_pos < self.presentation.total_slides
|
|
457
|
-
|
|
596
|
+
target_slide = last_pos if last_pos < self.presentation.total_slides else 0
|
|
597
|
+
|
|
598
|
+
# Set the slide - the watcher will initialize reveal state
|
|
599
|
+
if self.current_slide == target_slide:
|
|
600
|
+
# Watcher won't fire, so initialize manually
|
|
601
|
+
self._init_reveal_for_slide(target_slide, show_all=False)
|
|
602
|
+
self._update_display()
|
|
458
603
|
else:
|
|
459
|
-
self.current_slide =
|
|
604
|
+
self.current_slide = target_slide
|
|
460
605
|
|
|
461
|
-
self._update_display()
|
|
462
606
|
self._update_progress_bar()
|
|
463
607
|
|
|
464
608
|
if self.presentation.title:
|
|
@@ -479,6 +623,81 @@ class PrezoApp(App):
|
|
|
479
623
|
self._apply_timer_config(status_bar)
|
|
480
624
|
status_bar.reset_timer()
|
|
481
625
|
|
|
626
|
+
def _is_incremental_enabled(self, slide_index: int | None = None) -> bool:
|
|
627
|
+
"""Check if incremental mode is enabled for a slide.
|
|
628
|
+
|
|
629
|
+
Priority: per-slide directive > CLI flag > config > presentation directive
|
|
630
|
+
|
|
631
|
+
Args:
|
|
632
|
+
slide_index: Slide index to check. Uses current_slide if None.
|
|
633
|
+
|
|
634
|
+
Returns:
|
|
635
|
+
True if incremental lists should be enabled.
|
|
636
|
+
|
|
637
|
+
"""
|
|
638
|
+
if not self.presentation or not self.presentation.slides:
|
|
639
|
+
return False
|
|
640
|
+
|
|
641
|
+
idx = slide_index if slide_index is not None else self.current_slide
|
|
642
|
+
if idx < 0 or idx >= len(self.presentation.slides):
|
|
643
|
+
return False
|
|
644
|
+
|
|
645
|
+
slide = self.presentation.slides[idx]
|
|
646
|
+
|
|
647
|
+
# Per-slide directive takes highest priority
|
|
648
|
+
if slide.incremental is not None:
|
|
649
|
+
return slide.incremental
|
|
650
|
+
|
|
651
|
+
# CLI flag overrides config and presentation directives
|
|
652
|
+
if self.incremental_cli:
|
|
653
|
+
return True
|
|
654
|
+
|
|
655
|
+
# Presentation directive (from <!-- prezo --> block)
|
|
656
|
+
if self.presentation.directives.incremental_lists is not None:
|
|
657
|
+
return self.presentation.directives.incremental_lists
|
|
658
|
+
|
|
659
|
+
# Fall back to config
|
|
660
|
+
return self.config.behavior.incremental_lists
|
|
661
|
+
|
|
662
|
+
def _get_list_count(self, slide_index: int | None = None) -> int:
|
|
663
|
+
"""Get the number of top-level list items in a slide.
|
|
664
|
+
|
|
665
|
+
Args:
|
|
666
|
+
slide_index: Slide index to check. Uses current_slide if None.
|
|
667
|
+
|
|
668
|
+
Returns:
|
|
669
|
+
Number of top-level list items.
|
|
670
|
+
|
|
671
|
+
"""
|
|
672
|
+
if not self.presentation or not self.presentation.slides:
|
|
673
|
+
return 0
|
|
674
|
+
|
|
675
|
+
idx = slide_index if slide_index is not None else self.current_slide
|
|
676
|
+
if idx < 0 or idx >= len(self.presentation.slides):
|
|
677
|
+
return 0
|
|
678
|
+
|
|
679
|
+
slide = self.presentation.slides[idx]
|
|
680
|
+
return count_list_items(slide.content)
|
|
681
|
+
|
|
682
|
+
def _init_reveal_for_slide(
|
|
683
|
+
self, slide_index: int, *, show_all: bool = False
|
|
684
|
+
) -> None:
|
|
685
|
+
"""Initialize reveal state for a specific slide.
|
|
686
|
+
|
|
687
|
+
Args:
|
|
688
|
+
slide_index: The slide to initialize for.
|
|
689
|
+
show_all: If True, reveal all items. If False, start with first item.
|
|
690
|
+
|
|
691
|
+
"""
|
|
692
|
+
if self._is_incremental_enabled(slide_index):
|
|
693
|
+
list_count = self._get_list_count(slide_index)
|
|
694
|
+
if list_count > 0:
|
|
695
|
+
self.reveal_index = (list_count - 1) if show_all else 0
|
|
696
|
+
else:
|
|
697
|
+
self.reveal_index = -1 # No list items, show all content
|
|
698
|
+
else:
|
|
699
|
+
self.reveal_index = -1 # Incremental disabled, show all
|
|
700
|
+
|
|
482
701
|
def _apply_presentation_directives(self) -> None:
|
|
483
702
|
"""Apply presentation-specific directives on top of config."""
|
|
484
703
|
if not self.presentation:
|
|
@@ -496,6 +715,7 @@ class PrezoApp(App):
|
|
|
496
715
|
show_clock = self.config.timer.show_clock
|
|
497
716
|
show_elapsed = self.config.timer.show_elapsed
|
|
498
717
|
countdown = self.config.timer.countdown_minutes
|
|
718
|
+
time_budget = self.config.timer.time_budget_minutes
|
|
499
719
|
|
|
500
720
|
# Override with presentation directives if specified
|
|
501
721
|
if self.presentation:
|
|
@@ -506,12 +726,19 @@ class PrezoApp(App):
|
|
|
506
726
|
show_elapsed = directives.show_elapsed
|
|
507
727
|
if directives.countdown_minutes is not None:
|
|
508
728
|
countdown = directives.countdown_minutes
|
|
729
|
+
if directives.time_budget_minutes is not None:
|
|
730
|
+
time_budget = directives.time_budget_minutes
|
|
731
|
+
|
|
732
|
+
# CLI flag takes highest priority for time budget
|
|
733
|
+
if self.time_budget_cli is not None:
|
|
734
|
+
time_budget = self.time_budget_cli
|
|
509
735
|
|
|
510
736
|
# Apply to status bar
|
|
511
737
|
status_bar.show_clock = show_clock
|
|
512
738
|
status_bar.show_elapsed = show_elapsed
|
|
513
739
|
status_bar.countdown_minutes = countdown
|
|
514
740
|
status_bar.show_countdown = countdown > 0
|
|
741
|
+
status_bar.time_budget_minutes = time_budget
|
|
515
742
|
|
|
516
743
|
def _show_welcome(self) -> None:
|
|
517
744
|
"""Show welcome message when no presentation is loaded."""
|
|
@@ -583,9 +810,13 @@ class PrezoApp(App):
|
|
|
583
810
|
image_widget.clear()
|
|
584
811
|
|
|
585
812
|
# Use cleaned content (bg images already removed by parser)
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
813
|
+
content = slide.content.strip()
|
|
814
|
+
|
|
815
|
+
# Apply incremental filtering if enabled
|
|
816
|
+
if self._is_incremental_enabled() and self.reveal_index >= 0:
|
|
817
|
+
content = filter_list_items(content, self.reveal_index + 1)
|
|
818
|
+
|
|
819
|
+
self.query_one("#slide-content", SlideContent).set_content(content)
|
|
589
820
|
|
|
590
821
|
container = self.query_one("#slide-container", VerticalScroll)
|
|
591
822
|
container.scroll_home(animate=False)
|
|
@@ -594,7 +825,7 @@ class PrezoApp(App):
|
|
|
594
825
|
self._update_notes()
|
|
595
826
|
|
|
596
827
|
def _update_progress_bar(self) -> None:
|
|
597
|
-
"""Update the progress bar."""
|
|
828
|
+
"""Update the progress bar and reveal indicator."""
|
|
598
829
|
if not self.presentation:
|
|
599
830
|
return
|
|
600
831
|
|
|
@@ -602,6 +833,19 @@ class PrezoApp(App):
|
|
|
602
833
|
status.current = self.current_slide
|
|
603
834
|
status.total = self.presentation.total_slides
|
|
604
835
|
|
|
836
|
+
# Update reveal indicator
|
|
837
|
+
if self._is_incremental_enabled():
|
|
838
|
+
list_count = self._get_list_count()
|
|
839
|
+
if list_count > 0 and self.reveal_index >= 0:
|
|
840
|
+
status.reveal_current = self.reveal_index
|
|
841
|
+
status.reveal_total = list_count
|
|
842
|
+
else:
|
|
843
|
+
status.reveal_current = -1
|
|
844
|
+
status.reveal_total = 0
|
|
845
|
+
else:
|
|
846
|
+
status.reveal_current = -1
|
|
847
|
+
status.reveal_total = 0
|
|
848
|
+
|
|
605
849
|
def _update_notes(self) -> None:
|
|
606
850
|
"""Update the notes panel content."""
|
|
607
851
|
if not self.presentation or not self.presentation.slides:
|
|
@@ -617,6 +861,9 @@ class PrezoApp(App):
|
|
|
617
861
|
|
|
618
862
|
def watch_current_slide(self, old_value: int, new_value: int) -> None:
|
|
619
863
|
"""React to slide changes."""
|
|
864
|
+
# Determine direction and initialize reveal state appropriately
|
|
865
|
+
going_back = new_value < old_value
|
|
866
|
+
self._init_reveal_for_slide(new_value, show_all=going_back)
|
|
620
867
|
self._update_display()
|
|
621
868
|
self._save_position()
|
|
622
869
|
|
|
@@ -636,15 +883,41 @@ class PrezoApp(App):
|
|
|
636
883
|
notes_panel.remove_class("visible")
|
|
637
884
|
|
|
638
885
|
def action_next_slide(self) -> None:
|
|
639
|
-
"""Go to the next slide."""
|
|
640
|
-
if
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
886
|
+
"""Go to the next slide or reveal next list item."""
|
|
887
|
+
if not self.presentation:
|
|
888
|
+
return
|
|
889
|
+
|
|
890
|
+
# Check if we should reveal next item instead of advancing slide
|
|
891
|
+
if self._is_incremental_enabled():
|
|
892
|
+
list_count = self._get_list_count()
|
|
893
|
+
if (
|
|
894
|
+
list_count > 0
|
|
895
|
+
and self.reveal_index >= 0
|
|
896
|
+
and self.reveal_index < list_count - 1
|
|
897
|
+
):
|
|
898
|
+
self.reveal_index += 1
|
|
899
|
+
self._update_display()
|
|
900
|
+
self._update_progress_bar()
|
|
901
|
+
return
|
|
902
|
+
|
|
903
|
+
# No more items to reveal, go to next slide
|
|
904
|
+
if self.current_slide < self.presentation.total_slides - 1:
|
|
905
|
+
# The watcher will initialize reveal_index for the new slide
|
|
644
906
|
self.current_slide += 1
|
|
645
907
|
|
|
646
908
|
def action_prev_slide(self) -> None:
|
|
647
|
-
"""Go to the previous slide."""
|
|
909
|
+
"""Go to the previous slide or hide last revealed item."""
|
|
910
|
+
if not self.presentation:
|
|
911
|
+
return
|
|
912
|
+
|
|
913
|
+
# Check if we should hide last item instead of going back
|
|
914
|
+
if self._is_incremental_enabled() and self.reveal_index > 0:
|
|
915
|
+
self.reveal_index -= 1
|
|
916
|
+
self._update_display()
|
|
917
|
+
self._update_progress_bar()
|
|
918
|
+
return
|
|
919
|
+
|
|
920
|
+
# Go to previous slide (watcher will show all items)
|
|
648
921
|
if self.current_slide > 0:
|
|
649
922
|
self.current_slide -= 1
|
|
650
923
|
|
|
@@ -721,6 +994,10 @@ class PrezoApp(App):
|
|
|
721
994
|
"""Cycle through clock display modes."""
|
|
722
995
|
self.query_one("#status-bar", StatusBar).toggle_clock()
|
|
723
996
|
|
|
997
|
+
def action_toggle_timer(self) -> None:
|
|
998
|
+
"""Start or stop the elapsed timer."""
|
|
999
|
+
self.query_one("#status-bar", StatusBar).toggle_timer()
|
|
1000
|
+
|
|
724
1001
|
def action_cycle_theme(self) -> None:
|
|
725
1002
|
"""Cycle through available themes."""
|
|
726
1003
|
self.app_theme = get_next_theme(self.app_theme)
|
|
@@ -931,6 +1208,8 @@ def run_app(
|
|
|
931
1208
|
*,
|
|
932
1209
|
watch: bool | None = None,
|
|
933
1210
|
config: Config | None = None,
|
|
1211
|
+
incremental: bool = False,
|
|
1212
|
+
time_budget: int | None = None,
|
|
934
1213
|
) -> None:
|
|
935
1214
|
"""Run the Prezo application.
|
|
936
1215
|
|
|
@@ -938,7 +1217,15 @@ def run_app(
|
|
|
938
1217
|
presentation_path: Path to the presentation file.
|
|
939
1218
|
watch: Whether to watch for file changes. Uses config default if None.
|
|
940
1219
|
config: Optional config override. Uses global config if None.
|
|
1220
|
+
incremental: Whether to display lists incrementally (-I flag).
|
|
1221
|
+
time_budget: Time budget in minutes for pacing indicator.
|
|
941
1222
|
|
|
942
1223
|
"""
|
|
943
|
-
app = PrezoApp(
|
|
1224
|
+
app = PrezoApp(
|
|
1225
|
+
presentation_path,
|
|
1226
|
+
watch=watch,
|
|
1227
|
+
config=config,
|
|
1228
|
+
incremental=incremental,
|
|
1229
|
+
time_budget=time_budget,
|
|
1230
|
+
)
|
|
944
1231
|
app.run()
|
|
@@ -61,6 +61,7 @@ class TimerConfig:
|
|
|
61
61
|
show_clock: bool = True
|
|
62
62
|
show_elapsed: bool = True
|
|
63
63
|
countdown_minutes: int = 0
|
|
64
|
+
time_budget_minutes: int = 0 # Total time budget for pacing indicator
|
|
64
65
|
|
|
65
66
|
|
|
66
67
|
@dataclass
|
|
@@ -69,6 +70,7 @@ class BehaviorConfig:
|
|
|
69
70
|
|
|
70
71
|
auto_reload: bool = True
|
|
71
72
|
reload_interval: float = 1.0
|
|
73
|
+
incremental_lists: bool = False # Display lists one item at a time
|
|
72
74
|
|
|
73
75
|
|
|
74
76
|
@dataclass
|