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 +12 -1
- prezo/app.py +276 -16
- prezo/config.py +1 -0
- prezo/layout.py +96 -5
- prezo/parser.py +29 -0
- prezo/widgets/status_bar.py +20 -2
- {prezo-2026.1.3.dist-info → prezo-2026.1.4.dist-info}/METADATA +1 -1
- {prezo-2026.1.3.dist-info → prezo-2026.1.4.dist-info}/RECORD +10 -10
- {prezo-2026.1.3.dist-info → prezo-2026.1.4.dist-info}/WHEEL +0 -0
- {prezo-2026.1.3.dist-info → prezo-2026.1.4.dist-info}/entry_points.txt +0 -0
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(
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
587
|
-
|
|
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
|
-
|
|
642
|
-
|
|
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(
|
|
1201
|
+
app = PrezoApp(
|
|
1202
|
+
presentation_path, watch=watch, config=config, incremental=incremental
|
|
1203
|
+
)
|
|
944
1204
|
app.run()
|
prezo/config.py
CHANGED
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
|
-
|
|
542
|
-
|
|
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
|
|
prezo/widgets/status_bar.py
CHANGED
|
@@ -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
|
-
prezo/__init__.py,sha256=
|
|
2
|
-
prezo/app.py,sha256=
|
|
3
|
-
prezo/config.py,sha256=
|
|
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=
|
|
20
|
-
prezo/parser.py,sha256=
|
|
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=
|
|
36
|
-
prezo-2026.1.
|
|
37
|
-
prezo-2026.1.
|
|
38
|
-
prezo-2026.1.
|
|
39
|
-
prezo-2026.1.
|
|
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,,
|
|
File without changes
|
|
File without changes
|