prezo 0.3.1__tar.gz → 2026.1.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. {prezo-0.3.1 → prezo-2026.1.1}/PKG-INFO +5 -3
  2. {prezo-0.3.1 → prezo-2026.1.1}/README.md +2 -0
  3. prezo-2026.1.1/pyproject.toml +73 -0
  4. {prezo-0.3.1 → prezo-2026.1.1}/src/prezo/__init__.py +23 -8
  5. {prezo-0.3.1 → prezo-2026.1.1}/src/prezo/app.py +9 -6
  6. {prezo-0.3.1 → prezo-2026.1.1}/src/prezo/config.py +1 -1
  7. {prezo-0.3.1 → prezo-2026.1.1}/src/prezo/export.py +4 -2
  8. {prezo-0.3.1 → prezo-2026.1.1}/src/prezo/images/ascii.py +10 -8
  9. {prezo-0.3.1 → prezo-2026.1.1}/src/prezo/images/kitty.py +2 -1
  10. {prezo-0.3.1 → prezo-2026.1.1}/src/prezo/images/overlay.py +2 -1
  11. {prezo-0.3.1 → prezo-2026.1.1}/src/prezo/images/sixel.py +7 -7
  12. {prezo-0.3.1 → prezo-2026.1.1}/src/prezo/terminal.py +1 -1
  13. {prezo-0.3.1 → prezo-2026.1.1}/src/prezo/widgets/image_display.py +2 -2
  14. {prezo-0.3.1 → prezo-2026.1.1}/src/prezo/widgets/status_bar.py +6 -2
  15. prezo-0.3.1/pyproject.toml +0 -44
  16. {prezo-0.3.1 → prezo-2026.1.1}/src/prezo/images/__init__.py +0 -0
  17. {prezo-0.3.1 → prezo-2026.1.1}/src/prezo/images/base.py +0 -0
  18. {prezo-0.3.1 → prezo-2026.1.1}/src/prezo/images/chafa.py +0 -0
  19. {prezo-0.3.1 → prezo-2026.1.1}/src/prezo/images/iterm.py +0 -0
  20. {prezo-0.3.1 → prezo-2026.1.1}/src/prezo/images/processor.py +0 -0
  21. {prezo-0.3.1 → prezo-2026.1.1}/src/prezo/parser.py +0 -0
  22. {prezo-0.3.1 → prezo-2026.1.1}/src/prezo/screens/__init__.py +0 -0
  23. {prezo-0.3.1 → prezo-2026.1.1}/src/prezo/screens/base.py +0 -0
  24. {prezo-0.3.1 → prezo-2026.1.1}/src/prezo/screens/blackout.py +0 -0
  25. {prezo-0.3.1 → prezo-2026.1.1}/src/prezo/screens/goto.py +0 -0
  26. {prezo-0.3.1 → prezo-2026.1.1}/src/prezo/screens/help.py +0 -0
  27. {prezo-0.3.1 → prezo-2026.1.1}/src/prezo/screens/overview.py +0 -0
  28. {prezo-0.3.1 → prezo-2026.1.1}/src/prezo/screens/search.py +0 -0
  29. {prezo-0.3.1 → prezo-2026.1.1}/src/prezo/screens/toc.py +0 -0
  30. {prezo-0.3.1 → prezo-2026.1.1}/src/prezo/themes.py +0 -0
  31. {prezo-0.3.1 → prezo-2026.1.1}/src/prezo/widgets/__init__.py +0 -0
  32. {prezo-0.3.1 → prezo-2026.1.1}/src/prezo/widgets/slide_button.py +0 -0
@@ -1,12 +1,12 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: prezo
3
- Version: 0.3.1
4
- Summary: Add your description here
3
+ Version: 2026.1.1
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>
7
7
  Requires-Dist: textual>=0.89.1
8
- Requires-Dist: python-frontmatter>=1.1.0
9
8
  Requires-Dist: textual-image>=0.8.0
