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.
- prezo/__init__.py +216 -0
- prezo/app.py +947 -0
- prezo/config.py +247 -0
- prezo/export.py +833 -0
- prezo/images/__init__.py +14 -0
- prezo/images/ascii.py +240 -0
- prezo/images/base.py +111 -0
- prezo/images/chafa.py +137 -0
- prezo/images/iterm.py +126 -0
- prezo/images/kitty.py +360 -0
- prezo/images/overlay.py +291 -0
- prezo/images/processor.py +139 -0
- prezo/images/sixel.py +180 -0
- prezo/parser.py +456 -0
- prezo/screens/__init__.py +21 -0
- prezo/screens/base.py +65 -0
- prezo/screens/blackout.py +60 -0
- prezo/screens/goto.py +99 -0
- prezo/screens/help.py +140 -0
- prezo/screens/overview.py +184 -0
- prezo/screens/search.py +252 -0
- prezo/screens/toc.py +254 -0
- prezo/terminal.py +147 -0
- prezo/themes.py +129 -0
- prezo/widgets/__init__.py +9 -0
- prezo/widgets/image_display.py +117 -0
- prezo/widgets/slide_button.py +72 -0
- prezo/widgets/status_bar.py +240 -0
- prezo-0.3.1.dist-info/METADATA +194 -0
- prezo-0.3.1.dist-info/RECORD +32 -0
- prezo-0.3.1.dist-info/WHEEL +4 -0
- prezo-0.3.1.dist-info/entry_points.txt +3 -0
|
@@ -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
|
+
[](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,,
|