prezo 2026.1.1__tar.gz → 2026.1.3__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 (39) hide show
  1. {prezo-2026.1.1 → prezo-2026.1.3}/PKG-INFO +25 -4
  2. {prezo-2026.1.1 → prezo-2026.1.3}/README.md +24 -3
  3. {prezo-2026.1.1 → prezo-2026.1.3}/pyproject.toml +11 -3
  4. {prezo-2026.1.1 → prezo-2026.1.3}/src/prezo/__init__.py +8 -0
  5. {prezo-2026.1.1 → prezo-2026.1.3}/src/prezo/app.py +25 -31
  6. {prezo-2026.1.1 → prezo-2026.1.3}/src/prezo/config.py +1 -1
  7. prezo-2026.1.3/src/prezo/export/__init__.py +36 -0
  8. prezo-2026.1.3/src/prezo/export/common.py +77 -0
  9. prezo-2026.1.3/src/prezo/export/html.py +340 -0
  10. prezo-2026.1.3/src/prezo/export/images.py +261 -0
  11. prezo-2026.1.3/src/prezo/export/pdf.py +497 -0
  12. prezo-2026.1.3/src/prezo/export/svg.py +170 -0
  13. prezo-2026.1.3/src/prezo/layout.py +680 -0
  14. {prezo-2026.1.1 → prezo-2026.1.3}/src/prezo/parser.py +11 -4
  15. {prezo-2026.1.1 → prezo-2026.1.3}/src/prezo/widgets/__init__.py +9 -1
  16. prezo-2026.1.3/src/prezo/widgets/slide_content.py +81 -0
  17. prezo-2026.1.1/src/prezo/export.py +0 -835
  18. {prezo-2026.1.1 → prezo-2026.1.3}/src/prezo/images/__init__.py +0 -0
  19. {prezo-2026.1.1 → prezo-2026.1.3}/src/prezo/images/ascii.py +0 -0
  20. {prezo-2026.1.1 → prezo-2026.1.3}/src/prezo/images/base.py +0 -0
  21. {prezo-2026.1.1 → prezo-2026.1.3}/src/prezo/images/chafa.py +0 -0
  22. {prezo-2026.1.1 → prezo-2026.1.3}/src/prezo/images/iterm.py +0 -0
  23. {prezo-2026.1.1 → prezo-2026.1.3}/src/prezo/images/kitty.py +0 -0
  24. {prezo-2026.1.1 → prezo-2026.1.3}/src/prezo/images/overlay.py +0 -0
  25. {prezo-2026.1.1 → prezo-2026.1.3}/src/prezo/images/processor.py +0 -0
  26. {prezo-2026.1.1 → prezo-2026.1.3}/src/prezo/images/sixel.py +0 -0
  27. {prezo-2026.1.1 → prezo-2026.1.3}/src/prezo/screens/__init__.py +0 -0
  28. {prezo-2026.1.1 → prezo-2026.1.3}/src/prezo/screens/base.py +0 -0
  29. {prezo-2026.1.1 → prezo-2026.1.3}/src/prezo/screens/blackout.py +0 -0
  30. {prezo-2026.1.1 → prezo-2026.1.3}/src/prezo/screens/goto.py +0 -0
  31. {prezo-2026.1.1 → prezo-2026.1.3}/src/prezo/screens/help.py +0 -0
  32. {prezo-2026.1.1 → prezo-2026.1.3}/src/prezo/screens/overview.py +0 -0
  33. {prezo-2026.1.1 → prezo-2026.1.3}/src/prezo/screens/search.py +0 -0
  34. {prezo-2026.1.1 → prezo-2026.1.3}/src/prezo/screens/toc.py +0 -0
  35. {prezo-2026.1.1 → prezo-2026.1.3}/src/prezo/terminal.py +0 -0
  36. {prezo-2026.1.1 → prezo-2026.1.3}/src/prezo/themes.py +0 -0
  37. {prezo-2026.1.1 → prezo-2026.1.3}/src/prezo/widgets/image_display.py +0 -0
  38. {prezo-2026.1.1 → prezo-2026.1.3}/src/prezo/widgets/slide_button.py +0 -0
  39. {prezo-2026.1.1 → prezo-2026.1.3}/src/prezo/widgets/status_bar.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: prezo
