prezo 2026.1.3__py3-none-any.whl → 2026.1.4__py3-none-any.whl

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/__init__.py CHANGED
@@ -159,6 +159,12 @@ def main() -> None:
159
159
  choices=["auto", "kitty", "sixel", "iterm", "ascii", "none"],
160
160
  help="Image rendering mode (auto, kitty, sixel, iterm, ascii, none)",
161
161
  )
162
+ parser.add_argument(
163
+ "-I",
164
+ "--incremental",
165
+ action="store_true",
166
+ help="Display lists incrementally, one item at a time (like Pandoc)",
167
+ )
162
168
 
163
169
  args = parser.parse_args()
164
170
 
@@ -236,4 +242,9 @@ def main() -> None:
236
242
  if args.file:
237
243
  file_path = _validate_file(Path(args.file))
238
244
 
239
- run_app(file_path, watch=not args.no_watch, config=config)
245
+ run_app(
246
+ file_path,
247
+ watch=not args.no_watch,
248
+ config=config,
249
+ incremental=args.incremental,
250
+ )
prezo/app.py CHANGED
@@ -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
@@ -109,6 +110,121 @@ def _format_recent_files(recent_files: list[str], max_files: int = 5) -> str:
109
110
  return "\n".join(lines)
110
111
 
111
112
 
113
+ # -----------------------------------------------------------------------------
114
+ # Incremental List Helpers
115
+ # -----------------------------------------------------------------------------
116
+
117
+ # Pattern matching markdown list items (unordered and ordered)
118
+ _LIST_ITEM_PATTERN = re.compile(r"^(\s*)([-*+]|\d+\.)\s+")
119
+
120
+ # Pattern matching layout directive markers (:::)
121
+ _LAYOUT_MARKER_PATTERN = re.compile(r"^\s*:::")
122
+
123
+
124
+ def count_list_items(content: str) -> int:
125
+ """Count the number of top-level list items in markdown content.
126
+
127
+ Args:
128
+ content: Markdown content to analyze.
129
+
130
+ Returns:
131
+ Number of top-level list items found.
132
+
133
+ """
134
+ count = 0
135
+ for line in content.split("\n"):
136
+ match = _LIST_ITEM_PATTERN.match(line)
137
+ if match:
138
+ # Only count top-level items (no leading whitespace)
139
+ indent = match.group(1)
140
+ if not indent:
141
+ count += 1
142
+ return count
143
+
144
+
145
+ # Braille Pattern Blank - invisible character with width, behaves like text for layout
146
+ _INVISIBLE_CHAR = "\u2800"
147
+
148
+
149
+ def _make_placeholder(text: str) -> str:
150
+ """Create an invisible placeholder that matches the visual width of text.
151
+
152
+ Uses Braille Pattern Blank characters which are invisible but have
153
+ width and wrap like normal text.
154
+ """
155
+ return _INVISIBLE_CHAR * len(text) if text else _INVISIBLE_CHAR
156
+
157
+
158
+ def filter_list_items(content: str, max_items: int) -> str:
159
+ """Filter content to show only the first N list items.
160
+
161
+ Preserves layout directive markers (:::) and other structural elements.
162
+ Hidden items are replaced with placeholder text of the same length
163
+ to maintain visual height when text wraps.
164
+
165
+ Args:
166
+ content: Markdown content to filter.
167
+ max_items: Maximum number of top-level list items to show.
168
+
169
+ Returns:
170
+ Filtered content with only the first N list items visible.
171
+
172
+ """
173
+ if max_items < 0:
174
+ return content # Show all
175
+
176
+ lines = content.split("\n")
177
+ result_lines = []
178
+ item_count = 0
179
+ in_hidden_item = False
180
+
181
+ for line in lines:
182
+ # Always preserve layout markers (:::)
183
+ if _LAYOUT_MARKER_PATTERN.match(line):
184
+ result_lines.append(line)
185
+ # Reset hidden state when entering/exiting a block
186
+ in_hidden_item = False
187
+ continue
188
+
189
+ match = _LIST_ITEM_PATTERN.match(line)
190
+
191
+ if match:
192
+ indent = match.group(1) # Leading whitespace
193
+ marker = match.group(2) # List marker (-, *, +, 1.)
194
+ text_start = match.end()
195
+ text = line[text_start:] # The actual text content
196
+
197
+ if len(indent) == 0:
198
+ # Top-level item
199
+ item_count += 1
200
+ if item_count <= max_items:
201
+ result_lines.append(line)
202
+ in_hidden_item = False
203
+ else:
204
+ # Replace with same-length placeholder
205
+ placeholder = _make_placeholder(text)
206
+ result_lines.append(f"{indent}{marker} {placeholder}")
207
+ in_hidden_item = True
208
+ elif in_hidden_item:
209
+ # Nested item under hidden parent - also hide
210
+ placeholder = _make_placeholder(text)
211
+ result_lines.append(f"{indent}{marker} {placeholder}")
212
+ else:
213
+ # Nested item - show if parent is visible
214
+ result_lines.append(line)
215
+ elif in_hidden_item:
216
+ # Content continuation of hidden item - preserve length
217
+ stripped = line.lstrip()
218
+ leading = line[: len(line) - len(stripped)]
219
+ placeholder = _make_placeholder(stripped)
220
+ result_lines.append(f"{leading}{placeholder}")
221
+ else:
222
+ # Non-list line (could be continuation or other content)
223
+ result_lines.append(line)
224
+
225
+ return "\n".join(result_lines)
226
+
227
+
112
228
  class PrezoCommands(Provider):
