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.
Files changed (38) hide show
  1. {prezo-2026.1.3 → prezo-2026.2.1}/PKG-INFO +6 -1
  2. {prezo-2026.1.3 → prezo-2026.2.1}/README.md +5 -0
  3. {prezo-2026.1.3 → prezo-2026.2.1}/pyproject.toml +1 -1
  4. {prezo-2026.1.3 → prezo-2026.2.1}/src/prezo/__init__.py +35 -1
  5. {prezo-2026.1.3 → prezo-2026.2.1}/src/prezo/app.py +303 -16
  6. {prezo-2026.1.3 → prezo-2026.2.1}/src/prezo/config.py +2 -0
  7. {prezo-2026.1.3 → prezo-2026.2.1}/src/prezo/export/svg.py +74 -71
  8. {prezo-2026.1.3 → prezo-2026.2.1}/src/prezo/layout.py +96 -5
  9. {prezo-2026.1.3 → prezo-2026.2.1}/src/prezo/parser.py +37 -0
  10. {prezo-2026.1.3 → prezo-2026.2.1}/src/prezo/screens/help.py +4 -0
  11. {prezo-2026.1.3 → prezo-2026.2.1}/src/prezo/widgets/status_bar.py +126 -12
  12. {prezo-2026.1.3 → prezo-2026.2.1}/src/prezo/export/__init__.py +0 -0
  13. {prezo-2026.1.3 → prezo-2026.2.1}/src/prezo/export/common.py +0 -0
  14. {prezo-2026.1.3 → prezo-2026.2.1}/src/prezo/export/html.py +0 -0
  15. {prezo-2026.1.3 → prezo-2026.2.1}/src/prezo/export/images.py +0 -0
  16. {prezo-2026.1.3 → prezo-2026.2.1}/src/prezo/export/pdf.py +0 -0
  17. {prezo-2026.1.3 → prezo-2026.2.1}/src/prezo/images/__init__.py +0 -0
  18. {prezo-2026.1.3 → prezo-2026.2.1}/src/prezo/images/ascii.py +0 -0
  19. {prezo-2026.1.3 → prezo-2026.2.1}/src/prezo/images/base.py +0 -0
  20. {prezo-2026.1.3 → prezo-2026.2.1}/src/prezo/images/chafa.py +0 -0
  21. {prezo-2026.1.3 → prezo-2026.2.1}/src/prezo/images/iterm.py +0 -0
  22. {prezo-2026.1.3 → prezo-2026.2.1}/src/prezo/images/kitty.py +0 -0
  23. {prezo-2026.1.3 → prezo-2026.2.1}/src/prezo/images/overlay.py +0 -0
  24. {prezo-2026.1.3 → prezo-2026.2.1}/src/prezo/images/processor.py +0 -0
  25. {prezo-2026.1.3 → prezo-2026.2.1}/src/prezo/images/sixel.py +0 -0
  26. {prezo-2026.1.3 → prezo-2026.2.1}/src/prezo/screens/__init__.py +0 -0
  27. {prezo-2026.1.3 → prezo-2026.2.1}/src/prezo/screens/base.py +0 -0
  28. {prezo-2026.1.3 → prezo-2026.2.1}/src/prezo/screens/blackout.py +0 -0
  29. {prezo-2026.1.3 → prezo-2026.2.1}/src/prezo/screens/goto.py +0 -0
  30. {prezo-2026.1.3 → prezo-2026.2.1}/src/prezo/screens/overview.py +0 -0
  31. {prezo-2026.1.3 → prezo-2026.2.1}/src/prezo/screens/search.py +0 -0
  32. {prezo-2026.1.3 → prezo-2026.2.1}/src/prezo/screens/toc.py +0 -0
  33. {prezo-2026.1.3 → prezo-2026.2.1}/src/prezo/terminal.py +0 -0
  34. {prezo-2026.1.3 → prezo-2026.2.1}/src/prezo/themes.py +0 -0
  35. {prezo-2026.1.3 → prezo-2026.2.1}/src/prezo/widgets/__init__.py +0 -0
  36. {prezo-2026.1.3 → prezo-2026.2.1}/src/prezo/widgets/image_display.py +0 -0
  37. {prezo-2026.1.3 → prezo-2026.2.1}/src/prezo/widgets/slide_button.py +0 -0
  38. {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
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 |
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "prezo"
3
- version = "2026.1.3"
3
+ version = "2026.2.1"
4
4
  description = "A TUI-based presentation tool for the terminal, built with Textual."
5
5
  readme = "README.md"
6
6
  authors = [
@@ -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(file_path, watch=not args.no_watch, config=config)
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
- self.current_slide = max(0, self.presentation.total_slides - 1)
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
- self.current_slide = last_pos
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 = 0
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
- self.query_one("#slide-content", SlideContent).set_content(
587
- slide.content.strip()
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
- self.presentation
642
- and self.current_slide < self.presentation.total_slides - 1
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(presentation_path, watch=watch, config=config)
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