9
+ Requires-Dist: python-frontmatter>=1.1.0
10
10
  Requires-Python: >=3.12
11
11
  Description-Content-Type: text/markdown
12
12
 
@@ -135,6 +135,8 @@ Presenter notes go here (after ???)
135
135
  More content...
136
136
  ```
137
137
 
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.
139
+
138
140
  ## Themes
139
141
 
140
142
  Available themes: `dark`, `light`, `dracula`, `solarized-dark`, `nord`, `gruvbox`
@@ -123,6 +123,8 @@ Presenter notes go here (after ???)
123
123
  More content...
124
124
  ```
125
125
 
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.
127
+
126
128
  ## Themes
127
129
 
128
130
  Available themes: `dark`, `light`, `dracula`, `solarized-dark`, `nord`, `gruvbox`
@@ -0,0 +1,73 @@
1
+ [project]
2
+ name = "prezo"
3
+ version = "2026.1.1"
4
+ description = "A TUI-based presentation tool for the terminal, built with Textual."
5
+ readme = "README.md"
6
+ authors = [
7
+ { name = "Stefane Fermigier", email = "sf@fermigier.com" }
8
+ ]
9
+ requires-python = ">=3.12"
10
+ dependencies = [
11
+ "textual>=0.89.1",
12
+ "textual-image>=0.8.0",
13
+ "python-frontmatter>=1.1.0",
14
+ ]
15
+
16
+ [project.scripts]
17
+ prezo = "prezo:main"
18
+
19
+ [dependency-groups]
20
+ dev = [
21
+ "pytest>=8.0.0",
22
+ "pytest-asyncio>=1.3.0",
23
+ "abilian-devtools>=0.8.0",
24
+ "ty>=0.0.2",
25
+ # See below
26
+ "cairosvg>=2.7.0",
27
+ "pypdf>=4.0.0",
28
+ "types-markdown>=3.10.0.20251106",
29
+ ]
30
+ export = [
31
+ "cairosvg>=2.7.0",
32
+ "pypdf>=4.0.0",
33
+ ]
34
+
35
+ [tool.pytest.ini_options]
36
+ testpaths = ["tests"]
37
+ asyncio_mode = "auto"
38
+ asyncio_default_fixture_loop_scope = "function"
39
+
40
+ [build-system]
41
+ requires = ["uv_build>=0.8.4,<0.9.0"]
42
+ build-backend = "uv_build"
43
+
44
+ [tool.pyrefly.errors]
45
+ # Disable errors for third-party library type issues
46
+ missing-source-for-stubs = false # markdown stubs bundled but source not found
47
+ missing-import = false # libsixel optional dependency
48
+ missing-attribute = false # PIL.Image.ADAPTIVE
49
+ no-matching-overload = false # PIL type stub issues
50
+ bad-argument-type = false # LiteralString strictness with string append
51
+ bad-override = false # Textual COMMANDS type override
52
+
53
+ [tool.ty.rules]
54
+ # Disable errors for third-party library type issues
55
+ unresolved-import = "ignore" # markdown, libsixel optional dependencies
56
+ unresolved-attribute = "ignore" # PIL.Image.ADAPTIVE
57
+ invalid-argument-type = "ignore" # PIL getdata() type issues
58
+ invalid-type-form = "ignore" # TextualImage type annotation
59
+
60
+ [tool.mypy]
61
+ python_version = "3.12"
62
+ warn_return_any = false
63
+ warn_unused_ignores = true
64
+ ignore_missing_imports = true # For optional dependencies like libsixel
65
+
66
+ [[tool.mypy.overrides]]
67
+ module = [
68
+ "frontmatter",
69
+ "cairosvg",
70
+ "libsixel",
71
+ "textual_image.*",
72
+ ]
73
+ ignore_missing_imports = true
@@ -5,6 +5,7 @@ from __future__ import annotations
5
5
  import argparse