113
229
  """Command provider for Prezo actions."""
114
230
 
@@ -342,6 +458,7 @@ class PrezoApp(App):
342
458
  current_slide: reactive[int] = reactive(0)
343
459
  notes_visible: reactive[bool] = reactive(False)
344
460
  app_theme: reactive[str] = reactive("dark")
461
+ reveal_index: reactive[int] = reactive(-1) # -1 = show all, 0+ = show up to index
345
462
 
346
463
  TITLE = "Prezo"
347
464
 
@@ -351,6 +468,7 @@ class PrezoApp(App):
351
468
  *,
352
469
  watch: bool | None = None,
353
470
  config: Config | None = None,
471
+ incremental: bool = False,
354
472
  ) -> None:
355
473
  """Initialize the Prezo application.
356
474
 
@@ -358,6 +476,7 @@ class PrezoApp(App):
358
476
  presentation_path: Path to the Markdown presentation file.
359
477
  watch: Whether to enable file watching for live reload.
360
478
  config: Optional config override. Uses global config if None.
479
+ incremental: Whether to display lists incrementally (-I flag).
361
480
 
362
481
  """
363
482
  super().__init__()
@@ -367,6 +486,9 @@ class PrezoApp(App):
367
486
  self.presentation_path = Path(presentation_path) if presentation_path else None
368
487
  self.presentation: Presentation | None = None
369
488
 
489
+ # Incremental lists: CLI flag overrides config
490
+ self.incremental_cli = incremental
491
+
370
492
  # Use config for watch if not explicitly set
371
493
  if watch is None:
372
494
  self.watch_enabled = self.config.behavior.auto_reload
@@ -436,11 +558,20 @@ class PrezoApp(App):
436
558
  return
437
559
 
438
560
  old_slide = self.current_slide
561
+ old_reveal = self.reveal_index
439
562
  self.presentation = parse_presentation(self.presentation_path)
440
563
 
441
564
  if old_slide >= self.presentation.total_slides:
442
- self.current_slide = max(0, self.presentation.total_slides - 1)
565
+ target_slide = max(0, self.presentation.total_slides - 1)
566
+ self._init_reveal_for_slide(target_slide, show_all=False)
567
+ self.current_slide = target_slide
443
568
  else:
569
+ # Preserve reveal position if still valid
570
+ list_count = self._get_list_count(old_slide)
571
+ if self._is_incremental_enabled(old_slide) and list_count > 0:
572
+ self.reveal_index = max(0, min(old_reveal, list_count - 1))
573
+ else:
574
+ self.reveal_index = -1
444
575
  self._update_display()
445
576
 
446
577
  self.notify("Presentation reloaded", timeout=2)
