prezo 2026.1.2__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/layout.py CHANGED
@@ -11,6 +11,13 @@ Supports Pandoc-style fenced div syntax:
11
11
  :::
12
12
  :::
13
13
 
14
+ Additional layout blocks:
15
+ ::: center - Horizontally centered content
16
+ ::: right - Right-aligned content
17
+ ::: spacer [n] - Vertical space (default 1 line)
18
+ ::: box [title] - Bordered panel with optional title
19
+ ::: divider [style] - Horizontal rule (single/double/thick/dashed)
20
+
14
21
  """
15
22
 
16
23
  from __future__ import annotations
@@ -20,9 +27,12 @@ from dataclasses import dataclass, field
20
27
  from io import StringIO
21
28
  from typing import TYPE_CHECKING, Literal
22
29
 
30
+ from rich.cells import cell_len
23
31
  from rich.console import Console, ConsoleOptions, Group, RenderResult
24
32
  from rich.markdown import Markdown
25
33
  from rich.measure import Measurement
34
+ from rich.panel import Panel
35
+ from rich.rule import Rule
26
36
  from rich.text import Text
27
37
 
28
38
  if TYPE_CHECKING:
@@ -33,22 +43,30 @@ if TYPE_CHECKING:
33
43
  # -----------------------------------------------------------------------------
34
44
 
35
45
 
46
+ BlockType = Literal[
47
+ "plain", "columns", "column", "center", "right", "spacer", "box", "divider"
48
+ ]
49
+
50
+
36
51
  @dataclass
37
52
  class LayoutBlock:
38
53
  """A block of content with layout information."""
39
54
 
40
- type: Literal["plain", "columns", "column", "center"]
55
+ type: BlockType
41
56
  content: str = "" # Raw markdown content (for leaf blocks)
42
57
  children: list[LayoutBlock] = field(default_factory=list)
43
58
  width_percent: int = 0 # For column blocks (0 = auto/equal)
59
+ title: str = "" # For box blocks
60
+ style: str = "" # For divider blocks (single/double/thick/dashed)
44
61
 
45
62
 
46
63
  # -----------------------------------------------------------------------------
47
64
  # Parser
48
65
  # -----------------------------------------------------------------------------
49
66
 
50
- # Pattern for opening fenced div: ::: type [width]
51
- OPEN_PATTERN = re.compile(r"^:::\s*(\w+)(?:\s+(\d+))?\s*$")
67
+ # Pattern for opening fenced div: ::: type [arg]
68
+ # arg can be: a number (width %), a quoted string (title), or a word (style)
69
+ OPEN_PATTERN = re.compile(r'^:::\s*(\w+)(?:\s+"([^"]+)"|\s+(\S+))?\s*$')
52
70
  # Pattern for closing fenced div: :::
53
71
  CLOSE_PATTERN = re.compile(r"^:::\s*$")
54
72
 
@@ -76,10 +94,14 @@ def parse_layout(content: str) -> list[LayoutBlock]:
76
94
 
77
95
  if match:
78
96
  block_type = match.group(1).lower()
79
- width = int(match.group(2)) if match.group(2) else 0
97
+ # Group 2 is quoted string, Group 3 is unquoted arg
98
+ quoted_arg = match.group(2) # For "title"
99
+ unquoted_arg = match.group(3) # For width or style
80
100
 
81
101
  # Find matching close and nested content
82
- block, end_idx = _parse_fenced_block(lines, i, block_type, width)
102
+ block, end_idx = _parse_fenced_block(
103
+ lines, i, block_type, quoted_arg, unquoted_arg
104
+ )
83
105
  if block:
84
106
  blocks.append(block)
85
107
  i = end_idx + 1
@@ -104,8 +126,59 @@ def parse_layout(content: str) -> list[LayoutBlock]:
104
126
  return blocks
105
127
 
106
128
 
129
+ def _create_block(
130
+ block_type: str,
131
+ inner_content: str,
132
+ quoted_arg: str | None,
133
+ unquoted_arg: str | None,
134
+ ) -> LayoutBlock:
135
+ """Create a LayoutBlock from parsed fenced div content.
136
+
137
+ Args:
138
+ block_type: The type from ::: type.
139
+ inner_content: Content inside the fenced div.
140
+ quoted_arg: Quoted argument (e.g., title for box).
141
+ unquoted_arg: Unquoted argument (e.g., width or style).
142
+
143
+ Returns:
144
+ A LayoutBlock of the appropriate type.
145
+
146
+ """
147
+ content = inner_content.strip()
148
+ width = int(unquoted_arg) if unquoted_arg and unquoted_arg.isdigit() else 0
149
+
150
+ # Use a dispatch table for simple content blocks
151
+ simple_types = {"center", "right", "plain"}
152
+
153
+ if block_type == "columns":
154
+ block = LayoutBlock(type="columns")
155
+ block.children = parse_layout(inner_content)
156
+ elif block_type == "column":
157
+ block = LayoutBlock(type="column", content=content, width_percent=width)
158
+ elif block_type == "spacer":
159
+ lines_count = width if width > 0 else 1
160
+ block = LayoutBlock(type="spacer", width_percent=lines_count)
161
+ elif block_type == "box":
162
+ title = quoted_arg or unquoted_arg or ""
163
+ block = LayoutBlock(type="box", content=content, title=title)
164
+ elif block_type == "divider":
165
+ style = unquoted_arg if unquoted_arg else "single"
166
+ block = LayoutBlock(type="divider", style=style)
167
+ elif block_type in simple_types:
168
+ block = LayoutBlock(type=block_type, content=content)
169
+ else:
170
+ # Unknown block type - treat as plain
171
+ block = LayoutBlock(type="plain", content=content)
172
+
173
+ return block
174
+
175
+
107
176
  def _parse_fenced_block(
108
- lines: list[str], start: int, block_type: str, width: int
177
+ lines: list[str],
178
+ start: int,
179
+ block_type: str,
180
+ quoted_arg: str | None,
181
+ unquoted_arg: str | None,
109
182
  ) -> tuple[LayoutBlock | None, int]:
110
183
  """Parse a fenced div block starting at the given line.