3
- Version: 2026.1.1
3
+ Version: 2026.1.3
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>
@@ -16,9 +16,10 @@ A TUI-based presentation tool for the terminal, built with [Textual](https://tex
16
16
 
17
17
  Display presentations written in Markdown using conventions similar to those of [MARP](https://marp.app/) or [Deckset](https://www.deckset.com/).
18
18
 
19
- ## Features (v0.3)
19
+ ## Features
20
20
 
21
21
  - **Markdown presentations** - MARP/Deckset format with `---` slide separators
22
+ - **Column layouts** - Pandoc-style fenced divs for multi-column slides (`::: columns`)
22
23
  - **Live reload** - Auto-refresh when file changes (1s polling)
23
24
  - **Keyboard navigation** - Vim-style keys, arrow keys, and more
24
25
  - **Slide overview** - Grid view for quick navigation (`o`)
@@ -130,12 +131,32 @@ Presenter notes go here (after ???)
130
131
 
131
132
  # Third Slide
132
133
 
134
+ ::: columns
135
+ ::: column
136
+ **Left Column**
137
+ - Point A
138
+ - Point B
139
+ :::
140
+
141
+ ::: column
142
+ **Right Column**
143
+ - Point C
144
+ - Point D
145
+ :::
146
+ :::
147
+
148
+ ---
149
+
150
+ # Fourth Slide
151
+
133
152
  <!-- notes: Alternative notes syntax -->
134
153
 
135
- More content...
154
+ ::: center
155
+ **Centered content**
156
+ :::
136
157
  ```
137
158
 
138
- See the [Writing Presentations in Markdown](docs/tutorial.md) tutorial for a complete guide on creating presentations, including images, presenter notes, and configuration directives.
159
+ See the [Writing Presentations in Markdown](docs/tutorial.md) tutorial for a complete guide on creating presentations, including column layouts, images, presenter notes, and configuration directives.
139
160
 
140
161
  ## Themes
141
162
 
@@ -4,9 +4,10 @@ A TUI-based presentation tool for the terminal, built with [Textual](https://tex
4
4
 
5
5
  Display presentations written in Markdown using conventions similar to those of [MARP](https://marp.app/) or [Deckset](https://www.deckset.com/).
6
6
 
7
- ## Features (v0.3)
7
+ ## Features
8
8
 
9
9
  - **Markdown presentations** - MARP/Deckset format with `---` slide separators
10
+ - **Column layouts** - Pandoc-style fenced divs for multi-column slides (`::: columns`)
10
11
  - **Live reload** - Auto-refresh when file changes (1s polling)
11
12
  - **Keyboard navigation** - Vim-style keys, arrow keys, and more
12
13
  - **Slide overview** - Grid view for quick navigation (`o`)
@@ -118,12 +119,32 @@ Presenter notes go here (after ???)
118
119
 
119
120
  # Third Slide
120
121
 
122
+ ::: columns
123
+ ::: column
124
+ **Left Column**
125
+ - Point A
126
+ - Point B
127
+ :::
128
+
129
+ ::: column
130
+ **Right Column**
131
+ - Point C
132
+ - Point D
133
+ :::
134
+ :::
135
+
136
+ ---
137
+
138
+ # Fourth Slide
139
+
121
140
  <!-- notes: Alternative notes syntax -->
122
141
 
123
- More content...
142
+ ::: center
143
+ **Centered content**
144
+ :::
124
145
  ```
125
146
 
126
- See the [Writing Presentations in Markdown](docs/tutorial.md) tutorial for a complete guide on creating presentations, including images, presenter notes, and configuration directives.
147
+ See the [Writing Presentations in Markdown](docs/tutorial.md) tutorial for a complete guide on creating presentations, including column layouts, images, presenter notes, and configuration directives.
127
148
 
128
149
  ## Themes
129
150
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "prezo"
3
- version = "2026.1.1"
3
+ version = "2026.1.3"
4
4
  description = "A TUI-based presentation tool for the terminal, built with Textual."
5
5
  readme = "README.md"
6
6
  authors = [
@@ -18,14 +18,16 @@ prezo = "prezo:main"
18
18
 
19
19
  [dependency-groups]
20
20
  dev = [
21
+ "abilian-devtools>=0.8.0",
22
+ # Testing
21
23
  "pytest>=8.0.0",
22
24
  "pytest-asyncio>=1.3.0",
23
- "abilian-devtools>=0.8.0",
25
+ # Typechecking
24
26
  "ty>=0.0.2",
27
+ "types-markdown>=3.10.0.20251106",
25
28
  # See below
26
29
  "cairosvg>=2.7.0",
27
30
  "pypdf>=4.0.0",
28
- "types-markdown>=3.10.0.20251106",
29
31
  ]
30
32
  export = [
31
33
  "cairosvg>=2.7.0",
@@ -36,6 +38,12 @@ export = [
36
38
  testpaths = ["tests"]
37
39
  asyncio_mode = "auto"
38
40
  asyncio_default_fixture_loop_scope = "function"
41
+ markers = [
42
+ "unit: Unit tests (fast, isolated, no I/O)",
43
+ "integration: Integration tests (component interactions, file I/O)",
44
+ "e2e: End-to-end tests (full app/CLI workflows)",
45
+ "slow: Slow tests that can be skipped with -m 'not slow'",
46
+ ]
39
47
 
40
48
  [build-system]
41
49
  requires = ["uv_build>=0.8.4,<0.9.0"]
@@ -128,6 +128,13 @@ def main() -> None:
128
128
  action="store_true",
129
129
  help="Export without window decorations (for printing)",
130
130
  )
131
+ parser.add_argument(
132
+ "--pdf-backend",
133
+ metavar="BACKEND",
134
+ choices=["auto", "chrome", "inkscape", "cairosvg"],
135
+ default="auto",
136
+ help="PDF conversion backend (auto, chrome, inkscape, cairosvg). Default: auto",
137
+ )
131
138
  parser.add_argument(
132
139
  "--scale",
133
140
  metavar="FACTOR",
@@ -218,6 +225,7 @@ def main() -> None:
218
225
  width=width,
219
226
  height=height,
220
227
  chrome=not args.no_chrome,
228
+ pdf_backend=args.pdf_backend,
221
229
  ),
222
230
  )
223
231
  else:
@@ -39,7 +39,7 @@ from .screens import (
39
39
  )
40
40
  from .terminal import ImageCapability, detect_image_capability
41
41
  from .themes import get_next_theme, get_theme
42
- from .widgets import ImageDisplay, StatusBar
42
+ from .widgets import ImageDisplay, SlideContent, StatusBar
43
43
 
44
44
  WELCOME_MESSAGE = """\
45
45
  # Welcome to Prezo
@@ -199,7 +199,7 @@ class PrezoApp(App):
199
199
 
200
200
  ENABLE_COMMAND_PALETTE = True
201
201
  COMMAND_PALETTE_BINDING = "ctrl+p"
202
- COMMANDS: ClassVar[set[type[Provider]]] = {PrezoCommands} # type: ignore[assignment]
202
+ COMMANDS: ClassVar[set[type[Provider]]] = {PrezoCommands}
203
203
 
204
204
  CSS = """
205
205
  Screen {
@@ -389,7 +389,7 @@ class PrezoApp(App):
389
389
  yield ImageDisplay(id="slide-image")
390
390
  # Text container
391
391
  with VerticalScroll(id="slide-container"):
392
- yield Markdown("", id="slide-content")
392
+ yield SlideContent("", id="slide-content")
393
393
  with Vertical(id="notes-panel"):
394
394
  yield Static("Notes", id="notes-title")
395
395
  yield Markdown("", id="notes-content")
@@ -519,7 +519,7 @@ class PrezoApp(App):
519
519
  recent_section = _format_recent_files(self.state.recent_files)
520
520
  if recent_section:
521
521
  welcome += recent_section
522
- self.query_one("#slide-content", Markdown).update(welcome)
522
+ self.query_one("#slide-content", SlideContent).set_content(welcome)
523
523
  status = self.query_one("#status-bar", StatusBar)
524
524
  status.current = 0
525
525
  status.total = 1
@@ -555,35 +555,27 @@ class PrezoApp(App):
555
555
  image_container.add_class("visible")
556
556
 
557
557
  # Apply layout based on MARP directive
558
- layout = first_image.layout
559
- if layout == "left":
560
- image_container.add_class("layout-left")
561
- # Ensure image is before text
562
- horizontal_container.move_child(
563
- image_container, before=slide_container
564
- )
565
- elif layout == "right":
566
- image_container.add_class("layout-right")
567
- # Move image after text
568
- horizontal_container.move_child(
569
- image_container, after=slide_container
570
- )
571
- elif layout == "inline":
572
- image_container.add_class("layout-inline")
573
- horizontal_container.move_child(
574
- image_container, before=slide_container
575
- )
576
- elif layout in ("background", "fit"):
577
- # Background/fit images: show image full width behind/above text
578
- image_container.add_class("layout-inline")
579
- horizontal_container.move_child(
580
- image_container, before=slide_container
581
- )
558
+ match first_image.layout:
559
+ case "left":
560
+ image_container.add_class("layout-left")
561
+ horizontal_container.move_child(
562
+ image_container, before=slide_container
563
+ )
564
+ case "right":
565
+ image_container.add_class("layout-right")
566
+ horizontal_container.move_child(
567
+ image_container, after=slide_container
568
+ )
569
+ case "inline" | "background" | "fit":
570
+ image_container.add_class("layout-inline")
571
+ horizontal_container.move_child(
572
+ image_container, before=slide_container
573
+ )
582
574
 
583
575
  # Apply dynamic width if size_percent is specified
584
576
  default_size = 50
585
577
  has_custom_size = first_image.size_percent != default_size
586
- if has_custom_size and layout in ("left", "right"):
578
+ if has_custom_size and first_image.layout in ("left", "right"):
587
579
  image_container.styles.width = f"{first_image.size_percent}%"
588
580
  else:
589
581
  image_container.styles.width = None # Reset to CSS default
@@ -591,7 +583,9 @@ class PrezoApp(App):
591
583
  image_widget.clear()
592
584
 
593
585
  # Use cleaned content (bg images already removed by parser)
594
- self.query_one("#slide-content", Markdown).update(slide.content.strip())
586
+ self.query_one("#slide-content", SlideContent).set_content(
587
+ slide.content.strip()
588
+ )
595
589
 
596
590
  container = self.query_one("#slide-container", VerticalScroll)
597
591
  container.scroll_home(animate=False)
@@ -738,7 +732,7 @@ class PrezoApp(App):
738
732
  def watch_app_theme(self, theme_name: str) -> None:
739
733
  """Apply theme when it changes."""
740
734
  # Only apply to widgets after mount (watcher fires during init)
741
- if not self.is_mounted: # type: ignore[truthy-function]
735
+ if not self.is_mounted:
742
736
  return
743
737
  self._apply_theme(theme_name)
744
738
  self.notify(f"Theme: {theme_name}", timeout=1)
@@ -11,7 +11,7 @@ from typing import Any
11
11
  try:
12
12
  import tomllib
13
13
  except ImportError:
14
- import tomli as tomllib # type: ignore[no-redef]
14
+ import tomli as tomllib
15
15
 
16
16
 
17
17
  CONFIG_DIR = Path.home() / ".config" / "prezo"
@@ -0,0 +1,36 @@
1
+ """Export functionality for prezo presentations.
2
+
3
+ Exports presentations to PDF, HTML, PNG, and SVG formats.
4
+
5
+ IMPORTANT: PDF/PNG/SVG export must be a faithful image of the TUI console.
6
+ This requires proper monospace font loading. If Fira Code is not available,
7
+ alignment may be incorrect.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from .common import (
13
+ ExportError,
14
+ check_font_availability,
15
+ print_font_warnings,
16
+ )
17
+ from .html import export_to_html, render_slide_to_html, run_html_export
18
+ from .images import export_slide_to_image, export_to_images, run_image_export
19
+ from .pdf import combine_svgs_to_pdf, export_to_pdf, run_export
20
+ from .svg import render_slide_to_svg
21
+
22
+ __all__ = [
23
+ "ExportError",
24
+ "check_font_availability",
25
+ "combine_svgs_to_pdf",
26
+ "export_slide_to_image",
27
+ "export_to_html",
28
+ "export_to_images",
29
+ "export_to_pdf",
30
+ "print_font_warnings",
31
+ "render_slide_to_html",
32
+ "render_slide_to_svg",
33
+ "run_export",
34
+ "run_html_export",
35
+ "run_image_export",
36
+ ]
@@ -0,0 +1,77 @@
1
+ """Common utilities and constants for export functionality."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import shutil
6
+ import subprocess
7
+ import sys
8
+
9
+
10
+ class ExportError(Exception):
11
+ """Raised when an export operation fails."""
12
+
13
+
14
+ # Exit codes for CLI (only used in run_* wrapper functions)
15
+ EXIT_SUCCESS = 0
16
+ EXIT_FAILURE = 2
17
+
18
+ # Backwards compatibility aliases (deprecated, use exceptions instead)
19
+ EXPORT_SUCCESS = EXIT_SUCCESS
20
+ EXPORT_FAILED = EXIT_FAILURE
21
+
22
+
23
+ def check_font_availability() -> list[str]:
24
+ """Check if required fonts are available on the system.
25
+
26
+ Returns a list of warning messages (empty if all fonts are available).
27
+ """
28
+ warnings = []
29
+
30
+ # Check for fc-list (fontconfig) to query system fonts
31
+ fc_list_path = shutil.which("fc-list")
32
+ if fc_list_path:
33
+ try:
34
+ result = subprocess.run(
35
+ [fc_list_path, ":family"],
36
+ capture_output=True,
37
+ text=True,
38
+ timeout=5,
39
+ check=False,
40
+ )
41
+ fonts = result.stdout.lower()
42
+
43
+ # Check for Fira Code (primary monospace font)
44
+ if "fira code" not in fonts and "firacode" not in fonts:
45
+ warnings.append(
46
+ "Fira Code font not found. Install it for best results:\n"
47
+ " macOS: brew install --cask font-fira-code\n"
48
+ " Ubuntu: sudo apt install fonts-firacode\n"
49
+ " Or download from: https://github.com/tonsky/FiraCode"
50
+ )
51
+
52
+ except (subprocess.TimeoutExpired, subprocess.SubprocessError):
53
+ # Can't check fonts, skip warning
54
+ pass
55
+ else:
56
+ # No fontconfig available (Windows or minimal system)
57
+ # We can't easily check fonts, so just note the requirement
58
+ warnings.append(
59
+ "Cannot verify font availability. For correct alignment, ensure "
60
+ "Fira Code font is installed."
61
+ )
62
+
63
+ return warnings
64
+
65
+
66
+ def print_font_warnings(warnings: list[str]) -> None:
67
+ """Print font warnings to stderr."""
68
+ if warnings:
69
+ print("\n⚠️ Font Warning:", file=sys.stderr)
70
+ for warning in warnings:
71
+ for line in warning.split("\n"):
72
+ print(f" {line}", file=sys.stderr)
73
+ print(
74
+ "\n Without proper fonts, column alignment may be incorrect in exports.",
75
+ file=sys.stderr,
76
+ )
77
+ print(file=sys.stderr)