@@ -453,12 +584,16 @@ class PrezoApp(App):
453
584
  # Restore last position or start at 0
454
585
  abs_path = str(self.presentation_path.absolute())
455
586
  last_pos = self.state.get_position(abs_path)
456
- if last_pos < self.presentation.total_slides:
457
- self.current_slide = last_pos
587
+ target_slide = last_pos if last_pos < self.presentation.total_slides else 0
588
+
589
+ # Set the slide - the watcher will initialize reveal state
590
+ if self.current_slide == target_slide:
591
+ # Watcher won't fire, so initialize manually
592
+ self._init_reveal_for_slide(target_slide, show_all=False)
593
+ self._update_display()
458
594
  else:
459
- self.current_slide = 0
595
+ self.current_slide = target_slide
460
596
 
461
- self._update_display()
462
597
  self._update_progress_bar()
463
598
 
464
599
  if self.presentation.title:
@@ -479,6 +614,81 @@ class PrezoApp(App):
479
614
  self._apply_timer_config(status_bar)
480
615
  status_bar.reset_timer()
481
616
 
617
+ def _is_incremental_enabled(self, slide_index: int | None = None) -> bool:
618
+ """Check if incremental mode is enabled for a slide.
619
+
620
+ Priority: per-slide directive > CLI flag > config > presentation directive
621
+
622
+ Args:
623
+ slide_index: Slide index to check. Uses current_slide if None.
624
+
625
+ Returns:
626
+ True if incremental lists should be enabled.
627
+
628
+ """
629
+ if not self.presentation or not self.presentation.slides:
630
+ return False
631
+
632
+ idx = slide_index if slide_index is not None else self.current_slide
633
+ if idx < 0 or idx >= len(self.presentation.slides):
634
+ return False
635
+
636
+ slide = self.presentation.slides[idx]
637
+
638
+ # Per-slide directive takes highest priority
639
+ if slide.incremental is not None:
640
+ return slide.incremental
641
+
642
+ # CLI flag overrides config and presentation directives
643
+ if self.incremental_cli:
644
+ return True
645
+
646
+ # Presentation directive (from <!-- prezo --> block)
647
+ if self.presentation.directives.incremental_lists is not None:
648
+ return self.presentation.directives.incremental_lists
649
+
650
+ # Fall back to config
651
+ return self.config.behavior.incremental_lists
652
+
653
+ def _get_list_count(self, slide_index: int | None = None) -> int:
654
+ """Get the number of top-level list items in a slide.
655
+
656
+ Args:
657
+ slide_index: Slide index to check. Uses current_slide if None.
658
+
659
+ Returns:
660
+ Number of top-level list items.
661
+
662
+ """
663
+ if not self.presentation or not self.presentation.slides:
664
+ return 0
665
+
666
+ idx = slide_index if slide_index is not None else self.current_slide
667
+ if idx < 0 or idx >= len(self.presentation.slides):
668
+ return 0
669
+
670
+ slide = self.presentation.slides[idx]
671
+ return count_list_items(slide.content)
672
+
673
+ def _init_reveal_for_slide(
674
+ self, slide_index: int, *, show_all: bool = False
675
+ ) -> None:
676
+ """Initialize reveal state for a specific slide.
677
+
678
+ Args:
679
+ slide_index: The slide to initialize for.
680
+ show_all: If True, reveal all items. If False, start with first item.
681
+
682
+ """
683
+ if self._is_incremental_enabled(slide_index):
684
+ list_count = self._get_list_count(slide_index)
685
+ if list_count > 0:
686
+ self.reveal_index = (list_count - 1) if show_all else 0
687
+ else:
688
+ self.reveal_index = -1 # No list items, show all content
689
+ else:
690
+ self.reveal_index = -1 # Incremental disabled, show all
691
+
482
692
  def _apply_presentation_directives(self) -> None:
483
693
  """Apply presentation-specific directives on top of config."""
484
694
  if not self.presentation:
@@ -583,9 +793,13 @@ class PrezoApp(App):
583
793
  image_widget.clear()
584
794
 
585
795
  # Use cleaned content (bg images already removed by parser)
