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/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