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
prezo/config.py
ADDED
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
"""Configuration management for Prezo."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
# Try to import tomllib (Python 3.11+) or tomli as fallback
|
|
11
|
+
try:
|
|
12
|
+
import tomllib
|
|
13
|
+
except ImportError:
|
|
14
|
+
import tomli as tomllib # type: ignore[import-not-found,no-redef]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
CONFIG_DIR = Path.home() / ".config" / "prezo"
|
|
18
|
+
CONFIG_FILE = CONFIG_DIR / "config.toml"
|
|
19
|
+
STATE_FILE = CONFIG_DIR / "state.json"
|
|
20
|
+
|
|
21
|
+
DEFAULT_CONFIG_TOML = """\
|
|
22
|
+
# Prezo Configuration
|
|
23
|
+
# See https://github.com/user/prezo for documentation
|
|
24
|
+
|
|
25
|
+
[display]
|
|
26
|
+
theme = "dark" # dark, light, dracula, solarized-dark, nord, gruvbox
|
|
27
|
+
# syntax_theme = "monokai" # Code block highlighting (future)
|
|
28
|
+
|
|
29
|
+
[timer]
|
|
30
|
+
show_clock = true
|
|
31
|
+
show_elapsed = true
|
|
32
|
+
countdown_minutes = 0 # 0 = disabled
|
|
33
|
+
|
|
34
|
+
[behavior]
|
|
35
|
+
auto_reload = true
|
|
36
|
+
reload_interval = 1.0 # seconds
|
|
37
|
+
|
|
38
|
+
[export]
|
|
39
|
+
default_theme = "light"
|
|
40
|
+
default_size = "100x30"
|
|
41
|
+
chrome = true
|
|
42
|
+
|
|
43
|
+
[images]
|
|
44
|
+
mode = "auto" # auto, kitty, sixel, iterm, ascii, none
|
|
45
|
+
ascii_width = 60
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass
|
|
50
|
+
class DisplayConfig:
|
|
51
|
+
"""Display configuration."""
|
|
52
|
+
|
|
53
|
+
theme: str = "dark"
|
|
54
|
+
syntax_theme: str = "monokai"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@dataclass
|
|
58
|
+
class TimerConfig:
|
|
59
|
+
"""Timer configuration."""
|
|
60
|
+
|
|
61
|
+
show_clock: bool = True
|
|
62
|
+
show_elapsed: bool = True
|
|
63
|
+
countdown_minutes: int = 0
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@dataclass
|
|
67
|
+
class BehaviorConfig:
|
|
68
|
+
"""Behavior configuration."""
|
|
69
|
+
|
|
70
|
+
auto_reload: bool = True
|
|
71
|
+
reload_interval: float = 1.0
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@dataclass
|
|
75
|
+
class ExportConfig:
|
|
76
|
+
"""Export configuration."""
|
|
77
|
+
|
|
78
|
+
default_theme: str = "light"
|
|
79
|
+
default_size: str = "100x30"
|
|
80
|
+
chrome: bool = True
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@dataclass
|
|
84
|
+
class ImageConfig:
|
|
85
|
+
"""Image rendering configuration."""
|
|
86
|
+
|
|
87
|
+
mode: str = "auto" # auto, kitty, sixel, iterm, ascii, none
|
|
88
|
+
ascii_width: int = 60
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@dataclass
|
|
92
|
+
class Config:
|
|
93
|
+
"""Prezo configuration."""
|
|
94
|
+
|
|
95
|
+
display: DisplayConfig = field(default_factory=DisplayConfig)
|
|
96
|
+
timer: TimerConfig = field(default_factory=TimerConfig)
|
|
97
|
+
behavior: BehaviorConfig = field(default_factory=BehaviorConfig)
|
|
98
|
+
export: ExportConfig = field(default_factory=ExportConfig)
|
|
99
|
+
images: ImageConfig = field(default_factory=ImageConfig)
|
|
100
|
+
|
|
101
|
+
@classmethod
|
|
102
|
+
def from_dict(cls, data: dict[str, Any]) -> Config:
|
|
103
|
+
"""Create config from dictionary."""
|
|
104
|
+
return cls(
|
|
105
|
+
display=DisplayConfig(**data.get("display", {})),
|
|
106
|
+
timer=TimerConfig(**data.get("timer", {})),
|
|
107
|
+
behavior=BehaviorConfig(**data.get("behavior", {})),
|
|
108
|
+
export=ExportConfig(**data.get("export", {})),
|
|
109
|
+
images=ImageConfig(**data.get("images", {})),
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
def update_from_dict(self, data: dict[str, Any]) -> None:
|
|
113
|
+
"""Update config from dictionary (partial update)."""
|
|
114
|
+
if "display" in data:
|
|
115
|
+
for key, value in data["display"].items():
|
|
116
|
+
if hasattr(self.display, key):
|
|
117
|
+
setattr(self.display, key, value)
|
|
118
|
+
if "timer" in data:
|
|
119
|
+
for key, value in data["timer"].items():
|
|
120
|
+
if hasattr(self.timer, key):
|
|
121
|
+
setattr(self.timer, key, value)
|
|
122
|
+
if "behavior" in data:
|
|
123
|
+
for key, value in data["behavior"].items():
|
|
124
|
+
if hasattr(self.behavior, key):
|
|
125
|
+
setattr(self.behavior, key, value)
|
|
126
|
+
if "export" in data:
|
|
127
|
+
for key, value in data["export"].items():
|
|
128
|
+
if hasattr(self.export, key):
|
|
129
|
+
setattr(self.export, key, value)
|
|
130
|
+
if "images" in data:
|
|
131
|
+
for key, value in data["images"].items():
|
|
132
|
+
if hasattr(self.images, key):
|
|
133
|
+
setattr(self.images, key, value)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
@dataclass
|
|
137
|
+
class AppState:
|
|
138
|
+
"""Persistent application state."""
|
|
139
|
+
|
|
140
|
+
recent_files: list[str] = field(default_factory=list)
|
|
141
|
+
last_positions: dict[str, int] = field(default_factory=dict)
|
|
142
|
+
|
|
143
|
+
def add_recent_file(self, path: str, max_files: int = 20) -> None:
|
|
144
|
+
"""Add a file to recent files list."""
|
|
145
|
+
# Remove if already exists
|
|
146
|
+
if path in self.recent_files:
|
|
147
|
+
self.recent_files.remove(path)
|
|
148
|
+
# Add to front
|
|
149
|
+
self.recent_files.insert(0, path)
|
|
150
|
+
# Trim to max
|
|
151
|
+
self.recent_files = self.recent_files[:max_files]
|
|
152
|
+
|
|
153
|
+
def set_position(self, path: str, position: int) -> None:
|
|
154
|
+
"""Save last position for a file."""
|
|
155
|
+
self.last_positions[path] = position
|
|
156
|
+
|
|
157
|
+
def get_position(self, path: str) -> int:
|
|
158
|
+
"""Get last position for a file."""
|
|
159
|
+
return self.last_positions.get(path, 0)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def ensure_config_dir() -> None:
|
|
163
|
+
"""Ensure config directory exists."""
|
|
164
|
+
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def load_config(config_path: Path | None = None) -> Config:
|
|
168
|
+
"""Load configuration from file.
|
|
169
|
+
|
|
170
|
+
Args:
|
|
171
|
+
config_path: Optional custom config path. Uses default if None.
|
|
172
|
+
|
|
173
|
+
Returns:
|
|
174
|
+
Loaded configuration with defaults for missing values.
|
|
175
|
+
|
|
176
|
+
"""
|
|
177
|
+
config = Config()
|
|
178
|
+
path = config_path or CONFIG_FILE
|
|
179
|
+
|
|
180
|
+
if path.exists():
|
|
181
|
+
try:
|
|
182
|
+
with open(path, "rb") as f:
|
|
183
|
+
data = tomllib.load(f)
|
|
184
|
+
config.update_from_dict(data)
|
|
185
|
+
except Exception:
|
|
186
|
+
# If config is invalid, use defaults
|
|
187
|
+
pass
|
|
188
|
+
|
|
189
|
+
return config
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def save_default_config() -> Path:
|
|
193
|
+
"""Save default configuration file.
|
|
194
|
+
|
|
195
|
+
Returns:
|
|
196
|
+
Path to the saved config file.
|
|
197
|
+
|
|
198
|
+
"""
|
|
199
|
+
ensure_config_dir()
|
|
200
|
+
if not CONFIG_FILE.exists():
|
|
201
|
+
CONFIG_FILE.write_text(DEFAULT_CONFIG_TOML)
|
|
202
|
+
return CONFIG_FILE
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def load_state() -> AppState:
|
|
206
|
+
"""Load application state from file."""
|
|
207
|
+
if STATE_FILE.exists():
|
|
208
|
+
try:
|
|
209
|
+
data = json.loads(STATE_FILE.read_text())
|
|
210
|
+
return AppState(
|
|
211
|
+
recent_files=data.get("recent_files", []),
|
|
212
|
+
last_positions=data.get("last_positions", {}),
|
|
213
|
+
)
|
|
214
|
+
except Exception:
|
|
215
|
+
pass
|
|
216
|
+
return AppState()
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def save_state(state: AppState) -> None:
|
|
220
|
+
"""Save application state to file."""
|
|
221
|
+
ensure_config_dir()
|
|
222
|
+
data = {
|
|
223
|
+
"recent_files": state.recent_files,
|
|
224
|
+
"last_positions": state.last_positions,
|
|
225
|
+
}
|
|
226
|
+
STATE_FILE.write_text(json.dumps(data, indent=2))
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
# Global config instance (loaded on first access)
|
|
230
|
+
_config: Config | None = None
|
|
231
|
+
_state: AppState | None = None
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def get_config() -> Config:
|
|
235
|
+
"""Get the global configuration instance."""
|
|
236
|
+
global _config
|
|
237
|
+
if _config is None:
|
|
238
|
+
_config = load_config()
|
|
239
|
+
return _config
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def get_state() -> AppState:
|
|
243
|
+
"""Get the global state instance."""
|
|
244
|
+
global _state
|
|
245
|
+
if _state is None:
|
|
246
|
+
_state = load_state()
|
|
247
|
+
return _state
|