prezo 0.3.1__py3-none-any.whl → 2026.1.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
prezo/__init__.py CHANGED
@@ -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
prezo/app.py CHANGED
@@ -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)
prezo/config.py CHANGED
@@ -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"
prezo/export.py CHANGED
@@ -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:
prezo/images/ascii.py CHANGED
@@ -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):
prezo/images/kitty.py CHANGED
@@ -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."""
prezo/images/overlay.py CHANGED
@@ -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."""
prezo/images/sixel.py CHANGED
@@ -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")
prezo/terminal.py CHANGED
@@ -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,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`
@@ -1,16 +1,16 @@
1
- prezo/__init__.py,sha256=01nNl1YtriyC0t81wvf_TYv9-GfdE3GrJ6hAAtewqUE,6118
2
- prezo/app.py,sha256=Y22P-v26eeUXeZrRevKRDHnba8ttMyH6LrS1y7VYc6g,31937
3
- prezo/config.py,sha256=643qfnmDB6mKxL5A0Hj52PPk7chscwtXEZp7vFAQUXc,6689
4
- prezo/export.py,sha256=gAVh9EjcJlG_DxSwy7nrG8bzNb9Mg5xo7pxLYW9t_HQ,23219
1
+ prezo/__init__.py,sha256=blmZoW_bPndFLVT4UZzrfAp8IaGuvmFhtNGUs0nHpFQ,6434
2
+ prezo/app.py,sha256=5K1tcinaTSI-X24bqV7W7fuwC0Wtw6IH_Gfy2RI3WMo,32086
3
+ prezo/config.py,sha256=byBBFHZU3fkq3dRfg5h4zG_eihbi7lHZkriSv-g-ogY,6672
4
+ prezo/export.py,sha256=1npeBV3m9_eTGhYHG7G-ekDGIRX4vNBgtt2RwpjLydc,23324
5
5
  prezo/images/__init__.py,sha256=xrWSR3z0SXYpLtjIvR2VOMxiJGkxEsls5-EOs9GecFA,324
6
- prezo/images/ascii.py,sha256=lBN6LT2f3wd65lqp5HviByxIvf_w0bcByE_uAkV6ZaY,7342
6
+ prezo/images/ascii.py,sha256=aNz02jN4rkDw0WzmkGDrAGw1R1dY5QGREvIIPI6jwow,7613
7
7
  prezo/images/base.py,sha256=STuS57AVSJ2lzwyn0QrIceGaSd2IWEiLGN-elT3u3AM,2749
8
8
  prezo/images/chafa.py,sha256=rqqctIw5xQarEYz5SR-2a5ePJ3xbm0a3NWiLwxNBEUE,3726
9
9
  prezo/images/iterm.py,sha256=bSIN6qfOt3URTjbV-d963K2bX9KdfT5cBQQOrIivZPs,3742
10
- prezo/images/kitty.py,sha256=PyB7aw4kI5Va4-HFtpCg7_TPIH3-V55M18-FyfScH7k,10822
11
- prezo/images/overlay.py,sha256=iPRJFGtf9MVVEhwfhgyx54c_GxdFBGGD3_VqDsRPMIA,9539
10
+ prezo/images/kitty.py,sha256=mWR-tIE_WDP5BjOkQydPpxWBBGNaZL8PkMICesWQid8,10883
11
+ prezo/images/overlay.py,sha256=lWIAvINxZrKembtB0gzWWaUoedNt7beFU4OhErfwWaw,9600
12
12
  prezo/images/processor.py,sha256=zMcfwltecup_AX2FhUIlPdO7c87a9jw7P9tLTIkr54U,4069
13
- prezo/images/sixel.py,sha256=6TL8WllZfH-rn4LDH2iigoTygyxY_jg80A2jUNvl8W0,5484
13
+ prezo/images/sixel.py,sha256=2IeKDiMsWU1Tn3HYI3PC972ygxKGqpfz6tnhQcM_sVM,5604
14
14
  prezo/parser.py,sha256=bD2MecHm7EssHd5LB2Bw6JuUqbjWPztWUu2meYwsyIQ,14793
15
15
  prezo/screens/__init__.py,sha256=xHG9jNJz4vi1tpneSEVlD0D9I0M2U4gAGk6-R9xbUf4,508
16
16
  prezo/screens/base.py,sha256=2n6Uj8evfIbcpn4AVYNG5iM_k7rIJ3Vwmor_xrQPU9E,2057
@@ -20,13 +20,13 @@ prezo/screens/help.py,sha256=fjwHp9qPMmyRIaME-Bcz-g6bn8UrtbL_Dk269QSU-zs,2987
20
20
  prezo/screens/overview.py,sha256=s9-ifbcnXYhbxb_Kl2UhpB3IE7msInX6LWB-J1dazLo,5382
21
21
  prezo/screens/search.py,sha256=3YG9WLGEIKW3YHpM0K1lgwhuqBveXd8ZoQZ178_zGd4,7809
22
22
  prezo/screens/toc.py,sha256=8WYb5nbgP9agY-hUTATxLU4X1uka_bc2MN86hFW4aRg,8241
23
- prezo/terminal.py,sha256=1eMUFDpsnjlAkvBAjyPoMMuN7GxpvY94je99F47KxF8,3632
23
+ prezo/terminal.py,sha256=Z3DuuighY-qfF6GWH_AkR5RnAc5Gj3LsPS266VNj7Pk,3638
24
24
  prezo/themes.py,sha256=3keUgheOsNGjS0uCjRv7az9sVSnrz5tc-jZ58YNB7tg,3070
25
25
  prezo/widgets/__init__.py,sha256=UeTHBgPDvqTkK5tTsPXhdJXP3qZefnltKtUtvJBx9m0,295
26
- prezo/widgets/image_display.py,sha256=c3buM6NiM3oCcS2HWsb0HMeumsksS-U9PQnfl1EViEc,3349
26
+ prezo/widgets/image_display.py,sha256=8IKncaoC2iWebmJQp_QomF7UVgRxD4WThOshN1Nht2M,3361
27
27
  prezo/widgets/slide_button.py,sha256=g5mvtCZSorTIZp_PXgHYeYeeCSNFy0pW3K7iDlZu7yA,2012
28
- prezo/widgets/status_bar.py,sha256=C4Jw4pOpkx9FFLWo6Cmi57dJF0qt13LhfdKmBcGaY3o,8020
29
- prezo-0.3.1.dist-info/WHEEL,sha256=eh7sammvW2TypMMMGKgsM83HyA_3qQ5Lgg3ynoecH3M,79
30
- prezo-0.3.1.dist-info/entry_points.txt,sha256=74ShZJ_EKjzi63JyPynVnc0uCHGNjIWjAVs8vU_qTyA,38
31
- prezo-0.3.1.dist-info/METADATA,sha256=1h7pr64Tq1ji0obykUIEpg1zBRwhJ_0Otlrj05eDUgU,4831
32
- prezo-0.3.1.dist-info/RECORD,,
28
+ prezo/widgets/status_bar.py,sha256=Wcun71kg2Q4s5aduPwTvS4kDHZj5p-zDmD7Cx3_ZFP4,8136
29
+ prezo-2026.1.1.dist-info/WHEEL,sha256=eh7sammvW2TypMMMGKgsM83HyA_3qQ5Lgg3ynoecH3M,79
30
+ prezo-2026.1.1.dist-info/entry_points.txt,sha256=74ShZJ_EKjzi63JyPynVnc0uCHGNjIWjAVs8vU_qTyA,38
31
+ prezo-2026.1.1.dist-info/METADATA,sha256=b1ymsfoTcOMGVvcoTbb3OSucstuN2QmPz-te-beEjSM,5061
32
+ prezo-2026.1.1.dist-info/RECORD,,