586
- self.query_one("#slide-content", SlideContent).set_content(
587
- slide.content.strip()
588
- )
796
+ content = slide.content.strip()
797
+
798
+ # Apply incremental filtering if enabled
799
+ if self._is_incremental_enabled() and self.reveal_index >= 0:
800
+ content = filter_list_items(content, self.reveal_index + 1)
801
+
802
+ self.query_one("#slide-content", SlideContent).set_content(content)
589
803
 
590
804
  container = self.query_one("#slide-container", VerticalScroll)
591
805
  container.scroll_home(animate=False)
@@ -594,7 +808,7 @@ class PrezoApp(App):
594
808
  self._update_notes()
595
809
 
596
810
  def _update_progress_bar(self) -> None:
597
- """Update the progress bar."""
811
+ """Update the progress bar and reveal indicator."""
598
812
  if not self.presentation:
599
813
  return
600
814
 
@@ -602,6 +816,19 @@ class PrezoApp(App):
602
816
  status.current = self.current_slide
603
817
  status.total = self.presentation.total_slides
604
818
 
819
+ # Update reveal indicator
820
+ if self._is_incremental_enabled():
821
+ list_count = self._get_list_count()
822
+ if list_count > 0 and self.reveal_index >= 0:
823
+ status.reveal_current = self.reveal_index
824
+ status.reveal_total = list_count
825
+ else:
826
+ status.reveal_current = -1
827
+ status.reveal_total = 0
828
+ else:
829
+ status.reveal_current = -1
830
+ status.reveal_total = 0
831
+
605
832
  def _update_notes(self) -> None:
606
833
  """Update the notes panel content."""
607
834
  if not self.presentation or not self.presentation.slides:
@@ -617,6 +844,9 @@ class PrezoApp(App):
617
844
 
618
845
  def watch_current_slide(self, old_value: int, new_value: int) -> None:
619
846
  """React to slide changes."""
847
+ # Determine direction and initialize reveal state appropriately
848
+ going_back = new_value < old_value
849
+ self._init_reveal_for_slide(new_value, show_all=going_back)
620
850
  self._update_display()
621
851
  self._save_position()
622
852
 
@@ -636,15 +866,41 @@ class PrezoApp(App):
636
866
  notes_panel.remove_class("visible")
637
867
 
638
868
  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
- ):
869
+ """Go to the next slide or reveal next list item."""
870
+ if not self.presentation:
871
+ return
872
+
873
+ # Check if we should reveal next item instead of advancing slide
874
+ if self._is_incremental_enabled():
875
+ list_count = self._get_list_count()
876
+ if (
877
+ list_count > 0
878
+ and self.reveal_index >= 0
879
+ and self.reveal_index < list_count - 1
880
+ ):
881
+ self.reveal_index += 1
882
+ self._update_display()
883
+ self._update_progress_bar()
884
+ return
885
+
886
+ # No more items to reveal, go to next slide
887
+ if self.current_slide < self.presentation.total_slides - 1:
888
+ # The watcher will initialize reveal_index for the new slide
644
889
  self.current_slide += 1
645
890
 
646
891
  def action_prev_slide(self) -> None:
647
- """Go to the previous slide."""
892
+ """Go to the previous slide or hide last revealed item."""
893
+ if not self.presentation:
894
+ return
895
+
896
+ # Check if we should hide last item instead of going back
897
+ if self._is_incremental_enabled() and self.reveal_index > 0:
898
+ self.reveal_index -= 1
899
+ self._update_display()
900
+ self._update_progress_bar()
901
+ return
902
+
903
+ # Go to previous slide (watcher will show all items)
648
904
  if self.current_slide > 0:
649
905
  self.current_slide -= 1
650
906
 
@@ -931,6 +1187,7 @@ def run_app(
931
1187
  *,
932
1188
  watch: bool | None = None,
933
1189
  config: Config | None = None,
1190
+ incremental: bool = False,
934
1191
  ) -> None:
935
1192
  """Run the Prezo application.
936
1193
 
@@ -938,7 +1195,10 @@ def run_app(
938
1195
  presentation_path: Path to the presentation file.
939
1196
  watch: Whether to watch for file changes. Uses config default if None.
940
1197
  config: Optional config override. Uses global config if None.
1198
+ incremental: Whether to display lists incrementally (-I flag).
941
1199
 
942
1200
  """