6
6
  import sys
7
7
  from pathlib import Path
8
+ from typing import NoReturn
8
9
 
9
10
  # ANSI color codes for error messages
10
11
  RED = "\033[91m"
@@ -13,7 +14,7 @@ BOLD = "\033[1m"
13
14
  RESET = "\033[0m"
14
15
 
15
16
 
16
- def _error(message: str) -> None:
17
+ def _error(message: str) -> NoReturn:
17
18
  """Print an error message and exit."""
18
19
  sys.stderr.write(f"{RED}{BOLD}error:{RESET} {message}\n")
19
20
  sys.exit(1)
@@ -24,6 +25,26 @@ def _warn(message: str) -> None:
24
25
  sys.stderr.write(f"{YELLOW}{BOLD}warning:{RESET} {message}\n")
25
26
 
26
27
 
28
+ def _parse_size(size_str: str) -> tuple[int, int]:
29
+ """Parse a size string like '80x24' into width and height.
30
+
31
+ Args:
32
+ size_str: Size string in WIDTHxHEIGHT format.
33
+
34
+ Returns:
35
+ Tuple of (width, height).
36
+
37
+ """
38
+ try:
39
+ width, height = map(int, size_str.lower().split("x"))
40
+ return width, height
41
+ except ValueError:
42
+ _error(
43
+ f"invalid size format: {size_str}\n\n"
44
+ "Use WIDTHxHEIGHT format (e.g., 80x24, 100x30)"
45
+ )
46
+
47
+
27
48
  def _validate_file(path: Path, must_exist: bool = True) -> Path:
28
49
  """Validate a file path and return the resolved path.
29
50
 
@@ -158,13 +179,7 @@ def main() -> None:
158
179
  source_path = _validate_file(Path(args.file))
159
180
 
160
181
  # Parse size
161
- try:
162
- width, height = map(int, args.size.lower().split("x"))
163
- except ValueError:
164
- _error(
165
- f"invalid size format: {args.size}\n\n"
166
- "Use WIDTHxHEIGHT format (e.g., 80x24, 100x30)"
167
- )
182
+ width, height = _parse_size(args.size)
168
183
 
169
184
  if args.export == "html":
170
185
  from .export import run_html_export # noqa: PLC0415
@@ -12,9 +12,12 @@ import termios
12
12
  import tty
13
13
  from functools import partial
14
14
  from pathlib import Path
15
- from typing import ClassVar
15
+ from typing import TYPE_CHECKING, ClassVar
16
16
 
17
17
  from textual.app import App, ComposeResult
18
+
19
+ if TYPE_CHECKING:
20
+ from textual.timer import Timer
18
21
  from textual.binding import Binding, BindingType
19
22
  from textual.command import Hit, Hits, Provider
20
23
  from textual.containers import Horizontal, Vertical, VerticalScroll
@@ -196,7 +199,7 @@ class PrezoApp(App):
196
199
 
197
200
  ENABLE_COMMAND_PALETTE = True
198
201
  COMMAND_PALETTE_BINDING = "ctrl+p"
199
- COMMANDS: ClassVar[set[type[Provider]]] = {PrezoCommands}
202
+ COMMANDS: ClassVar[set[type[Provider]]] = {PrezoCommands} # type: ignore[assignment]
200
203
 
201
204
  CSS = """
202
205
  Screen {
@@ -239,12 +242,12 @@ class PrezoApp(App):
239
242
  #slide-container {
240
243
  width: 1fr;
241
244
  height: 100%;
242
- padding: 1 4;
245
+ padding: 0 4 1 4;
243
246
  }
244
247
 
245
248
  #slide-content {
246
249
  width: 100%;
247
- padding: 1 2;
250
+ padding: 0 2;
248
251
  }
249
252
 
250
253
  /* Image container - hidden by default */
@@ -371,7 +374,7 @@ class PrezoApp(App):
371
374
  self.watch_enabled = watch
