figrecipe 0.5.0__py3-none-any.whl → 0.7.4__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.
Files changed (189) hide show
  1. figrecipe/__init__.py +220 -819
  2. figrecipe/_api/__init__.py +48 -0
  3. figrecipe/_api/_extract.py +108 -0
  4. figrecipe/_api/_notebook.py +61 -0
  5. figrecipe/_api/_panel.py +46 -0
  6. figrecipe/_api/_save.py +191 -0
  7. figrecipe/_api/_seaborn_proxy.py +34 -0
  8. figrecipe/_api/_style_manager.py +153 -0
  9. figrecipe/_api/_subplots.py +333 -0
  10. figrecipe/_api/_validate.py +82 -0
  11. figrecipe/_dev/__init__.py +29 -0
  12. figrecipe/_dev/_plotters.py +76 -0
  13. figrecipe/_dev/_run_demos.py +56 -0
  14. figrecipe/_dev/demo_plotters/__init__.py +64 -0
  15. figrecipe/_dev/demo_plotters/_categories.py +81 -0
  16. figrecipe/_dev/demo_plotters/_figure_creators.py +119 -0
  17. figrecipe/_dev/demo_plotters/_helpers.py +31 -0
  18. figrecipe/_dev/demo_plotters/_registry.py +50 -0
  19. figrecipe/_dev/demo_plotters/bar_categorical/__init__.py +4 -0
  20. figrecipe/_dev/demo_plotters/bar_categorical/plot_bar.py +25 -0
  21. figrecipe/_dev/demo_plotters/bar_categorical/plot_barh.py +25 -0
  22. figrecipe/_dev/demo_plotters/contour_surface/__init__.py +4 -0
  23. figrecipe/_dev/demo_plotters/contour_surface/plot_contour.py +30 -0
  24. figrecipe/_dev/demo_plotters/contour_surface/plot_contourf.py +29 -0
  25. figrecipe/_dev/demo_plotters/contour_surface/plot_tricontour.py +28 -0
  26. figrecipe/_dev/demo_plotters/contour_surface/plot_tricontourf.py +28 -0
  27. figrecipe/_dev/demo_plotters/contour_surface/plot_tripcolor.py +29 -0
  28. figrecipe/_dev/demo_plotters/contour_surface/plot_triplot.py +25 -0
  29. figrecipe/_dev/demo_plotters/distribution/__init__.py +4 -0
  30. figrecipe/_dev/demo_plotters/distribution/plot_boxplot.py +24 -0
  31. figrecipe/_dev/demo_plotters/distribution/plot_ecdf.py +24 -0
  32. figrecipe/_dev/demo_plotters/distribution/plot_hist.py +24 -0
  33. figrecipe/_dev/demo_plotters/distribution/plot_hist2d.py +25 -0
  34. figrecipe/_dev/demo_plotters/distribution/plot_violinplot.py +25 -0
  35. figrecipe/_dev/demo_plotters/image_matrix/__init__.py +4 -0
  36. figrecipe/_dev/demo_plotters/image_matrix/plot_hexbin.py +25 -0
  37. figrecipe/_dev/demo_plotters/image_matrix/plot_imshow.py +23 -0
  38. figrecipe/_dev/demo_plotters/image_matrix/plot_matshow.py +23 -0
  39. figrecipe/_dev/demo_plotters/image_matrix/plot_pcolor.py +29 -0
  40. figrecipe/_dev/demo_plotters/image_matrix/plot_pcolormesh.py +29 -0
  41. figrecipe/_dev/demo_plotters/image_matrix/plot_spy.py +29 -0
  42. figrecipe/_dev/demo_plotters/line_curve/__init__.py +4 -0
  43. figrecipe/_dev/demo_plotters/line_curve/plot_errorbar.py +28 -0
  44. figrecipe/_dev/demo_plotters/line_curve/plot_fill.py +29 -0
  45. figrecipe/_dev/demo_plotters/line_curve/plot_fill_between.py +30 -0
  46. figrecipe/_dev/demo_plotters/line_curve/plot_fill_betweenx.py +28 -0
  47. figrecipe/_dev/demo_plotters/line_curve/plot_plot.py +28 -0
  48. figrecipe/_dev/demo_plotters/line_curve/plot_stackplot.py +29 -0
  49. figrecipe/_dev/demo_plotters/line_curve/plot_stairs.py +27 -0
  50. figrecipe/_dev/demo_plotters/line_curve/plot_step.py +27 -0
  51. figrecipe/_dev/demo_plotters/scatter_points/__init__.py +4 -0
  52. figrecipe/_dev/demo_plotters/scatter_points/plot_scatter.py +24 -0
  53. figrecipe/_dev/demo_plotters/special/__init__.py +4 -0
  54. figrecipe/_dev/demo_plotters/special/plot_eventplot.py +25 -0
  55. figrecipe/_dev/demo_plotters/special/plot_loglog.py +27 -0
  56. figrecipe/_dev/demo_plotters/special/plot_pie.py +27 -0
  57. figrecipe/_dev/demo_plotters/special/plot_semilogx.py +27 -0
  58. figrecipe/_dev/demo_plotters/special/plot_semilogy.py +27 -0
  59. figrecipe/_dev/demo_plotters/special/plot_stem.py +27 -0
  60. figrecipe/_dev/demo_plotters/spectral_signal/__init__.py +4 -0
  61. figrecipe/_dev/demo_plotters/spectral_signal/plot_acorr.py +24 -0
  62. figrecipe/_dev/demo_plotters/spectral_signal/plot_angle_spectrum.py +28 -0
  63. figrecipe/_dev/demo_plotters/spectral_signal/plot_cohere.py +29 -0
  64. figrecipe/_dev/demo_plotters/spectral_signal/plot_csd.py +29 -0
  65. figrecipe/_dev/demo_plotters/spectral_signal/plot_magnitude_spectrum.py +28 -0
  66. figrecipe/_dev/demo_plotters/spectral_signal/plot_phase_spectrum.py +28 -0
  67. figrecipe/_dev/demo_plotters/spectral_signal/plot_psd.py +29 -0
  68. figrecipe/_dev/demo_plotters/spectral_signal/plot_specgram.py +30 -0
  69. figrecipe/_dev/demo_plotters/spectral_signal/plot_xcorr.py +25 -0
  70. figrecipe/_dev/demo_plotters/vector_flow/__init__.py +4 -0
  71. figrecipe/_dev/demo_plotters/vector_flow/plot_barbs.py +30 -0
  72. figrecipe/_dev/demo_plotters/vector_flow/plot_quiver.py +30 -0
  73. figrecipe/_dev/demo_plotters/vector_flow/plot_streamplot.py +30 -0
  74. figrecipe/_editor/__init__.py +278 -0
  75. figrecipe/_editor/_bbox/__init__.py +43 -0
  76. figrecipe/_editor/_bbox/_collections.py +177 -0
  77. figrecipe/_editor/_bbox/_elements.py +159 -0
  78. figrecipe/_editor/_bbox/_extract.py +256 -0
  79. figrecipe/_editor/_bbox/_extract_axes.py +370 -0
  80. figrecipe/_editor/_bbox/_extract_text.py +342 -0
  81. figrecipe/_editor/_bbox/_lines.py +173 -0
  82. figrecipe/_editor/_bbox/_transforms.py +146 -0
  83. figrecipe/_editor/_flask_app.py +258 -0
  84. figrecipe/_editor/_helpers.py +242 -0
  85. figrecipe/_editor/_hitmap/__init__.py +76 -0
  86. figrecipe/_editor/_hitmap/_artists/__init__.py +21 -0
  87. figrecipe/_editor/_hitmap/_artists/_collections.py +345 -0
  88. figrecipe/_editor/_hitmap/_artists/_images.py +68 -0
  89. figrecipe/_editor/_hitmap/_artists/_lines.py +107 -0
  90. figrecipe/_editor/_hitmap/_artists/_patches.py +163 -0
  91. figrecipe/_editor/_hitmap/_artists/_text.py +190 -0
  92. figrecipe/_editor/_hitmap/_colors.py +181 -0
  93. figrecipe/_editor/_hitmap/_detect.py +137 -0
  94. figrecipe/_editor/_hitmap/_restore.py +154 -0
  95. figrecipe/_editor/_hitmap_main.py +182 -0
  96. figrecipe/_editor/_overrides.py +318 -0
  97. figrecipe/_editor/_preferences.py +135 -0
  98. figrecipe/_editor/_render_overrides.py +480 -0
  99. figrecipe/_editor/_renderer.py +199 -0
  100. figrecipe/_editor/_routes_axis.py +453 -0
  101. figrecipe/_editor/_routes_core.py +284 -0
  102. figrecipe/_editor/_routes_element.py +317 -0
  103. figrecipe/_editor/_routes_style.py +223 -0
  104. figrecipe/_editor/_templates/__init__.py +152 -0
  105. figrecipe/_editor/_templates/_html.py +502 -0
  106. figrecipe/_editor/_templates/_scripts/__init__.py +120 -0
  107. figrecipe/_editor/_templates/_scripts/_api.py +228 -0
  108. figrecipe/_editor/_templates/_scripts/_colors.py +485 -0
  109. figrecipe/_editor/_templates/_scripts/_core.py +436 -0
  110. figrecipe/_editor/_templates/_scripts/_debug_snapshot.py +186 -0
  111. figrecipe/_editor/_templates/_scripts/_element_editor.py +310 -0
  112. figrecipe/_editor/_templates/_scripts/_files.py +195 -0
  113. figrecipe/_editor/_templates/_scripts/_hitmap.py +509 -0
  114. figrecipe/_editor/_templates/_scripts/_inspector.py +315 -0
  115. figrecipe/_editor/_templates/_scripts/_labels.py +464 -0
  116. figrecipe/_editor/_templates/_scripts/_legend_drag.py +265 -0
  117. figrecipe/_editor/_templates/_scripts/_modals.py +226 -0
  118. figrecipe/_editor/_templates/_scripts/_overlays.py +292 -0
  119. figrecipe/_editor/_templates/_scripts/_panel_drag.py +334 -0
  120. figrecipe/_editor/_templates/_scripts/_panel_position.py +279 -0
  121. figrecipe/_editor/_templates/_scripts/_selection.py +237 -0
  122. figrecipe/_editor/_templates/_scripts/_tabs.py +89 -0
  123. figrecipe/_editor/_templates/_scripts/_view_mode.py +107 -0
  124. figrecipe/_editor/_templates/_scripts/_zoom.py +179 -0
  125. figrecipe/_editor/_templates/_styles/__init__.py +69 -0
  126. figrecipe/_editor/_templates/_styles/_base.py +64 -0
  127. figrecipe/_editor/_templates/_styles/_buttons.py +206 -0
  128. figrecipe/_editor/_templates/_styles/_color_input.py +123 -0
  129. figrecipe/_editor/_templates/_styles/_controls.py +265 -0
  130. figrecipe/_editor/_templates/_styles/_dynamic_props.py +144 -0
  131. figrecipe/_editor/_templates/_styles/_forms.py +126 -0
  132. figrecipe/_editor/_templates/_styles/_hitmap.py +184 -0
  133. figrecipe/_editor/_templates/_styles/_inspector.py +90 -0
  134. figrecipe/_editor/_templates/_styles/_labels.py +118 -0
  135. figrecipe/_editor/_templates/_styles/_modals.py +98 -0
  136. figrecipe/_editor/_templates/_styles/_overlays.py +130 -0
  137. figrecipe/_editor/_templates/_styles/_preview.py +225 -0
  138. figrecipe/_editor/_templates/_styles/_selection.py +73 -0
  139. figrecipe/_params/_DECORATION_METHODS.py +33 -0
  140. figrecipe/_params/_PLOTTING_METHODS.py +58 -0
  141. figrecipe/_params/__init__.py +9 -0
  142. figrecipe/_recorder.py +92 -110
  143. figrecipe/_recorder_utils.py +124 -0
  144. figrecipe/_reproducer/__init__.py +18 -0
  145. figrecipe/_reproducer/_core.py +498 -0
  146. figrecipe/_reproducer/_custom_plots.py +279 -0
  147. figrecipe/_reproducer/_seaborn.py +100 -0
  148. figrecipe/_reproducer/_violin.py +186 -0
  149. figrecipe/_seaborn.py +14 -9
  150. figrecipe/_serializer.py +2 -2
  151. figrecipe/_signatures/README.md +68 -0
  152. figrecipe/_signatures/__init__.py +12 -2
  153. figrecipe/_signatures/_kwargs.py +273 -0
  154. figrecipe/_signatures/_loader.py +114 -57
  155. figrecipe/_signatures/_parsing.py +147 -0
  156. figrecipe/_utils/__init__.py +6 -4
  157. figrecipe/_utils/_crop.py +10 -4
  158. figrecipe/_utils/_image_diff.py +37 -33
  159. figrecipe/_utils/_numpy_io.py +0 -1
  160. figrecipe/_utils/_units.py +11 -3
  161. figrecipe/_validator.py +12 -3
  162. figrecipe/_wrappers/_axes.py +193 -170
  163. figrecipe/_wrappers/_axes_helpers.py +136 -0
  164. figrecipe/_wrappers/_axes_plots.py +418 -0
  165. figrecipe/_wrappers/_axes_seaborn.py +157 -0
  166. figrecipe/_wrappers/_figure.py +277 -18
  167. figrecipe/_wrappers/_panel_labels.py +127 -0
  168. figrecipe/_wrappers/_plot_helpers.py +143 -0
  169. figrecipe/_wrappers/_violin_helpers.py +180 -0
  170. figrecipe/plt.py +0 -1
  171. figrecipe/pyplot.py +2 -1
  172. figrecipe/styles/__init__.py +12 -11
  173. figrecipe/styles/_dotdict.py +72 -0
  174. figrecipe/styles/_finalize.py +134 -0
  175. figrecipe/styles/_fonts.py +77 -0
  176. figrecipe/styles/_kwargs_converter.py +178 -0
  177. figrecipe/styles/_plot_styles.py +209 -0
  178. figrecipe/styles/_style_applier.py +60 -202
  179. figrecipe/styles/_style_loader.py +73 -121
  180. figrecipe/styles/_themes.py +151 -0
  181. figrecipe/styles/presets/MATPLOTLIB.yaml +95 -0
  182. figrecipe/styles/presets/SCITEX.yaml +181 -0
  183. figrecipe-0.7.4.dist-info/METADATA +429 -0
  184. figrecipe-0.7.4.dist-info/RECORD +188 -0
  185. figrecipe/_reproducer.py +0 -358
  186. figrecipe-0.5.0.dist-info/METADATA +0 -336
  187. figrecipe-0.5.0.dist-info/RECORD +0 -26
  188. {figrecipe-0.5.0.dist-info → figrecipe-0.7.4.dist-info}/WHEEL +0 -0
  189. {figrecipe-0.5.0.dist-info → figrecipe-0.7.4.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,318 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ Style override management with separation of base and manual styles.
5
+
6
+ This module handles the layered style system:
7
+ 1. Base style (from preset like SCITEX)
8
+ 2. Programmatic style (from code, e.g., fr.load_style())
9
+ 3. Manual overrides (from GUI editor)
10
+
11
+ Manual overrides are stored separately with timestamps, allowing:
12
+ - Restoration to original (programmatic) style
13
+ - Tracking when manual edits were made
14
+ - Comparison between original and edited versions
15
+ """
16
+
17
+ import json
18
+ from dataclasses import dataclass, field
19
+ from datetime import datetime
20
+ from pathlib import Path
21
+ from typing import Any, Dict, Optional
22
+
23
+
24
+ @dataclass
25
+ class StyleOverrides:
26
+ """
27
+ Manages layered style overrides with timestamp tracking.
28
+
29
+ Attributes
30
+ ----------
31
+ base_style : dict
32
+ Original style from preset (e.g., SCITEX values).
33
+ programmatic_style : dict
34
+ Style set programmatically via code.
35
+ manual_overrides : dict
36
+ Overrides from GUI editor.
37
+ call_overrides : dict
38
+ Call parameter overrides {call_id: {param: value}}.
39
+ base_timestamp : str
40
+ When base style was set (ISO format).
41
+ manual_timestamp : str
42
+ When manual overrides were last modified.
43
+ """
44
+
45
+ base_style: Dict[str, Any] = field(default_factory=dict)
46
+ programmatic_style: Dict[str, Any] = field(default_factory=dict)
47
+ manual_overrides: Dict[str, Any] = field(default_factory=dict)
48
+ call_overrides: Dict[str, Dict[str, Any]] = field(default_factory=dict)
49
+ base_timestamp: Optional[str] = None
50
+ manual_timestamp: Optional[str] = None
51
+
52
+ def get_effective_style(self) -> Dict[str, Any]:
53
+ """
54
+ Get the final effective style by merging all layers.
55
+
56
+ Priority: base < programmatic < manual
57
+
58
+ Returns
59
+ -------
60
+ dict
61
+ Merged style dictionary.
62
+ """
63
+ result = {}
64
+ result.update(self.base_style)
65
+ result.update(self.programmatic_style)
66
+ result.update(self.manual_overrides)
67
+ return result
68
+
69
+ def get_original_style(self) -> Dict[str, Any]:
70
+ """
71
+ Get style without manual overrides (for "Restore" function).
72
+
73
+ Returns
74
+ -------
75
+ dict
76
+ Style with only base + programmatic layers.
77
+ """
78
+ result = {}
79
+ result.update(self.base_style)
80
+ result.update(self.programmatic_style)
81
+ return result
82
+
83
+ def set_manual_override(self, key: str, value: Any) -> None:
84
+ """
85
+ Set a single manual override.
86
+
87
+ Parameters
88
+ ----------
89
+ key : str
90
+ Style property key (e.g., 'axes_width_mm').
91
+ value : Any
92
+ Property value.
93
+ """
94
+ self.manual_overrides[key] = value
95
+ self.manual_timestamp = datetime.now().isoformat()
96
+
97
+ def update_manual_overrides(self, overrides: Dict[str, Any]) -> None:
98
+ """
99
+ Update multiple manual overrides at once.
100
+
101
+ Parameters
102
+ ----------
103
+ overrides : dict
104
+ Dictionary of overrides to apply.
105
+ """
106
+ self.manual_overrides.update(overrides)
107
+ self.manual_timestamp = datetime.now().isoformat()
108
+
109
+ def clear_manual_overrides(self, clear_call_overrides: bool = True) -> None:
110
+ """Clear all manual overrides (restore to original).
111
+
112
+ Parameters
113
+ ----------
114
+ clear_call_overrides : bool
115
+ Also clear call parameter overrides (default True).
116
+ """
117
+ self.manual_overrides = {}
118
+ if clear_call_overrides:
119
+ self.call_overrides = {}
120
+ self.manual_timestamp = None
121
+
122
+ def has_manual_overrides(self) -> bool:
123
+ """Check if any manual overrides exist."""
124
+ return len(self.manual_overrides) > 0
125
+
126
+ def get_diff(self) -> Dict[str, Dict[str, Any]]:
127
+ """
128
+ Get differences between original and manual overrides.
129
+
130
+ Returns
131
+ -------
132
+ dict
133
+ Dictionary showing changes: {key: {'original': val, 'manual': val}}
134
+ """
135
+ original = self.get_original_style()
136
+ diff = {}
137
+
138
+ for key, manual_val in self.manual_overrides.items():
139
+ original_val = original.get(key)
140
+ if original_val != manual_val:
141
+ diff[key] = {
142
+ "original": original_val,
143
+ "manual": manual_val,
144
+ }
145
+
146
+ return diff
147
+
148
+ def set_call_override(self, call_id: str, param: str, value: Any) -> None:
149
+ """
150
+ Set a call parameter override.
151
+
152
+ Parameters
153
+ ----------
154
+ call_id : str
155
+ ID of the call (e.g., 'bp1', 'vp1').
156
+ param : str
157
+ Parameter name.
158
+ value : Any
159
+ Parameter value.
160
+ """
161
+ if call_id not in self.call_overrides:
162
+ self.call_overrides[call_id] = {}
163
+ self.call_overrides[call_id][param] = value
164
+ self.manual_timestamp = datetime.now().isoformat()
165
+
166
+ def get_call_overrides(self, call_id: str) -> Dict[str, Any]:
167
+ """Get all overrides for a specific call."""
168
+ return self.call_overrides.get(call_id, {})
169
+
170
+ def has_call_overrides(self) -> bool:
171
+ """Check if any call overrides exist."""
172
+ return len(self.call_overrides) > 0
173
+
174
+ def to_dict(self) -> Dict[str, Any]:
175
+ """Convert to dictionary for serialization."""
176
+ return {
177
+ "base_style": self.base_style,
178
+ "programmatic_style": self.programmatic_style,
179
+ "manual_overrides": self.manual_overrides,
180
+ "call_overrides": self.call_overrides,
181
+ "base_timestamp": self.base_timestamp,
182
+ "manual_timestamp": self.manual_timestamp,
183
+ }
184
+
185
+ @classmethod
186
+ def from_dict(cls, data: Dict[str, Any]) -> "StyleOverrides":
187
+ """Create from dictionary."""
188
+ return cls(
189
+ base_style=data.get("base_style", {}),
190
+ programmatic_style=data.get("programmatic_style", {}),
191
+ manual_overrides=data.get("manual_overrides", {}),
192
+ call_overrides=data.get("call_overrides", {}),
193
+ base_timestamp=data.get("base_timestamp"),
194
+ manual_timestamp=data.get("manual_timestamp"),
195
+ )
196
+
197
+
198
+ def get_overrides_path(recipe_path: Path) -> Path:
199
+ """
200
+ Get the path for storing overrides alongside a recipe.
201
+
202
+ Parameters
203
+ ----------
204
+ recipe_path : Path
205
+ Path to the recipe file.
206
+
207
+ Returns
208
+ -------
209
+ Path
210
+ Path to the overrides file.
211
+
212
+ Examples
213
+ --------
214
+ >>> get_overrides_path(Path('figure.yaml'))
215
+ Path('figure.overrides.json')
216
+ """
217
+ return recipe_path.with_suffix(".overrides.json")
218
+
219
+
220
+ def save_overrides(
221
+ overrides: StyleOverrides,
222
+ path: Path,
223
+ ) -> Path:
224
+ """
225
+ Save style overrides to JSON file.
226
+
227
+ Parameters
228
+ ----------
229
+ overrides : StyleOverrides
230
+ Overrides to save.
231
+ path : Path
232
+ Path to save to (or recipe path to derive from).
233
+
234
+ Returns
235
+ -------
236
+ Path
237
+ Path where overrides were saved.
238
+ """
239
+ # If path is a recipe, derive overrides path
240
+ if path.suffix in (".yaml", ".yml"):
241
+ path = get_overrides_path(path)
242
+
243
+ data = overrides.to_dict()
244
+ data["_version"] = "1.0"
245
+ data["_saved_at"] = datetime.now().isoformat()
246
+
247
+ with open(path, "w") as f:
248
+ json.dump(data, f, indent=2)
249
+
250
+ return path
251
+
252
+
253
+ def load_overrides(path: Path) -> Optional[StyleOverrides]:
254
+ """
255
+ Load style overrides from JSON file.
256
+
257
+ Parameters
258
+ ----------
259
+ path : Path
260
+ Path to overrides file or recipe file.
261
+
262
+ Returns
263
+ -------
264
+ StyleOverrides or None
265
+ Loaded overrides, or None if file doesn't exist.
266
+ """
267
+ # If path is a recipe, derive overrides path
268
+ if path.suffix in (".yaml", ".yml"):
269
+ path = get_overrides_path(path)
270
+
271
+ if not path.exists():
272
+ return None
273
+
274
+ with open(path) as f:
275
+ data = json.load(f)
276
+
277
+ # Remove metadata
278
+ data.pop("_version", None)
279
+ data.pop("_saved_at", None)
280
+
281
+ return StyleOverrides.from_dict(data)
282
+
283
+
284
+ def create_overrides_from_style(
285
+ base_style: Optional[Dict[str, Any]] = None,
286
+ programmatic_style: Optional[Dict[str, Any]] = None,
287
+ ) -> StyleOverrides:
288
+ """
289
+ Create StyleOverrides from existing style configuration.
290
+
291
+ Parameters
292
+ ----------
293
+ base_style : dict, optional
294
+ Base style from preset.
295
+ programmatic_style : dict, optional
296
+ Style set via code.
297
+
298
+ Returns
299
+ -------
300
+ StyleOverrides
301
+ New overrides object.
302
+ """
303
+ return StyleOverrides(
304
+ base_style=base_style or {},
305
+ programmatic_style=programmatic_style or {},
306
+ manual_overrides={},
307
+ base_timestamp=datetime.now().isoformat(),
308
+ manual_timestamp=None,
309
+ )
310
+
311
+
312
+ __all__ = [
313
+ "StyleOverrides",
314
+ "get_overrides_path",
315
+ "save_overrides",
316
+ "load_overrides",
317
+ "create_overrides_from_style",
318
+ ]
@@ -0,0 +1,135 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """User preferences management for the figure editor.
4
+
5
+ Preferences are stored in ~/.figrecipe/preferences.json and persist
6
+ across sessions. This allows users to set defaults like dark mode.
7
+ """
8
+
9
+ import json
10
+ from pathlib import Path
11
+ from typing import Any, Dict
12
+
13
+ # Default preferences
14
+ DEFAULT_PREFERENCES = {
15
+ "dark_mode": False,
16
+ "default_style": "SCITEX",
17
+ "auto_save": True,
18
+ "show_hit_regions": False,
19
+ }
20
+
21
+ # Preferences file location
22
+ PREFERENCES_DIR = Path.home() / ".figrecipe"
23
+ PREFERENCES_FILE = PREFERENCES_DIR / "preferences.json"
24
+
25
+
26
+ def get_preferences_path() -> Path:
27
+ """Get the path to the preferences file."""
28
+ return PREFERENCES_FILE
29
+
30
+
31
+ def load_preferences() -> Dict[str, Any]:
32
+ """Load user preferences from disk.
33
+
34
+ Returns
35
+ -------
36
+ dict
37
+ User preferences merged with defaults.
38
+ """
39
+ prefs = DEFAULT_PREFERENCES.copy()
40
+
41
+ if PREFERENCES_FILE.exists():
42
+ try:
43
+ with open(PREFERENCES_FILE, "r") as f:
44
+ saved = json.load(f)
45
+ prefs.update(saved)
46
+ except (json.JSONDecodeError, IOError):
47
+ # If file is corrupted, use defaults
48
+ pass
49
+
50
+ return prefs
51
+
52
+
53
+ def save_preferences(prefs: Dict[str, Any]) -> bool:
54
+ """Save user preferences to disk.
55
+
56
+ Parameters
57
+ ----------
58
+ prefs : dict
59
+ Preferences to save.
60
+
61
+ Returns
62
+ -------
63
+ bool
64
+ True if save was successful.
65
+ """
66
+ try:
67
+ PREFERENCES_DIR.mkdir(parents=True, exist_ok=True)
68
+ with open(PREFERENCES_FILE, "w") as f:
69
+ json.dump(prefs, f, indent=2)
70
+ return True
71
+ except IOError:
72
+ return False
73
+
74
+
75
+ def get_preference(key: str, default: Any = None) -> Any:
76
+ """Get a single preference value.
77
+
78
+ Parameters
79
+ ----------
80
+ key : str
81
+ Preference key.
82
+ default : Any, optional
83
+ Default value if key not found.
84
+
85
+ Returns
86
+ -------
87
+ Any
88
+ Preference value.
89
+ """
90
+ prefs = load_preferences()
91
+ return prefs.get(key, default)
92
+
93
+
94
+ def set_preference(key: str, value: Any) -> bool:
95
+ """Set a single preference value.
96
+
97
+ Parameters
98
+ ----------
99
+ key : str
100
+ Preference key.
101
+ value : Any
102
+ Value to set.
103
+
104
+ Returns
105
+ -------
106
+ bool
107
+ True if save was successful.
108
+ """
109
+ prefs = load_preferences()
110
+ prefs[key] = value
111
+ return save_preferences(prefs)
112
+
113
+
114
+ def reset_preferences() -> bool:
115
+ """Reset all preferences to defaults.
116
+
117
+ Returns
118
+ -------
119
+ bool
120
+ True if reset was successful.
121
+ """
122
+ return save_preferences(DEFAULT_PREFERENCES.copy())
123
+
124
+
125
+ __all__ = [
126
+ "DEFAULT_PREFERENCES",
127
+ "get_preferences_path",
128
+ "load_preferences",
129
+ "save_preferences",
130
+ "get_preference",
131
+ "set_preference",
132
+ "reset_preferences",
133
+ ]
134
+
135
+ # EOF