111
184
 
@@ -113,7 +186,8 @@ def _parse_fenced_block(
113
186
  lines: All lines of content.
114
187
  start: Starting line index (the opening :::).
115
188
  block_type: The type from ::: type.
116
- width: Width percentage if specified.
189
+ quoted_arg: Quoted argument (e.g., title for box).
190
+ unquoted_arg: Unquoted argument (e.g., width or style).
117
191
 
118
192
  Returns:
119
193
  Tuple of (LayoutBlock or None, end line index).
@@ -143,28 +217,8 @@ def _parse_fenced_block(
143
217
  return None, start
144
218
 
145
219
  inner_content = "\n".join(content_lines)
146
-
147
- # For columns/column types, parse children
148
- if block_type in ("columns", "column", "center"):
149
- block = LayoutBlock(
150
- type=block_type, # type: ignore[arg-type]
151
- width_percent=width,
152
- )
153
-
154
- if block_type == "columns":
155
- # Parse children (should be column blocks)
156
- block.children = parse_layout(inner_content)
157
- elif block_type == "column":
158
- # Column contains markdown content, but might have nested structure
159
- # For now, treat as plain content
160
- block.content = inner_content.strip()
161
- elif block_type == "center":
162
- block.content = inner_content.strip()
163
-
164
- return block, i
165
-
166
- # Unknown block type - treat content as plain
167
- return LayoutBlock(type="plain", content=inner_content.strip()), i
220
+ block = _create_block(block_type, inner_content, quoted_arg, unquoted_arg)
221
+ return block, i
168
222
 
169
223
 
170
224
  def has_layout_blocks(content: str) -> bool:
@@ -303,10 +357,17 @@ class ColumnsRenderable:
303
357
  file=StringIO(),
304
358
  )
305
359
 
306
- # Render markdown content
360
+ # Render content - check for nested layout blocks
307
361
  if column.content:
308
- md = Markdown(column.content)
309
- col_console.print(md)
362
+ if has_layout_blocks(column.content):
363
+ # Parse and render nested layout blocks
364
+ blocks = parse_layout(column.content)
365
+ renderable = render_layout(blocks)
366
+ col_console.print(renderable)
367
+ else:
368
+ # Plain markdown
369
+ md = Markdown(column.content)
370
+ col_console.print(md)
310
371
 
311
372
  # Get rendered lines
312
373
  output = col_console.export_text(styles=True)
@@ -405,6 +466,168 @@ class CenterRenderable:
405
466
  return Measurement(1, options.max_width)
406
467
 
407
468
 
469
+ class RightRenderable:
470
+ """Rich renderable that right-aligns content."""
471
+
472
+ def __init__(self, content: str) -> None:
473
+ """Initialize right-align renderable.
474
+
475
+ Args:
476
+ content: Markdown content to right-align.
477
+
478
+ """
479
+ self.content = content
480
+
481
+ def __rich_console__(
482
+ self, console: Console, options: ConsoleOptions
483
+ ) -> RenderResult:
484
+ """Render right-aligned content."""
485
+ yield Text("")
486
+ md = Markdown(self.content, justify="right")
487
+ yield md
488
+ yield Text("")
489
+
490
+ def __rich_measure__(
491
+ self, console: Console, options: ConsoleOptions
492
+ ) -> Measurement:
493
+ """Return the measurement of this renderable."""
494
+ return Measurement(1, options.max_width)
495
+
496
+
497
+ class SpacerRenderable:
498
+ """Rich renderable that creates vertical space."""
499
+
500
+ def __init__(self, lines: int = 1) -> None:
501
+ """Initialize spacer renderable.
502
+
503
+ Args:
504
+ lines: Number of blank lines to insert.
505
+
506
+ """
507
+ self.lines = max(1, lines)
508
+
509
+ def __rich_console__(
510
+ self, console: Console, options: ConsoleOptions
511
+ ) -> RenderResult:
512
+ """Render vertical space."""
513
+ for _ in range(self.lines):
514
+ yield Text("")
515
+
516
+ def __rich_measure__(
517
+ self, console: Console, options: ConsoleOptions
518
+ ) -> Measurement:
519
+ """Return the measurement of this renderable."""
520
+ return Measurement(0, 0)
521
+
522
+
523
+ class BoxRenderable:
524
+ """Rich renderable that displays content in a bordered panel."""
525
+
526
+ def __init__(self, content: str, title: str = "") -> None:
527
+ """Initialize box renderable.
528
+
529
+ Args:
530
+ content: Markdown content to display in the box.
531
+ title: Optional title for the box.
532
+
533
+ """
534
+ self.content = content
535
+ self.title = title
536
+
537
+ def __rich_console__(
538
+ self, console: Console, options: ConsoleOptions
539
+ ) -> RenderResult:
540
+ """Render content in a bordered panel."""
541
+ content = _render_box_content(self.content)
542
+ panel = Panel(content, title=self.title if self.title else None)
543
+ yield panel
544
+
545
+ def __rich_measure__(
546
+ self, console: Console, options: ConsoleOptions
547
+ ) -> Measurement:
548
+ """Return the measurement of this renderable."""
549
+ return Measurement(1, options.max_width)
550
+
551
+
552
+ # Divider style characters
553
+ DIVIDER_STYLES = {
554
+ "single": "─",
555
+ "double": "═",
556
+ "thick": "━",
557
+ "dashed": "╌",
558
+ }
559
+
560
+
561
+ class DividerRenderable:
562
+ """Rich renderable that displays a horizontal rule."""
563
+
564
+ def __init__(self, style: str = "single") -> None:
565
+ """Initialize divider renderable.
566
+
567
+ Args:
568
+ style: Style of the divider (single, double, thick, dashed).
569
+
570
+ """
571
+ self.style = style if style in DIVIDER_STYLES else "single"
572
+
573
+ def __rich_console__(
574
+ self, console: Console, options: ConsoleOptions
575
+ ) -> RenderResult:
576
+ """Render horizontal rule."""
577
+ yield Text("")
578
+ char = DIVIDER_STYLES[self.style]
579
+ yield Rule(characters=char)
580
+ yield Text("")
581
+
582
+ def __rich_measure__(
583
+ self, console: Console, options: ConsoleOptions
584
+ ) -> Measurement:
585
+ """Return the measurement of this renderable."""
586
+ return Measurement(1, options.max_width)
587
+
588
+
589
+ def _render_block(block: LayoutBlock) -> list[RenderableType]:
590
+ """Render a single block to Rich renderables.
591
+
592
+ Args:
593
+ block: A LayoutBlock to render.
594
+
595
+ Returns:
596
+ List of Rich renderables for this block.
597
+
598
+ """
599
+ if block.type == "columns":
600
+ result: list[RenderableType] = []
601
+ columns = [c for c in block.children if c.type == "column"]
602
+ if columns:
603
+ result.append(ColumnsRenderable(columns))
604
+ # Also render any non-column children (plain text between columns)
605
+ for child in block.children:
606
+ if child.type == "plain":
607
+ result.append(Markdown(child.content))
608
+ return result
609
+
610
+ if block.type == "spacer":
611
+ return [SpacerRenderable(block.width_percent)]
612
+
613
+ if block.type == "box":
614
+ return [BoxRenderable(block.content, block.title)]
615
+
616
+ if block.type == "divider":
617
+ return [DividerRenderable(block.style)]
618
+
619
+ # Simple content blocks: plain, center, right, column
620
+ renderable_map: dict[str, type] = {
621
+ "center": CenterRenderable,
622
+ "right": RightRenderable,
623
+ }
624
+ if block.type in renderable_map:
625
+ return [renderable_map[block.type](block.content)]
626
+
627
+ # Default: plain markdown (for "plain" and standalone "column")
628
+ return [Markdown(block.content)]
629
+
630
+
408
631
  def render_layout(
409
632
  blocks: list[LayoutBlock],
410
633
  ) -> RenderableType:
@@ -419,24 +642,11 @@ def render_layout(
419
642
  """
420
643
  renderables: list[RenderableType] = []
421
644
 
422
- for block in blocks:
423
- match block.type:
424
- case "plain":
425
- renderables.append(Markdown(block.content))
426
- case "columns":
427
- # Filter to only column children
428
- columns = [c for c in block.children if c.type == "column"]
429
- if columns:
430
- renderables.append(ColumnsRenderable(columns))
431
- # Also render any non-column children (plain text between columns)
432
- for child in block.children:
433
- if child.type == "plain":
434
- renderables.append(Markdown(child.content))
435
- case "center":
436
- renderables.append(CenterRenderable(block.content))
437
- case "column":
438
- # Standalone column (shouldn't happen normally)
439
- renderables.append(Markdown(block.content))
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(""))
649
+ renderables.extend(_render_block(block))
440
650
 
441
651
  if len(renderables) == 1:
442
652
  return renderables[0]
@@ -447,18 +657,115 @@ def render_layout(
447
657
  # Utilities
448
658
  # -----------------------------------------------------------------------------
449
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
+
450
750
  # ANSI escape sequence pattern
451
751
  _ANSI_PATTERN = re.compile(r"\x1b\[[0-9;]*m")
452
752
 
453
753
 
454
754
  def _visible_length(text: str) -> int:
455
- """Calculate visible length of text, excluding ANSI codes.
755
+ """Calculate visible cell width of text, excluding ANSI codes.
756
+
757
+ Uses Rich's cell_len for proper Unicode width handling:
758
+ - Regular ASCII characters = 1 cell
759
+ - Wide characters (CJK, emoji) = 2 cells
760
+ - Zero-width characters = 0 cells
456
761
 
457
762
  Args:
458
763
  text: Text possibly containing ANSI escape codes.
459
764
 
460
765
  Returns:
461
- Visible character count.
766
+ Visible cell width (terminal columns).
462
767
 
463
768
  """
464
- return len(_ANSI_PATTERN.sub("", text))
769
+ # Strip ANSI codes first, then calculate cell width
770
+ clean_text = _ANSI_PATTERN.sub("", text)
771
+ return cell_len(clean_text)
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.2
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,7 +1,12 @@
1
- prezo/__init__.py,sha256=blmZoW_bPndFLVT4UZzrfAp8IaGuvmFhtNGUs0nHpFQ,6434
2
- prezo/app.py,sha256=Uk4Wn8Q9_lJVMwHhGvBLrZ_ARCvtwUpX9X_KDyJ-OXs,32144
3
- prezo/config.py,sha256=byBBFHZU3fkq3dRfg5h4zG_eihbi7lHZkriSv-g-ogY,6672
4
- prezo/export.py,sha256=BNqN3a9XXlh22ELJAXnxw_fp0VMas_t-7DWLqyg1kbc,24430
1
+ prezo/__init__.py,sha256=F91cPBSScY8J-ypfjhqSQ6vieknutK9ZreSa4jCT0Rc,7006
2
+ prezo/app.py,sha256=ujWjq1UKH8wjlbu1r5v78T8QOfMI_MruPvCJZpdAGIQ,41278
3
+ prezo/config.py,sha256=dVvXrcyDWmmYLt7otDrU386A6fJ2vAoVlCkRVLpJ3I8,6718
4
+ prezo/export/__init__.py,sha256=jdf4Xu71aUKPBXUt8k8TEUzgMee4bfEh71iJGZXtVKE,1010
5
+ prezo/export/common.py,sha256=W9Gn2_wjd3EPuenAECrs6pqsJEWFBJHtGs8y6-4VEKQ,2451
6
+ prezo/export/html.py,sha256=GLRjTvZUEmvee6F2NlU3sn94H47PqLTLpaC3Ab_kxo8,8787
7
+ prezo/export/images.py,sha256=TNTGIYNRTxwVjbHqZXYjIj0gouV_Dy1_DtenMlxsquE,7410
8
+ prezo/export/pdf.py,sha256=9rzybk8o1vEu_JwvKB2abzrYoKfpEA8IP5pEFJHF5WM,13942
9
+ prezo/export/svg.py,sha256=gxhRXpLiKcVr4NSF8nwaid74p8f9dvj4RwXwvMP0EbU,5698
5
10
  prezo/images/__init__.py,sha256=xrWSR3z0SXYpLtjIvR2VOMxiJGkxEsls5-EOs9GecFA,324
6
11
  prezo/images/ascii.py,sha256=aNz02jN4rkDw0WzmkGDrAGw1R1dY5QGREvIIPI6jwow,7613
7
12
  prezo/images/base.py,sha256=STuS57AVSJ2lzwyn0QrIceGaSd2IWEiLGN-elT3u3AM,2749
@@ -11,8 +16,8 @@ prezo/images/kitty.py,sha256=mWR-tIE_WDP5BjOkQydPpxWBBGNaZL8PkMICesWQid8,10883
11
16
  prezo/images/overlay.py,sha256=lWIAvINxZrKembtB0gzWWaUoedNt7beFU4OhErfwWaw,9600
12
17
  prezo/images/processor.py,sha256=zMcfwltecup_AX2FhUIlPdO7c87a9jw7P9tLTIkr54U,4069
13
18
  prezo/images/sixel.py,sha256=2IeKDiMsWU1Tn3HYI3PC972ygxKGqpfz6tnhQcM_sVM,5604
14
- prezo/layout.py,sha256=dQHp-9TV-vCp1yqt3WeiS1mrghAh9HPw3aap5DKOeoE,13551
15
- 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
16
21
  prezo/screens/__init__.py,sha256=xHG9jNJz4vi1tpneSEVlD0D9I0M2U4gAGk6-R9xbUf4,508
17
22
  prezo/screens/base.py,sha256=2n6Uj8evfIbcpn4AVYNG5iM_k7rIJ3Vwmor_xrQPU9E,2057
18
23
  prezo/screens/blackout.py,sha256=wPSdD9lgu8ykAIQjU1OesnmjQoQEn9BdC4TEpABYxW4,1640
@@ -27,8 +32,8 @@ prezo/widgets/__init__.py,sha256=6qAbVVWG2fb4DLv0EzMQ-Qbi74SviXIo-7D7DyDwnrI,378
27
32
  prezo/widgets/image_display.py,sha256=8IKncaoC2iWebmJQp_QomF7UVgRxD4WThOshN1Nht2M,3361
28
33
  prezo/widgets/slide_button.py,sha256=g5mvtCZSorTIZp_PXgHYeYeeCSNFy0pW3K7iDlZu7yA,2012
29
34
  prezo/widgets/slide_content.py,sha256=AO3doIuPBSo5vec_d1xE5F8YMJWxzOq5IFO7gSYSWNw,2300
30
- prezo/widgets/status_bar.py,sha256=Wcun71kg2Q4s5aduPwTvS4kDHZj5p-zDmD7Cx3_ZFP4,8136
31
- prezo-2026.1.2.dist-info/WHEEL,sha256=eh7sammvW2TypMMMGKgsM83HyA_3qQ5Lgg3ynoecH3M,79
32
- prezo-2026.1.2.dist-info/entry_points.txt,sha256=74ShZJ_EKjzi63JyPynVnc0uCHGNjIWjAVs8vU_qTyA,38
33
- prezo-2026.1.2.dist-info/METADATA,sha256=EVYgqxUj2L3lvUNia9MKC6RjKQXFAaijrnfSD7WRwDg,5320
34
- prezo-2026.1.2.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,,