372
375
 
373
376
  self._file_mtime: float | None = None
374
- self._watch_timer = None
377
+ self._watch_timer: Timer | None = None
375
378
  self._reload_interval = self.config.behavior.reload_interval
376
379
 
377
380
  def compose(self) -> ComposeResult:
@@ -735,7 +738,7 @@ class PrezoApp(App):
735
738
  def watch_app_theme(self, theme_name: str) -> None:
736
739
  """Apply theme when it changes."""
737
740
  # Only apply to widgets after mount (watcher fires during init)
738
- if not self.is_mounted:
741
+ if not self.is_mounted: # type: ignore[truthy-function]
739
742
  return
740
743
  self._apply_theme(theme_name)
741
744
  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[import-not-found,no-redef]
14
+ import tomli as tomllib # type: ignore[no-redef]
15
15
 
16
16
 
17
17
  CONFIG_DIR = Path.home() / ".config" / "prezo"
@@ -240,8 +240,8 @@ def export_to_pdf(
240
240
  return EXPORT_FAILED, "Presentation has no slides"
241
241
 
242
242
  # Create temporary directory for SVG files
243
- with tempfile.TemporaryDirectory() as tmpdir:
244
- tmpdir = Path(tmpdir)
243
+ with tempfile.TemporaryDirectory() as tmpdir_str:
244
+ tmpdir = Path(tmpdir_str)
245
245
  svg_files = []
246
246
 
247
247
  # Render each slide to SVG
@@ -674,6 +674,8 @@ def export_slide_to_image(
674
674
  bytestring=svg_content.encode("utf-8"),
675
675
  scale=scale,
676
676
  )
677
+ if png_data is None:
678
+ return EXPORT_FAILED, "PNG conversion returned no data"
677
679
  output_path.write_bytes(png_data)
678
680
  return EXPORT_SUCCESS, f"Exported slide {slide_num + 1} to {output_path}"
679
681
  except Exception as e:
@@ -4,6 +4,7 @@ from __future__ import annotations
4
4
 
5
5
  from functools import lru_cache
6
6
  from pathlib import Path
7
+ from typing import cast
7
8
 
8
9
  # ASCII characters from dark to light
9
10
  ASCII_CHARS = " .:-=+*#%@"
@@ -54,7 +55,7 @@ class AsciiRenderer:
54
55
  """Render image using PIL."""
55
56
  from PIL import Image
56
57
 
57
- img = Image.open(path)
58
+ img: Image.Image = Image.open(path)
58
59
 
59
60
  # Convert to grayscale
60
61
  img = img.convert("L")
@@ -68,8 +69,8 @@ class AsciiRenderer:
68
69
  # Resize
69
70
  img = img.resize((new_width, new_height))
70
71
 
71
- # Convert to ASCII
72
- pixels = list(img.getdata())
72
+ # Convert to ASCII (grayscale values 0-255)
73
+ pixels = cast("list[int]", list(img.get_flattened_data()))
73
74
  lines = []
74
75
 
75
76
  for y in range(new_height):
@@ -114,7 +115,7 @@ class ColorAsciiRenderer(AsciiRenderer):
114
115
  """Render image as colored ASCII."""
115
116
  from PIL import Image
116
117
 
117
- img = Image.open(path)
118
+ img: Image.Image = Image.open(path)
118
119
 
119
120
  # Keep color, convert to RGB
120
121
  img = img.convert("RGB")
@@ -127,8 +128,8 @@ class ColorAsciiRenderer(AsciiRenderer):
127
128
  # Resize
128
129
  img = img.resize((new_width, new_height))
129
130
 
130
- # Convert to colored ASCII
131
- pixels = list(img.getdata())
131
+ # Convert to colored ASCII (RGB tuples)
132
+ pixels = cast("list[tuple[int, int, int]]", list(img.get_flattened_data()))
132
133
  lines = []
133
134
 
134
135
  for y in range(new_height):
@@ -178,7 +179,7 @@ class HalfBlockRenderer:
178
179
  """Render image using half-blocks."""
179
180
  from PIL import Image
180
181
 
181
- img = Image.open(path)
182
+ img: Image.Image = Image.open(path)
182
183
  img = img.convert("RGB")
183
184
 
184
185
  # Calculate dimensions (height is doubled because of half-blocks)
@@ -190,7 +191,8 @@ class HalfBlockRenderer:
190
191
  new_height = new_height - (new_height % 2)
191
192
 
192
193
  img = img.resize((new_width, new_height))
193
- pixels = list(img.getdata())
194
+ # RGB tuples for half-block rendering
195
+ pixels = cast("list[tuple[int, int, int]]", list(img.get_flattened_data()))
194
196
 
195
197
  lines = []
196
198
  for y in range(0, new_height, 2):
@@ -47,13 +47,14 @@ class KittyImageManager:
47
47
 
48
48
  _instance: KittyImageManager | None = None
49
49
  _next_id: int = 1
50
+ _initialized: bool = False
50
51
 
51
52
  def __new__(cls) -> Self:
52
53
  """Create or return singleton instance."""
53
54
  if cls._instance is None:
54
55
  cls._instance = super().__new__(cls)
55
56
  cls._instance._initialized = False
56
- return cls._instance
57
+ return cls._instance # type: ignore[return-value]
57
58
 
58
59
  def __init__(self) -> None:
59
60
  """Initialize the Kitty image manager."""
@@ -45,13 +45,14 @@ class ImageOverlayRenderer:
45
45
  """
46
46
 
47
47
  _instance: ImageOverlayRenderer | None = None
48
+ _initialized: bool = False
48
49
 
49
50
  def __new__(cls) -> Self:
50
51
  """Create or return singleton instance."""
51
52
  if cls._instance is None:
52
53
  cls._instance = super().__new__(cls)
53
54
  cls._instance._initialized = False
54
- return cls._instance
55
+ return cls._instance # type: ignore[return-value]
55
56
 
56
57
  def __init__(self) -> None:
57
58
  """Initialize the image overlay renderer."""
@@ -2,7 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- from typing import TYPE_CHECKING
5
+ from typing import TYPE_CHECKING, cast
6
6
 
7
7
  if TYPE_CHECKING:
8
8
  from pathlib import Path
@@ -53,7 +53,7 @@ class SixelRenderer:
53
53
  from PIL import Image
54
54
 
55
55
  # Load and resize image
56
- img = Image.open(path)
56
+ img: Image.Image = Image.open(path)
57
57
  img = img.convert("RGB")
58
58
 
59
59
  # Calculate pixel dimensions
@@ -89,8 +89,8 @@ class SixelRenderer:
89
89
  """
90
90
  from PIL import Image
91
91
 
92
- img = Image.open(path)
93
- img = img.convert("P", palette=Image.ADAPTIVE, colors=256)
92
+ img: Image.Image = Image.open(path)
93
+ img = img.convert("P", palette=Image.Palette.ADAPTIVE, colors=256)
94
94
 
95
95
  # Calculate pixel dimensions
96
96
  pixel_width = width * 8
@@ -108,12 +108,12 @@ class SixelRenderer:
108
108
 
109
109
  img = img.resize((pixel_width, pixel_height))
110
110
 
111
- # Get palette
111
+ # Get palette and pixel data (palette indices 0-255)
112
112
  palette = img.getpalette()
113
- pixels = list(img.getdata())
113
+ pixels = cast("list[int]", list(img.get_flattened_data()))
114
114
 
115
115
  # Build sixel output
116
- output = []
116
+ output: list[str] = []
117
117
 
118
118
  # Start sixel sequence
119
119
  output.append("\x1bPq")
@@ -128,7 +128,7 @@ def supports_true_color() -> bool:
128
128
  return _is_iterm() or _is_kitty()
129
129
 
130
130
 
131
- def get_capability_summary() -> dict[str, bool | str]:
131
+ def get_capability_summary() -> dict[str, bool | int | str]:
132
132
  """Get a summary of terminal capabilities.
133
133
 
134
134
  Returns:
@@ -6,7 +6,7 @@ Falls back to Unicode halfcell rendering for unsupported terminals.
6
6
 
7
7
  from __future__ import annotations
8
8
 
9
- from typing import TYPE_CHECKING
9
+ from typing import TYPE_CHECKING, Any
10
10
 
11
11
  from textual.widgets import Static
12
12
  from textual_image.widget import Image as TextualImage
@@ -68,7 +68,7 @@ class ImageDisplay(Static):
68
68
  self._image_path: Path | str | None = image_path
69
69
  self._width: int | None = width
70
70
  self._height: int | None = height
71
- self._image_widget: TextualImage | None = None
71
+ self._image_widget: Any = None # TextualImage | None
72
72
 
73
73
  def compose(self) -> ComposeResult:
74
74
  """Compose the image widget."""
@@ -3,10 +3,14 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  from datetime import datetime, timezone
6
+ from typing import TYPE_CHECKING
6
7
 
7
8
  from textual.reactive import reactive
8
9
  from textual.widgets import Static
9
10
 
11
+ if TYPE_CHECKING:
12
+ from textual.timer import Timer
13
+
10
14
 
11
15
  def format_progress_bar(current: int, total: int, width: int = 20) -> str:
12
16
  """Generate a progress bar string.
@@ -57,7 +61,7 @@ class StatusBar(Static):
57
61
  """Initialize the status bar."""
58
62
  super().__init__(**kwargs)
59
63
  self._start_time: datetime | None = None
60
- self._timer = None
64
+ self._timer: Timer | None = None
61
65
 
62
66
  def on_mount(self) -> None:
63
67
  """Start the timer on mount."""
@@ -185,7 +189,7 @@ class ClockDisplay(Static):
185
189
  """Initialize the clock display."""
186
190
  super().__init__(**kwargs)
187
191
  self._start_time: datetime | None = None
188
- self._timer = None
192
+ self._timer: Timer | None = None
189
193
 
190
194
  def on_mount(self) -> None:
191
195
  """Start the clock timer on mount."""
@@ -1,44 +0,0 @@
1
- [project]
2
- name = "prezo"
3
- version = "0.3.1"
4
- description = "Add your description here"
5
- readme = "README.md"
6
- authors = [
7
- { name = "Stefane Fermigier", email = "sf@fermigier.com" }
8
- ]
9
- requires-python = ">=3.12"
10
- dependencies = [
11
- "textual>=0.89.1",
12
- "python-frontmatter>=1.1.0",
13
- "textual-image>=0.8.0",
14
- ]
15
-
16
- [project.scripts]
17
- prezo = "prezo:main"
18
-
19
- [dependency-groups]
20
- dev = [
21
- "pytest>=8.0.0",
22
- "cairosvg>=2.7.0",
23
- "pypdf>=4.0.0",
24
- "abilian-devtools>=0.8.0",
25
- "pytest-asyncio>=1.3.0",
26
- "ty>=0.0.2",
27
- ]
28
- export = [
29
- "cairosvg>=2.7.0",
30
- "pypdf>=4.0.0",
31
- ]
32
-
33
- [tool.pytest.ini_options]
34
- testpaths = ["tests"]
35
- asyncio_mode = "auto"
36
- asyncio_default_fixture_loop_scope = "function"
37
-
38
- [build-system]
39
- requires = ["uv_build>=0.8.4,<0.9.0"]
40
- build-backend = "uv_build"
41
-
42
- # [build-system]
43
- # requires = ["hatchling"]
44
- # build-backend = "hatchling.build"
File without changes
File without changes
File without changes
File without changes