prezo 0.3.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.
@@ -0,0 +1,72 @@
1
+ """Slide button widget for overview grid."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+
7
+ from textual.widgets import Button
8
+
9
+ MAX_TITLE_LENGTH = 35
10
+
11
+
12
+ def extract_slide_title(content: str, slide_index: int) -> str:
13
+ """Extract the first heading (any level) from slide content.
14
+
15
+ Args:
16
+ content: Slide markdown content
17
+ slide_index: Zero-based slide index (for fallback title)
18
+
19
+ Returns:
20
+ Extracted or generated title, truncated to MAX_TITLE_LENGTH
21
+
22
+ """
23
+ for line in content.strip().split("\n"):
24
+ line = line.strip()
25
+ match = re.match(r"^(#{1,6})\s+(.+)$", line)
26
+ if match:
27
+ title = match.group(2).strip()
28
+ title = re.sub(r"\*{1,2}([^*]+)\*{1,2}", r"\1", title)
29
+ return _truncate(title)
30
+
31
+ for line in content.strip().split("\n"):
32
+ line = line.strip()
33
+ if line and not line.startswith("<!--"):
34
+ return _truncate(line)
35
+
36
+ return f"Slide {slide_index + 1}"
37
+
38
+
39
+ def _truncate(text: str) -> str:
40
+ """Truncate text to MAX_TITLE_LENGTH with ellipsis if needed."""
41
+ if len(text) > MAX_TITLE_LENGTH:
42
+ return text[: MAX_TITLE_LENGTH - 3] + "..."
43
+ return text
44
+
45
+
46
+ class SlideButton(Button):
47
+ """A button representing a slide in the overview grid."""
48
+
49
+ def __init__(
50
+ self,
51
+ slide_index: int,
52
+ content: str,
53
+ *,
54
+ is_current: bool = False,
55
+ ) -> None:
56
+ """Initialize a slide button.
57
+
58
+ Args:
59
+ slide_index: Zero-based index of the slide.
60
+ content: Markdown content of the slide.
61
+ is_current: Whether this is the currently active slide.
62
+
63
+ """
64
+ title = extract_slide_title(content, slide_index)
65
+ super().__init__(title, id=f"slide-{slide_index}")
66
+ self.slide_index = slide_index
67
+ self.is_current = is_current
68
+
69
+ def on_mount(self) -> None:
70
+ """Add current class if this is the active slide."""
71
+ if self.is_current:
72
+ self.add_class("current")
@@ -0,0 +1,240 @@
1
+ """Status bar widgets for Prezo."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from datetime import datetime, timezone
6
+
7
+ from textual.reactive import reactive
8
+ from textual.widgets import Static
9
+
10
+
11
+ def format_progress_bar(current: int, total: int, width: int = 20) -> str:
12
+ """Generate a progress bar string.
13
+
14
+ Args:
15
+ current: Current position (0-indexed)
16
+ total: Total number of items
17
+ width: Width of the progress bar in characters
18
+
19
+ Returns:
20
+ Progress bar string like "████████░░░░░░░░░░░░"
21
+
22
+ """
23
+ if total <= 0:
24
+ return "░" * width
25
+
26
+ progress = (current + 1) / total
27
+ filled = int(progress * width)
28
+ empty = width - filled
29
+
30
+ return "█" * filled + "░" * empty
31
+
32
+
33
+ def format_time(seconds: int) -> str:
34
+ """Format seconds as HH:MM:SS or MM:SS."""
35
+ if seconds < 0:
36
+ return "-" + format_time(-seconds)
37
+
38
+ hours, remainder = divmod(seconds, 3600)
39
+ minutes, secs = divmod(remainder, 60)
40
+
41
+ if hours > 0:
42
+ return f"{hours}:{minutes:02d}:{secs:02d}"
43
+ return f"{minutes}:{secs:02d}"
44
+
45
+
46
+ class StatusBar(Static):
47
+ """Combined status bar showing progress, clock, and elapsed time."""
48
+
49
+ current: reactive[int] = reactive(0)
50
+ total: reactive[int] = reactive(1)
51
+ show_clock: reactive[bool] = reactive(True)
52
+ show_elapsed: reactive[bool] = reactive(True)
53
+ show_countdown: reactive[bool] = reactive(False)
54
+ countdown_minutes: reactive[int] = reactive(0)
55
+
56
+ def __init__(self, **kwargs) -> None:
57
+ """Initialize the status bar."""
58
+ super().__init__(**kwargs)
59
+ self._start_time: datetime | None = None
60
+ self._timer = None
61
+
62
+ def on_mount(self) -> None:
63
+ """Start the timer on mount."""
64
+ self._start_time = datetime.now(tz=timezone.utc)
65
+ self._timer = self.set_interval(1.0, self._tick)
66
+
67
+ def _tick(self) -> None:
68
+ """Timer callback to refresh the display."""
69
+ self.refresh()
70
+
71
+ def render(self) -> str:
72
+ """Render the status bar content."""
73
+ # Progress part
74
+ bar = format_progress_bar(self.current, self.total, width=20)
75
+ progress = f"{bar} {self.current + 1}/{self.total}"
76
+
77
+ # Clock part
78
+ clock_parts = []
79
+ if self.show_clock:
80
+ clock_parts.append(
81
+ datetime.now(tz=timezone.utc).astimezone().strftime("%H:%M:%S"),
82
+ )
83
+
84
+ if self.show_elapsed and self._start_time:
85
+ elapsed = datetime.now(tz=timezone.utc) - self._start_time
86
+ elapsed_secs = int(elapsed.total_seconds())
87
+ clock_parts.append(f"+{format_time(elapsed_secs)}")
88
+
89
+ if self.show_countdown and self.countdown_minutes > 0 and self._start_time:
90
+ total_secs = self.countdown_minutes * 60
91
+ elapsed = datetime.now(tz=timezone.utc) - self._start_time
92
+ remaining = total_secs - int(elapsed.total_seconds())
93
+ clock_parts.append(f"-{format_time(remaining)}")
94
+
95
+ clock = " │ ".join(clock_parts) if clock_parts else ""
96
+
97
+ # Combine with spacing
98
+ if clock:
99
+ return f" {progress} {clock} "
100
+ return f" {progress} "
101
+
102
+ def reset_timer(self) -> None:
103
+ """Reset the elapsed timer."""
104
+ self._start_time = datetime.now(tz=timezone.utc)
105
+ self.refresh()
106
+
107
+ def toggle_clock(self) -> None:
108
+ """Cycle through clock display modes."""
109
+ if self.show_clock and not self.show_elapsed:
110
+ self.show_elapsed = True
111
+ elif self.show_clock and self.show_elapsed and not self.show_countdown:
112
+ if self.countdown_minutes > 0:
113
+ self.show_countdown = True
114
+ else:
115
+ self.show_clock = False
116
+ self.show_elapsed = False
117
+ elif self.show_countdown:
118
+ self.show_clock = False
119
+ self.show_elapsed = False
120
+ self.show_countdown = False
121
+ else:
122
+ self.show_clock = True
123
+ self.show_elapsed = False
124
+
125
+ def watch_current(self, value: int) -> None:
126
+ """React to current slide changes."""
127
+ self.refresh()
128
+
129
+ def watch_total(self, value: int) -> None:
130
+ """React to total slides changes."""
131
+ self.refresh()
132
+
133
+ def watch_show_clock(self, value: bool) -> None:
134
+ """React to clock visibility changes."""
135
+ self.refresh()
136
+
137
+ def watch_show_elapsed(self, value: bool) -> None:
138
+ """React to elapsed time visibility changes."""
139
+ self.refresh()
140
+
141
+
142
+ # Keep these for backwards compatibility and separate use
143
+ class ProgressBar(Static):
144
+ """A progress bar widget showing slide progress."""
145
+
146
+ current: reactive[int] = reactive(0)
147
+ total: reactive[int] = reactive(1)
148
+
149
+ def __init__(self, current: int = 0, total: int = 1, **kwargs) -> None:
150
+ """Initialize the progress bar.
151
+
152
+ Args:
153
+ current: Current slide index (0-based).
154
+ total: Total number of slides.
155
+ **kwargs: Additional arguments for Static widget.
156
+
157
+ """
158
+ super().__init__(**kwargs)
159
+ self.current = current
160
+ self.total = total
161
+
162
+ def render(self) -> str:
163
+ """Render the progress bar."""
164
+ bar = format_progress_bar(self.current, self.total, width=30)
165
+ return f" {bar} {self.current + 1}/{self.total} "
166
+
167
+ def watch_current(self, value: int) -> None:
168
+ """React to current position changes."""
169
+ self.refresh()
170
+
171
+ def watch_total(self, value: int) -> None:
172
+ """React to total count changes."""
173
+ self.refresh()
174
+
175
+
176
+ class ClockDisplay(Static):
177
+ """A clock widget showing current time, elapsed, and countdown."""
178
+
179
+ show_clock: reactive[bool] = reactive(True)
180
+ show_elapsed: reactive[bool] = reactive(True)
181
+ show_countdown: reactive[bool] = reactive(False)
182
+ countdown_minutes: reactive[int] = reactive(0)
183
+
184
+ def __init__(self, **kwargs) -> None:
185
+ """Initialize the clock display."""
186
+ super().__init__(**kwargs)
187
+ self._start_time: datetime | None = None
188
+ self._timer = None
189
+
190
+ def on_mount(self) -> None:
191
+ """Start the clock timer on mount."""
192
+ self._start_time = datetime.now(tz=timezone.utc)
193
+ self._timer = self.set_interval(1.0, self._update_time)
194
+
195
+ def _update_time(self) -> None:
196
+ """Timer callback to update the display."""
197
+ self.refresh()
198
+
199
+ def render(self) -> str:
200
+ """Render the clock display."""
201
+ parts = []
202
+
203
+ if self.show_clock:
204
+ now = datetime.now(tz=timezone.utc).astimezone()
205
+ parts.append(now.strftime("%H:%M:%S"))
206
+
207
+ if self.show_elapsed and self._start_time:
208
+ elapsed = datetime.now(tz=timezone.utc) - self._start_time
209
+ elapsed_secs = int(elapsed.total_seconds())
210
+ parts.append(f"+{format_time(elapsed_secs)}")
211
+
212
+ if self.show_countdown and self.countdown_minutes > 0 and self._start_time:
213
+ total_secs = self.countdown_minutes * 60
214
+ elapsed = datetime.now(tz=timezone.utc) - self._start_time
215
+ remaining = total_secs - int(elapsed.total_seconds())
216
+ parts.append(f"-{format_time(remaining)}")
217
+
218
+ return " │ ".join(parts) if parts else ""
219
+
220
+ def reset_timer(self) -> None:
221
+ """Reset the elapsed timer."""
222
+ self._start_time = datetime.now(tz=timezone.utc)
223
+ self.refresh()
224
+
225
+ def toggle_clock(self) -> None:
226
+ """Cycle through clock display modes."""
227
+ if self.show_clock and not self.show_elapsed:
228
+ self.show_elapsed = True
229
+ elif self.show_clock and self.show_elapsed and not self.show_countdown:
230
+ if self.countdown_minutes > 0:
231
+ self.show_countdown = True
232
+ else:
233
+ self.show_clock = False
234
+ self.show_elapsed = False
235
+ elif self.show_countdown:
236
+ self.show_clock = False
237
+ self.show_elapsed = False
238
+ self.show_countdown = False
239
+ else:
240
+ self.show_clock = True
@@ -0,0 +1,194 @@
1
+ Metadata-Version: 2.3
2
+ Name: prezo
3
+ Version: 0.3.1
4
+ Summary: Add your description here
5
+ Author: Stefane Fermigier
6
+ Author-email: Stefane Fermigier <sf@fermigier.com>
7
+ Requires-Dist: textual>=0.89.1
8
+ Requires-Dist: python-frontmatter>=1.1.0
9
+ Requires-Dist: textual-image>=0.8.0
10
+ Requires-Python: >=3.12
11
+ Description-Content-Type: text/markdown
12
+
13
+ # Prezo
14
+
15
+ A TUI-based presentation tool for the terminal, built with [Textual](https://textual.textualize.io/).
16
+
17
+ Display presentations written in Markdown using conventions similar to those of [MARP](https://marp.app/) or [Deckset](https://www.deckset.com/).
18
+
19
+ ## Features (v0.3)
20
+
21
+ - **Markdown presentations** - MARP/Deckset format with `---` slide separators
22
+ - **Live reload** - Auto-refresh when file changes (1s polling)
23
+ - **Keyboard navigation** - Vim-style keys, arrow keys, and more
24
+ - **Slide overview** - Grid view for quick navigation (`o`)
25
+ - **Search** - Find slides by content (`/`)
26
+ - **Table of contents** - Navigate by headings (`t`)
27
+ - **Go to slide** - Jump to specific slide number (`:`)
28
+ - **Presenter notes** - Toggle notes panel (`p`)
29
+ - **Themes** - 6 color schemes (`T` to cycle): dark, light, dracula, solarized-dark, nord, gruvbox
30
+ - **Timer/Clock** - Elapsed time and countdown (`c`)
31
+ - **Edit slides** - Open in $EDITOR (`e`), saves back to source file
32
+ - **Export** - PDF, HTML, PNG, SVG formats with customizable themes and sizes
33
+ - **Image support** - Inline and background images with MARP layout directives (left/right/fit)
34
+ - **Native image viewing** - Press `i` for full-quality image display (iTerm2/Kitty protocols)
35
+ - **Blackout/Whiteout** - Blank screen modes (`b`/`w`)
36
+ - **Command palette** - Quick access to all commands (`Ctrl+P`)
37
+ - **Config file** - Customizable settings via `~/.config/prezo/config.toml`
38
+ - **Recent files** - Tracks recently opened presentations
39
+ - **Position memory** - Remembers last slide position per file
40
+
41
+ ## Demo
42
+
43
+ [![asciicast](https://asciinema.org/a/0rRbYzbq7iyha2wLkN6o4OPcX.svg)](https://asciinema.org/a/0rRbYzbq7iyha2wLkN6o4OPcX)
44
+
45
+
46
+ ## Installation
47
+
48
+ ```bash
49
+ pip install prezo
50
+ ```
51
+
52
+ Or with [uv](https://docs.astral.sh/uv/):
53
+
54
+ ```bash
55
+ uv tool install prezo
56
+ ```
57
+
58
+ ## Usage
59
+
60
+ ```bash
61
+ # View a presentation
62
+ prezo presentation.md
63
+
64
+ # Disable auto-reload
65
+ prezo --no-watch presentation.md
66
+
67
+ # Use custom config
68
+ prezo -c myconfig.toml presentation.md
69
+
70
+ # Set image rendering mode
71
+ prezo --image-mode ascii presentation.md # Options: auto, kitty, sixel, iterm, ascii, none
72
+
73
+ # Export to PDF
74
+ prezo -e pdf presentation.md
75
+
76
+ # Export with options
77
+ prezo -e pdf presentation.md --theme light --size 100x30 --no-chrome
78
+ ```
79
+
80
+ ## Keyboard Shortcuts
81
+
82
+ | Key | Action |
83
+ |-----|--------|
84
+ | `→` / `j` / `Space` | Next slide |
85
+ | `←` / `k` | Previous slide |
86
+ | `Home` / `g` | First slide |
87
+ | `End` / `G` | Last slide |
88
+ | `:` | Go to slide number |
89
+ | `/` | Search slides |
90
+ | `o` | Slide overview |
91
+ | `t` | Table of contents |
92
+ | `p` | Toggle notes panel |
93
+ | `c` | Cycle clock display |
94
+ | `T` | Cycle theme |
95
+ | `b` | Blackout screen |
96
+ | `w` | Whiteout screen |
97
+ | `i` | View image (native quality) |
98
+ | `e` | Edit in $EDITOR |
99
+ | `r` | Reload file |
100
+ | `Ctrl+P` | Command palette |
101
+ | `?` | Help |
102
+ | `q` | Quit |
103
+
104
+ ## Presentation Format
105
+
106
+ Prezo supports standard Markdown with MARP/Deckset conventions:
107
+
108
+ ```markdown
109
+ ---
110
+ title: My Presentation
111
+ theme: default
112
+ ---
113
+
114
+ # First Slide
115
+
116
+ Content here...
117
+
118
+ ---
119
+
120
+ # Second Slide
121
+
122
+ - Bullet points
123
+ - Code blocks
124
+ - Tables
125
+
126
+ ???
127
+ Presenter notes go here (after ???)
128
+
129
+ ---
130
+
131
+ # Third Slide
132
+
133
+ <!-- notes: Alternative notes syntax -->
134
+
135
+ More content...
136
+ ```
137
+
138
+ ## Themes
139
+
140
+ Available themes: `dark`, `light`, `dracula`, `solarized-dark`, `nord`, `gruvbox`
141
+
142
+ Press `T` to cycle through themes during presentation.
143
+
144
+ ## Export Options
145
+
146
+ Prezo supports multiple export formats: PDF, HTML, PNG, and SVG.
147
+
148
+ ```bash
149
+ # PDF export
150
+ prezo -e pdf presentation.md # Default: 80x24, dark theme
151
+ prezo -e pdf presentation.md --theme light # Light theme (for printing)
152
+ prezo -e pdf presentation.md --size 100x30 # Custom dimensions
153
+ prezo -e pdf presentation.md --no-chrome # No window decorations
154
+ prezo -e pdf presentation.md -o slides.pdf # Custom output path
155
+
156
+ # HTML export (single self-contained file)
157
+ prezo -e html presentation.md
158
+
159
+ # Image export (PNG/SVG)
160
+ prezo -e png presentation.md # All slides as PNG
161
+ prezo -e png presentation.md --slide 3 # Single slide (1-indexed)
162
+ prezo -e svg presentation.md --scale 2.0 # SVG with scale factor
163
+ ```
164
+
165
+ PDF/PNG/SVG export requires optional dependencies:
166
+
167
+ ```bash
168
+ pip install prezo[export]
169
+ # or
170
+ pip install cairosvg pypdf
171
+ ```
172
+
173
+ ## Development
174
+
175
+ ```bash
176
+ # Clone and install
177
+ git clone https://github.com/user/prezo.git
178
+ cd prezo
179
+ uv sync
180
+
181
+ # Run
182
+ uv run prezo presentation.md
183
+
184
+ # Run tests
185
+ uv run pytest
186
+
187
+ # Lint
188
+ uv run ruff check .
189
+ uv run ruff format .
190
+ ```
191
+
192
+ ## License
193
+
194
+ MIT
@@ -0,0 +1,32 @@
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
5
+ prezo/images/__init__.py,sha256=xrWSR3z0SXYpLtjIvR2VOMxiJGkxEsls5-EOs9GecFA,324
6
+ prezo/images/ascii.py,sha256=lBN6LT2f3wd65lqp5HviByxIvf_w0bcByE_uAkV6ZaY,7342
7
+ prezo/images/base.py,sha256=STuS57AVSJ2lzwyn0QrIceGaSd2IWEiLGN-elT3u3AM,2749
8
+ prezo/images/chafa.py,sha256=rqqctIw5xQarEYz5SR-2a5ePJ3xbm0a3NWiLwxNBEUE,3726
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
12
+ prezo/images/processor.py,sha256=zMcfwltecup_AX2FhUIlPdO7c87a9jw7P9tLTIkr54U,4069
13
+ prezo/images/sixel.py,sha256=6TL8WllZfH-rn4LDH2iigoTygyxY_jg80A2jUNvl8W0,5484
14
+ prezo/parser.py,sha256=bD2MecHm7EssHd5LB2Bw6JuUqbjWPztWUu2meYwsyIQ,14793
15
+ prezo/screens/__init__.py,sha256=xHG9jNJz4vi1tpneSEVlD0D9I0M2U4gAGk6-R9xbUf4,508
16
+ prezo/screens/base.py,sha256=2n6Uj8evfIbcpn4AVYNG5iM_k7rIJ3Vwmor_xrQPU9E,2057
17
+ prezo/screens/blackout.py,sha256=wPSdD9lgu8ykAIQjU1OesnmjQoQEn9BdC4TEpABYxW4,1640
18
+ prezo/screens/goto.py,sha256=l9q6RAU8GX8WIALvbaPE3rcszrYWsJob8lGIDvUaWFM,2687
19
+ prezo/screens/help.py,sha256=fjwHp9qPMmyRIaME-Bcz-g6bn8UrtbL_Dk269QSU-zs,2987
20
+ prezo/screens/overview.py,sha256=s9-ifbcnXYhbxb_Kl2UhpB3IE7msInX6LWB-J1dazLo,5382
21
+ prezo/screens/search.py,sha256=3YG9WLGEIKW3YHpM0K1lgwhuqBveXd8ZoQZ178_zGd4,7809
22
+ prezo/screens/toc.py,sha256=8WYb5nbgP9agY-hUTATxLU4X1uka_bc2MN86hFW4aRg,8241
23
+ prezo/terminal.py,sha256=1eMUFDpsnjlAkvBAjyPoMMuN7GxpvY94je99F47KxF8,3632
24
+ prezo/themes.py,sha256=3keUgheOsNGjS0uCjRv7az9sVSnrz5tc-jZ58YNB7tg,3070
25
+ prezo/widgets/__init__.py,sha256=UeTHBgPDvqTkK5tTsPXhdJXP3qZefnltKtUtvJBx9m0,295
26
+ prezo/widgets/image_display.py,sha256=c3buM6NiM3oCcS2HWsb0HMeumsksS-U9PQnfl1EViEc,3349
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,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: uv 0.8.24
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ prezo = prezo:main
3
+