943
- app = PrezoApp(presentation_path, watch=watch, config=config)
1201
+ app = PrezoApp(
1202
+ presentation_path, watch=watch, config=config, incremental=incremental
1203
+ )
944
1204
  app.run()
prezo/config.py CHANGED
@@ -69,6 +69,7 @@ class BehaviorConfig:
69
69
 
70
70
  auto_reload: bool = True
71
71
  reload_interval: float = 1.0
72
+ incremental_lists: bool = False # Display lists one item at a time
72
73
 
73
74
 
74
75
  @dataclass
prezo/layout.py CHANGED
@@ -538,11 +538,9 @@ class BoxRenderable:
538
538
  self, console: Console, options: ConsoleOptions
539
539
  ) -> RenderResult:
540
540
  """Render content in a bordered panel."""
541
- yield Text("")
542
- md = Markdown(self.content)
543
- panel = Panel(md, title=self.title if self.title else None)
541
+ content = _render_box_content(self.content)
542
+ panel = Panel(content, title=self.title if self.title else None)
544
543
  yield panel
545
- yield Text("")
546
544
 
547
545
  def __rich_measure__(
548
546
  self, console: Console, options: ConsoleOptions
@@ -644,7 +642,10 @@ def render_layout(
644
642
  """
645
643
  renderables: list[RenderableType] = []
646
644
 
647
- for block in blocks:
645
+ for i, block in enumerate(blocks):
646
+ # Add spacing before box blocks (except the first one)
647
+ if block.type == "box" and i > 0:
648
+ renderables.append(Text(""))
648
649
  renderables.extend(_render_block(block))
649
650
 
650
651
  if len(renderables) == 1:
@@ -656,6 +657,96 @@ def render_layout(
656
657
  # Utilities
657
658
  # -----------------------------------------------------------------------------
658
659
 
660
+ # Pattern for **bold** text
661
+ _BOLD_PATTERN = re.compile(r"\*\*(.+?)\*\*")
662
+
663
+ # Pattern for bullet list items
664
+ _BULLET_PATTERN = re.compile(r"^[-*+]\s+(.*)$")
665
+
666
+ # Pattern for numbered list items
667
+ _NUMBERED_PATTERN = re.compile(r"^(\d+\.)\s+(.*)$")
668
+
669
+
670
+ def _parse_inline_formatting(text: str) -> Text:
671
+ """Parse inline markdown formatting like **bold**.
672
+
673
+ Args:
674
+ text: Text with possible **bold** markers.
675
+
676
+ Returns:
677
+ Rich Text object with appropriate styling.
678
+
679
+ """
680
+ result = Text()
681
+ last_end = 0
682
+
683
+ for match in _BOLD_PATTERN.finditer(text):
684
+ # Add text before match
685
+ if match.start() > last_end:
686
+ result.append(text[last_end : match.start()])
687
+ # Add bold text
688
+ result.append(match.group(1), style="bold")
689
+ last_end = match.end()
690
+
691
+ # Add remaining text
692
+ if last_end < len(text):
693
+ result.append(text[last_end:])
694
+
695
+ return result
696
+
697
+
698
+ def _render_box_content(content: str) -> Text:
699
+ """Render box content with compact spacing.
700
+
701
+ Handles the common pattern of a title line followed by a bullet list,
702
+ without the extra blank line that Rich's Markdown adds.
703
+
704
+ Args:
705
+ content: Markdown content for the box.
706
+
707
+ Returns:
708
+ Rich Text object with compact formatting.
709
+
710
+ """
711
+ lines = content.strip().split("\n")
712
+ if not lines:
713
+ return Text()
714
+
715
+ result = Text()
716
+
717
+ for i, line in enumerate(lines):
718
+ stripped = line.strip()
719
+
720
+ if not stripped:
721
+ # Blank line - add single newline
722
+ if i > 0:
723
+ result.append("\n")
724
+ continue
725
+
726
+ # Add newline before this line (except first)
727
+ if i > 0 and result.plain and not result.plain.endswith("\n"):
728
+ result.append("\n")
729
+
730
+ # Check for bullet list item
731
+ bullet_match = _BULLET_PATTERN.match(stripped)
732
+ if bullet_match:
733
+ result.append(" • ")
734
+ result.append(_parse_inline_formatting(bullet_match.group(1)))
735
+ continue
736
+
737
+ # Check for numbered list item
738
+ numbered_match = _NUMBERED_PATTERN.match(stripped)
739
+ if numbered_match:
740
+ result.append(f" {numbered_match.group(1)} ")
741
+ result.append(_parse_inline_formatting(numbered_match.group(2)))
742
+ continue
743
+
744
+ # Regular text line
745
+ result.append(_parse_inline_formatting(stripped))
746
+
747
+ return result
748
+
749
+
659
750
  # ANSI escape sequence pattern
660
751
  _ANSI_PATTERN = re.compile(r"\x1b\[[0-9;]*m")
661
752
 
prezo/parser.py CHANGED
@@ -40,6 +40,7 @@ class Slide:
40
40
  raw_content: str = "" # Original content for editing
41
41
  notes: str = ""
42
42
  images: list[ImageRef] = field(default_factory=list)
43
+ incremental: bool | None = None # Per-slide override for incremental lists
43
44
 
44
45
 
45
46
  @dataclass
@@ -51,6 +52,7 @@ class PresentationConfig:
51
52
  show_elapsed: bool | None = None
52
53
  countdown_minutes: int | None = None
53
54
  image_mode: str | None = None
55
+ incremental_lists: bool | None = None
54
56
 
55
57
  def merge_to_dict(self) -> dict[str, Any]:
56
58
  """Convert non-None values to a config dict for merging."""
@@ -65,6 +67,10 @@ class PresentationConfig:
65
67
  result.setdefault("timer", {})["countdown_minutes"] = self.countdown_minutes
66
68
  if self.image_mode is not None:
67
69
  result.setdefault("images", {})["mode"] = self.image_mode
70
+ if self.incremental_lists is not None:
71
+ result.setdefault("behavior", {})["incremental_lists"] = (
72
+ self.incremental_lists
73
+ )
68
74
  return result
69
75
 
70
76
 
@@ -170,6 +176,24 @@ def extract_notes(content: str) -> tuple[str, str]:
170
176
  return content, ""
171
177
 
172
178
 
179
+ def extract_slide_incremental(content: str) -> bool | None:
180
+ """Extract per-slide incremental directive from content.
181
+
182
+ Looks for:
183
+ - <!-- incremental --> to enable incremental lists for this slide
184
+ - <!-- no-incremental --> to disable incremental lists for this slide
185
+
186
+ Returns:
187
+ True if incremental enabled, False if disabled, None if not specified.
188
+
189
+ """
190
+ if re.search(r"<!--\s*incremental\s*-->", content, re.IGNORECASE):
191
+ return True
192
+ if re.search(r"<!--\s*no-incremental\s*-->", content, re.IGNORECASE):
193
+ return False
194
+ return None
195
+
196
+
173
197
  def extract_prezo_directives(content: str) -> PresentationConfig:
174
198
  """Extract Prezo-specific directives from presentation content.
175
199
 
@@ -217,6 +241,8 @@ def extract_prezo_directives(content: str) -> PresentationConfig:
217
241
  config.countdown_minutes = int(value)
218
242
  elif key in ("image_mode", "imagemode", "images"):
219
243
  config.image_mode = value
244
+ elif key in ("incremental_lists", "incremental", "incrementallists"):
245
+ config.incremental_lists = value.lower() in ("true", "1", "yes", "on")
220
246
 
221
247
  return config
222
248
 
@@ -420,6 +446,8 @@ def _parse_content(text: str, source_path: Path | None) -> Presentation:
420
446
  slide_content, notes = extract_notes(raw_slide)
421
447
  # Extract images BEFORE cleaning (clean_marp_directives removes bg images)
422
448
  images = extract_images(slide_content)
449
+ # Extract per-slide incremental setting
450
+ incremental = extract_slide_incremental(slide_content)
423
451
  cleaned_content = clean_marp_directives(slide_content).strip()
424
452
  slide = Slide(
425
453
  content=cleaned_content,
@@ -427,6 +455,7 @@ def _parse_content(text: str, source_path: Path | None) -> Presentation:
427
455
  raw_content=raw_slide,
428
456
  notes=notes.strip(),
429
457
  images=images,
458
+ incremental=incremental,
430
459
  )
431
460
  presentation.slides.append(slide)
432
461
 
@@ -56,6 +56,9 @@ class StatusBar(Static):
56
56
  show_elapsed: reactive[bool] = reactive(True)
57
57
  show_countdown: reactive[bool] = reactive(False)
58
58
  countdown_minutes: reactive[int] = reactive(0)
59
+ # Incremental reveal indicator
60
+ reveal_current: reactive[int] = reactive(-1) # -1 = not in reveal mode
61
+ reveal_total: reactive[int] = reactive(0)
59
62
 
60
63
  def __init__(self, **kwargs) -> None:
61
64
  """Initialize the status bar."""
@@ -78,6 +81,13 @@ class StatusBar(Static):
78
81
  bar = format_progress_bar(self.current, self.total, width=20)
79
82
  progress = f"{bar} {self.current + 1}/{self.total}"
80
83
 
84
+ # Reveal indicator (shows remaining list items)
85
+ reveal = ""
86
+ if self.reveal_current >= 0 and self.reveal_total > 0:
87
+ remaining = self.reveal_total - self.reveal_current - 1
88
+ if remaining > 0:
89
+ reveal = f" [+{remaining}]"
90
+
81
91
  # Clock part
82
92
  clock_parts = []
83
93
  if self.show_clock:
@@ -100,8 +110,8 @@ class StatusBar(Static):
100
110
 
101
111
  # Combine with spacing
102
112
  if clock:
103
- return f" {progress} {clock} "
104
- return f" {progress} "
113
+ return f" {progress}{reveal} {clock} "
114
+ return f" {progress}{reveal} "
105
115
 
106
116
  def reset_timer(self) -> None:
107
117
  """Reset the elapsed timer."""
@@ -142,6 +152,14 @@ class StatusBar(Static):
142
152
  """React to elapsed time visibility changes."""
143
153
  self.refresh()
144
154
 
155
+ def watch_reveal_current(self, value: int) -> None:
156
+ """React to reveal indicator changes."""
157
+ self.refresh()
158
+
159
+ def watch_reveal_total(self, value: int) -> None:
160
+ """React to reveal total changes."""
161
+ self.refresh()
162
+
145
163
 
146
164
  # Keep these for backwards compatibility and separate use
147
165
  class ProgressBar(Static):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: prezo
3
- Version: 2026.1.3
3
+ Version: 2026.1.4
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>
@@ -1,6 +1,6 @@
1
- prezo/__init__.py,sha256=RDpFh0F3DGdMB08n7G3HM-c14JAoqvElq4DyXLSPDQg,6740
2
- prezo/app.py,sha256=3RPSx56hjyyG55ueNWHvuUNe-KkQ3ZzidNgBRc3I2WQ,31713
3
- prezo/config.py,sha256=DLHUQkThxhmYXQE1AgpWkPvtNlDwOxQRSNjRrfJJEew,6646
1
+ prezo/__init__.py,sha256=F91cPBSScY8J-ypfjhqSQ6vieknutK9ZreSa4jCT0Rc,7006
2
+ prezo/app.py,sha256=ujWjq1UKH8wjlbu1r5v78T8QOfMI_MruPvCJZpdAGIQ,41278
3
+ prezo/config.py,sha256=dVvXrcyDWmmYLt7otDrU386A6fJ2vAoVlCkRVLpJ3I8,6718
4
4
  prezo/export/__init__.py,sha256=jdf4Xu71aUKPBXUt8k8TEUzgMee4bfEh71iJGZXtVKE,1010
5
5
  prezo/export/common.py,sha256=W9Gn2_wjd3EPuenAECrs6pqsJEWFBJHtGs8y6-4VEKQ,2451
6
6
  prezo/export/html.py,sha256=GLRjTvZUEmvee6F2NlU3sn94H47PqLTLpaC3Ab_kxo8,8787
@@ -16,8 +16,8 @@ prezo/images/kitty.py,sha256=mWR-tIE_WDP5BjOkQydPpxWBBGNaZL8PkMICesWQid8,10883
16
16
  prezo/images/overlay.py,sha256=lWIAvINxZrKembtB0gzWWaUoedNt7beFU4OhErfwWaw,9600
17
17
  prezo/images/processor.py,sha256=zMcfwltecup_AX2FhUIlPdO7c87a9jw7P9tLTIkr54U,4069
18
18
  prezo/images/sixel.py,sha256=2IeKDiMsWU1Tn3HYI3PC972ygxKGqpfz6tnhQcM_sVM,5604
19
- prezo/layout.py,sha256=xy-UaZvU2ZDe0H7XjMCDb30tog4LRdZKB5OUh_r0aqo,19929
20
- prezo/parser.py,sha256=f1eJtr9xVnxa35jgQ3GvKwNa6TvaXrIKQWgBB_geVAI,15058
19
+ prezo/layout.py,sha256=FN-nMX902jx9MA7TnySbPskmnyWAiziv_xBvTTYrFd0,22463
20
+ prezo/parser.py,sha256=TY4btwASo45EhuZ62AU5mb2Ky_eo2dNL_LfQValWdWc,16247
21
21
  prezo/screens/__init__.py,sha256=xHG9jNJz4vi1tpneSEVlD0D9I0M2U4gAGk6-R9xbUf4,508
22
22
  prezo/screens/base.py,sha256=2n6Uj8evfIbcpn4AVYNG5iM_k7rIJ3Vwmor_xrQPU9E,2057
23
23
  prezo/screens/blackout.py,sha256=wPSdD9lgu8ykAIQjU1OesnmjQoQEn9BdC4TEpABYxW4,1640
@@ -32,8 +32,8 @@ prezo/widgets/__init__.py,sha256=6qAbVVWG2fb4DLv0EzMQ-Qbi74SviXIo-7D7DyDwnrI,378
32
32
  prezo/widgets/image_display.py,sha256=8IKncaoC2iWebmJQp_QomF7UVgRxD4WThOshN1Nht2M,3361
33
33
  prezo/widgets/slide_button.py,sha256=g5mvtCZSorTIZp_PXgHYeYeeCSNFy0pW3K7iDlZu7yA,2012
34
34
  prezo/widgets/slide_content.py,sha256=AO3doIuPBSo5vec_d1xE5F8YMJWxzOq5IFO7gSYSWNw,2300
35
- prezo/widgets/status_bar.py,sha256=Wcun71kg2Q4s5aduPwTvS4kDHZj5p-zDmD7Cx3_ZFP4,8136
36
- prezo-2026.1.3.dist-info/WHEEL,sha256=eh7sammvW2TypMMMGKgsM83HyA_3qQ5Lgg3ynoecH3M,79
37
- prezo-2026.1.3.dist-info/entry_points.txt,sha256=74ShZJ_EKjzi63JyPynVnc0uCHGNjIWjAVs8vU_qTyA,38
38
- prezo-2026.1.3.dist-info/METADATA,sha256=mv3i3_l0Rq03cmGDQzwBeZ9FiUIJ6enFmJNSvm83v2I,5320
39
- prezo-2026.1.3.dist-info/RECORD,,
35
+ prezo/widgets/status_bar.py,sha256=RuyH_HeGfoecTzS3QhORwV2ahnKjhVDmGZZZrb_jm7k,8843
36
+ prezo-2026.1.4.dist-info/WHEEL,sha256=eh7sammvW2TypMMMGKgsM83HyA_3qQ5Lgg3ynoecH3M,79
37
+ prezo-2026.1.4.dist-info/entry_points.txt,sha256=74ShZJ_EKjzi63JyPynVnc0uCHGNjIWjAVs8vU_qTyA,38
38
+ prezo-2026.1.4.dist-info/METADATA,sha256=UvctCI2bOVGIgoHLdhl7CSgDNNiiN6nTREg8s-FyQQY,5320
39
+ prezo-2026.1.4.dist-info/RECORD,,