setiastrosuitepro 1.6.0__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.

Potentially problematic release.


This version of setiastrosuitepro might be problematic. Click here for more details.

Files changed (174) hide show
  1. setiastro/__init__.py +2 -0
  2. setiastro/saspro/__init__.py +20 -0
  3. setiastro/saspro/__main__.py +784 -0
  4. setiastro/saspro/_generated/__init__.py +7 -0
  5. setiastro/saspro/_generated/build_info.py +2 -0
  6. setiastro/saspro/abe.py +1295 -0
  7. setiastro/saspro/abe_preset.py +196 -0
  8. setiastro/saspro/aberration_ai.py +694 -0
  9. setiastro/saspro/aberration_ai_preset.py +224 -0
  10. setiastro/saspro/accel_installer.py +218 -0
  11. setiastro/saspro/accel_workers.py +30 -0
  12. setiastro/saspro/add_stars.py +621 -0
  13. setiastro/saspro/astrobin_exporter.py +1007 -0
  14. setiastro/saspro/astrospike.py +153 -0
  15. setiastro/saspro/astrospike_python.py +1839 -0
  16. setiastro/saspro/autostretch.py +196 -0
  17. setiastro/saspro/backgroundneutral.py +560 -0
  18. setiastro/saspro/batch_convert.py +325 -0
  19. setiastro/saspro/batch_renamer.py +519 -0
  20. setiastro/saspro/blemish_blaster.py +488 -0
  21. setiastro/saspro/blink_comparator_pro.py +2923 -0
  22. setiastro/saspro/bundles.py +61 -0
  23. setiastro/saspro/bundles_dock.py +114 -0
  24. setiastro/saspro/cheat_sheet.py +168 -0
  25. setiastro/saspro/clahe.py +342 -0
  26. setiastro/saspro/comet_stacking.py +1377 -0
  27. setiastro/saspro/config.py +38 -0
  28. setiastro/saspro/config_bootstrap.py +40 -0
  29. setiastro/saspro/config_manager.py +316 -0
  30. setiastro/saspro/continuum_subtract.py +1617 -0
  31. setiastro/saspro/convo.py +1397 -0
  32. setiastro/saspro/convo_preset.py +414 -0
  33. setiastro/saspro/copyastro.py +187 -0
  34. setiastro/saspro/cosmicclarity.py +1564 -0
  35. setiastro/saspro/cosmicclarity_preset.py +407 -0
  36. setiastro/saspro/crop_dialog_pro.py +948 -0
  37. setiastro/saspro/crop_preset.py +189 -0
  38. setiastro/saspro/curve_editor_pro.py +2544 -0
  39. setiastro/saspro/curves_preset.py +375 -0
  40. setiastro/saspro/debayer.py +670 -0
  41. setiastro/saspro/debug_utils.py +29 -0
  42. setiastro/saspro/dnd_mime.py +35 -0
  43. setiastro/saspro/doc_manager.py +2634 -0
  44. setiastro/saspro/exoplanet_detector.py +2166 -0
  45. setiastro/saspro/file_utils.py +284 -0
  46. setiastro/saspro/fitsmodifier.py +744 -0
  47. setiastro/saspro/free_torch_memory.py +48 -0
  48. setiastro/saspro/frequency_separation.py +1343 -0
  49. setiastro/saspro/function_bundle.py +1594 -0
  50. setiastro/saspro/ghs_dialog_pro.py +660 -0
  51. setiastro/saspro/ghs_preset.py +284 -0
  52. setiastro/saspro/graxpert.py +634 -0
  53. setiastro/saspro/graxpert_preset.py +287 -0
  54. setiastro/saspro/gui/__init__.py +0 -0
  55. setiastro/saspro/gui/main_window.py +8494 -0
  56. setiastro/saspro/gui/mixins/__init__.py +33 -0
  57. setiastro/saspro/gui/mixins/dock_mixin.py +263 -0
  58. setiastro/saspro/gui/mixins/file_mixin.py +445 -0
  59. setiastro/saspro/gui/mixins/geometry_mixin.py +403 -0
  60. setiastro/saspro/gui/mixins/header_mixin.py +441 -0
  61. setiastro/saspro/gui/mixins/mask_mixin.py +421 -0
  62. setiastro/saspro/gui/mixins/menu_mixin.py +361 -0
  63. setiastro/saspro/gui/mixins/theme_mixin.py +367 -0
  64. setiastro/saspro/gui/mixins/toolbar_mixin.py +1324 -0
  65. setiastro/saspro/gui/mixins/update_mixin.py +309 -0
  66. setiastro/saspro/gui/mixins/view_mixin.py +435 -0
  67. setiastro/saspro/halobgon.py +462 -0
  68. setiastro/saspro/header_viewer.py +445 -0
  69. setiastro/saspro/headless_utils.py +88 -0
  70. setiastro/saspro/histogram.py +753 -0
  71. setiastro/saspro/history_explorer.py +939 -0
  72. setiastro/saspro/image_combine.py +414 -0
  73. setiastro/saspro/image_peeker_pro.py +1596 -0
  74. setiastro/saspro/imageops/__init__.py +37 -0
  75. setiastro/saspro/imageops/mdi_snap.py +292 -0
  76. setiastro/saspro/imageops/scnr.py +36 -0
  77. setiastro/saspro/imageops/starbasedwhitebalance.py +210 -0
  78. setiastro/saspro/imageops/stretch.py +244 -0
  79. setiastro/saspro/isophote.py +1179 -0
  80. setiastro/saspro/layers.py +208 -0
  81. setiastro/saspro/layers_dock.py +714 -0
  82. setiastro/saspro/lazy_imports.py +193 -0
  83. setiastro/saspro/legacy/__init__.py +2 -0
  84. setiastro/saspro/legacy/image_manager.py +2226 -0
  85. setiastro/saspro/legacy/numba_utils.py +3659 -0
  86. setiastro/saspro/legacy/xisf.py +1071 -0
  87. setiastro/saspro/linear_fit.py +534 -0
  88. setiastro/saspro/live_stacking.py +1830 -0
  89. setiastro/saspro/log_bus.py +5 -0
  90. setiastro/saspro/logging_config.py +460 -0
  91. setiastro/saspro/luminancerecombine.py +309 -0
  92. setiastro/saspro/main_helpers.py +201 -0
  93. setiastro/saspro/mask_creation.py +928 -0
  94. setiastro/saspro/masks_core.py +56 -0
  95. setiastro/saspro/mdi_widgets.py +353 -0
  96. setiastro/saspro/memory_utils.py +666 -0
  97. setiastro/saspro/metadata_patcher.py +75 -0
  98. setiastro/saspro/mfdeconv.py +3826 -0
  99. setiastro/saspro/mfdeconv_earlystop.py +71 -0
  100. setiastro/saspro/mfdeconvcudnn.py +3263 -0
  101. setiastro/saspro/mfdeconvsport.py +2382 -0
  102. setiastro/saspro/minorbodycatalog.py +567 -0
  103. setiastro/saspro/morphology.py +382 -0
  104. setiastro/saspro/multiscale_decomp.py +1290 -0
  105. setiastro/saspro/nbtorgb_stars.py +531 -0
  106. setiastro/saspro/numba_utils.py +3044 -0
  107. setiastro/saspro/numba_warmup.py +141 -0
  108. setiastro/saspro/ops/__init__.py +9 -0
  109. setiastro/saspro/ops/command_help_dialog.py +623 -0
  110. setiastro/saspro/ops/command_runner.py +217 -0
  111. setiastro/saspro/ops/commands.py +1594 -0
  112. setiastro/saspro/ops/script_editor.py +1102 -0
  113. setiastro/saspro/ops/scripts.py +1413 -0
  114. setiastro/saspro/ops/settings.py +560 -0
  115. setiastro/saspro/parallel_utils.py +554 -0
  116. setiastro/saspro/pedestal.py +121 -0
  117. setiastro/saspro/perfect_palette_picker.py +1053 -0
  118. setiastro/saspro/pipeline.py +110 -0
  119. setiastro/saspro/pixelmath.py +1600 -0
  120. setiastro/saspro/plate_solver.py +2435 -0
  121. setiastro/saspro/project_io.py +797 -0
  122. setiastro/saspro/psf_utils.py +136 -0
  123. setiastro/saspro/psf_viewer.py +549 -0
  124. setiastro/saspro/pyi_rthook_astroquery.py +95 -0
  125. setiastro/saspro/remove_green.py +314 -0
  126. setiastro/saspro/remove_stars.py +1625 -0
  127. setiastro/saspro/remove_stars_preset.py +404 -0
  128. setiastro/saspro/resources.py +472 -0
  129. setiastro/saspro/rgb_combination.py +207 -0
  130. setiastro/saspro/rgb_extract.py +19 -0
  131. setiastro/saspro/rgbalign.py +723 -0
  132. setiastro/saspro/runtime_imports.py +7 -0
  133. setiastro/saspro/runtime_torch.py +754 -0
  134. setiastro/saspro/save_options.py +72 -0
  135. setiastro/saspro/selective_color.py +1552 -0
  136. setiastro/saspro/sfcc.py +1425 -0
  137. setiastro/saspro/shortcuts.py +2807 -0
  138. setiastro/saspro/signature_insert.py +1099 -0
  139. setiastro/saspro/stacking_suite.py +17712 -0
  140. setiastro/saspro/star_alignment.py +7420 -0
  141. setiastro/saspro/star_alignment_preset.py +329 -0
  142. setiastro/saspro/star_metrics.py +49 -0
  143. setiastro/saspro/star_spikes.py +681 -0
  144. setiastro/saspro/star_stretch.py +470 -0
  145. setiastro/saspro/stat_stretch.py +502 -0
  146. setiastro/saspro/status_log_dock.py +78 -0
  147. setiastro/saspro/subwindow.py +3267 -0
  148. setiastro/saspro/supernovaasteroidhunter.py +1712 -0
  149. setiastro/saspro/swap_manager.py +99 -0
  150. setiastro/saspro/torch_backend.py +89 -0
  151. setiastro/saspro/torch_rejection.py +434 -0
  152. setiastro/saspro/view_bundle.py +1555 -0
  153. setiastro/saspro/wavescale_hdr.py +624 -0
  154. setiastro/saspro/wavescale_hdr_preset.py +100 -0
  155. setiastro/saspro/wavescalede.py +657 -0
  156. setiastro/saspro/wavescalede_preset.py +228 -0
  157. setiastro/saspro/wcs_update.py +374 -0
  158. setiastro/saspro/whitebalance.py +456 -0
  159. setiastro/saspro/widgets/__init__.py +48 -0
  160. setiastro/saspro/widgets/common_utilities.py +305 -0
  161. setiastro/saspro/widgets/graphics_views.py +122 -0
  162. setiastro/saspro/widgets/image_utils.py +518 -0
  163. setiastro/saspro/widgets/preview_dialogs.py +280 -0
  164. setiastro/saspro/widgets/spinboxes.py +275 -0
  165. setiastro/saspro/widgets/themed_buttons.py +13 -0
  166. setiastro/saspro/widgets/wavelet_utils.py +299 -0
  167. setiastro/saspro/window_shelf.py +185 -0
  168. setiastro/saspro/xisf.py +1123 -0
  169. setiastrosuitepro-1.6.0.dist-info/METADATA +266 -0
  170. setiastrosuitepro-1.6.0.dist-info/RECORD +174 -0
  171. setiastrosuitepro-1.6.0.dist-info/WHEEL +4 -0
  172. setiastrosuitepro-1.6.0.dist-info/entry_points.txt +6 -0
  173. setiastrosuitepro-1.6.0.dist-info/licenses/LICENSE +674 -0
  174. setiastrosuitepro-1.6.0.dist-info/licenses/license.txt +2580 -0
@@ -0,0 +1,38 @@
1
+ # pro/config.py
2
+ import os
3
+ import sys
4
+ from PyQt6.QtCore import QStandardPaths
5
+
6
+ class Config:
7
+ """Central configuration for Seti Astro Suite Pro."""
8
+
9
+ # GitHub Repos
10
+ GITHUB_ABERRATION_REPO = "riccardoalberghi/abberation_models"
11
+
12
+ # Paths
13
+ @staticmethod
14
+ def get_app_data_dir() -> str:
15
+ """Returns the application data directory."""
16
+ base = QStandardPaths.writableLocation(QStandardPaths.StandardLocation.AppDataLocation)
17
+ if not base:
18
+ base = os.path.expanduser("~/.local/share/SetiAstro")
19
+ return base
20
+
21
+ @staticmethod
22
+ def get_models_dir() -> str:
23
+ """Returns the directory for storing AI models."""
24
+ return os.path.join(Config.get_app_data_dir(), "Models")
25
+
26
+ @staticmethod
27
+ def get_aberration_models_dir() -> str:
28
+ """Returns the directory for Aberration AI models."""
29
+ return os.path.join(Config.get_models_dir(), "aberration_ai")
30
+
31
+ @staticmethod
32
+ def get_graxpert_default_path() -> str | None:
33
+ """Returns the default GraXpert executable path based on OS."""
34
+ if sys.platform == "darwin":
35
+ return "/Applications/GraXpert.app/Contents/MacOS/GraXpert"
36
+ elif sys.platform == "win32":
37
+ return "GraXpert.exe"
38
+ return None
@@ -0,0 +1,40 @@
1
+ """
2
+ Centralized matplotlib configuration bootstrap.
3
+
4
+ This module provides a single source of truth for setting up matplotlib's
5
+ configuration directory, used by both the main application entry point and
6
+ the GUI main window.
7
+ """
8
+ import os
9
+ import sys
10
+
11
+
12
+ def ensure_mpl_config_dir() -> str:
13
+ """
14
+ Make matplotlib use a known, writable folder.
15
+
16
+ Frozen (PyInstaller): <folder-with-exe>/mpl_config
17
+ Dev / IDE: <repo-folder>/mpl_config
18
+
19
+ This matches the pre-warm script that will build the font cache there.
20
+
21
+ Returns:
22
+ str: Path to the matplotlib configuration directory
23
+ """
24
+ if getattr(sys, "frozen", False):
25
+ base = os.path.dirname(sys.executable)
26
+ else:
27
+ base = os.path.dirname(os.path.abspath(__file__))
28
+ # We're in pro/ directory, go up one level to project root
29
+ base = os.path.dirname(base)
30
+
31
+ mpl_cfg = os.path.join(base, "mpl_config")
32
+ try:
33
+ os.makedirs(mpl_cfg, exist_ok=True)
34
+ except OSError:
35
+ # worst case: let matplotlib pick its default
36
+ return mpl_cfg
37
+
38
+ # only set if user / env didn't force something else
39
+ os.environ.setdefault("MPLCONFIGDIR", mpl_cfg)
40
+ return mpl_cfg
@@ -0,0 +1,316 @@
1
+ # pro/config_manager.py
2
+ """
3
+ Centralized configuration manager for Seti Astro Suite Pro.
4
+
5
+ Provides type-safe access to application settings with validation,
6
+ defaults, and change notifications.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ from typing import Any, Dict, Optional, TypeVar, Generic, Callable, List
11
+ from PyQt6.QtCore import QSettings, QObject, pyqtSignal
12
+ import json
13
+ import os
14
+
15
+ T = TypeVar('T')
16
+
17
+
18
+ class ConfigValue(Generic[T]):
19
+ """
20
+ Descriptor for a typed configuration value.
21
+
22
+ Usage:
23
+ class MyConfig(ConfigManager):
24
+ my_setting = ConfigValue("my_setting", default=10, type_=int)
25
+ """
26
+ def __init__(
27
+ self,
28
+ key: str,
29
+ default: T,
30
+ type_: type = str,
31
+ validator: Callable[[T], bool] | None = None,
32
+ description: str = ""
33
+ ):
34
+ self.key = key
35
+ self.default = default
36
+ self.type_ = type_
37
+ self.validator = validator
38
+ self.description = description
39
+
40
+ def __set_name__(self, owner, name):
41
+ self.name = name
42
+
43
+ def __get__(self, obj, objtype=None) -> T:
44
+ if obj is None:
45
+ return self # type: ignore
46
+ return obj.get(self.key, self.default, self.type_)
47
+
48
+ def __set__(self, obj, value: T):
49
+ if self.validator and not self.validator(value):
50
+ raise ValueError(f"Invalid value for {self.key}: {value}")
51
+ obj.set(self.key, value)
52
+
53
+
54
+ class ConfigManager(QObject):
55
+ """
56
+ Centralized configuration manager with type safety.
57
+
58
+ Wraps QSettings with:
59
+ - Type-safe get/set
60
+ - Default values
61
+ - Change notifications
62
+ - Validation
63
+
64
+ Usage:
65
+ config = ConfigManager.instance()
66
+ config.set("my_key", 42)
67
+ value = config.get("my_key", default=0, type_=int)
68
+
69
+ Or with descriptors:
70
+ class AppConfig(ConfigManager):
71
+ chunk_height = ConfigValue("stacking/chunk_height", default=512, type_=int)
72
+
73
+ config = AppConfig.instance()
74
+ print(config.chunk_height)
75
+ config.chunk_height = 1024
76
+ """
77
+
78
+ # Singleton instance
79
+ _instance: Optional['ConfigManager'] = None
80
+
81
+ # Signal emitted when any setting changes
82
+ settingChanged = pyqtSignal(str, object) # key, new_value
83
+
84
+ def __init__(self, organization: str = "SetiAstro", application: str = "SetiAstroSuitePro"):
85
+ super().__init__()
86
+ self._settings = QSettings(organization, application)
87
+ self._cache: Dict[str, Any] = {}
88
+ self._listeners: Dict[str, List[Callable]] = {}
89
+
90
+ @classmethod
91
+ def instance(cls) -> 'ConfigManager':
92
+ """Get the singleton instance."""
93
+ if cls._instance is None:
94
+ cls._instance = cls()
95
+ return cls._instance
96
+
97
+ @classmethod
98
+ def reset_instance(cls):
99
+ """Reset the singleton (mainly for testing)."""
100
+ cls._instance = None
101
+
102
+ def get(self, key: str, default: T = None, type_: type = str) -> T:
103
+ """
104
+ Get a configuration value with type conversion.
105
+
106
+ Args:
107
+ key: The setting key (can use "/" for groups, e.g., "stacking/chunk_size")
108
+ default: Default value if key doesn't exist
109
+ type_: Expected type (int, float, bool, str, list, dict)
110
+
111
+ Returns:
112
+ The value converted to the specified type
113
+ """
114
+ # Check cache first
115
+ cache_key = f"{key}:{type_.__name__}"
116
+ if cache_key in self._cache:
117
+ return self._cache[cache_key]
118
+
119
+ value = self._settings.value(key, default)
120
+
121
+ # Convert to requested type
122
+ if value is None:
123
+ return default
124
+
125
+ try:
126
+ if type_ == bool:
127
+ # QSettings stores bools as strings sometimes
128
+ if isinstance(value, str):
129
+ value = value.lower() in ('true', '1', 'yes')
130
+ else:
131
+ value = bool(value)
132
+ elif type_ == int:
133
+ value = int(value)
134
+ elif type_ == float:
135
+ value = float(value)
136
+ elif type_ == list:
137
+ if isinstance(value, str):
138
+ value = json.loads(value) if value else []
139
+ elif not isinstance(value, list):
140
+ value = list(value) if value else []
141
+ elif type_ == dict:
142
+ if isinstance(value, str):
143
+ value = json.loads(value) if value else {}
144
+ elif not isinstance(value, dict):
145
+ value = {}
146
+ else:
147
+ value = type_(value)
148
+ except (ValueError, TypeError, json.JSONDecodeError):
149
+ value = default
150
+
151
+ # Cache the result
152
+ self._cache[cache_key] = value
153
+ return value
154
+
155
+ def set(self, key: str, value: Any) -> None:
156
+ """
157
+ Set a configuration value.
158
+
159
+ Args:
160
+ key: The setting key
161
+ value: The value to store
162
+ """
163
+ # Invalidate cache for this key
164
+ for cache_key in list(self._cache.keys()):
165
+ if cache_key.startswith(f"{key}:"):
166
+ del self._cache[cache_key]
167
+
168
+ # Convert complex types to JSON for storage
169
+ if isinstance(value, (list, dict)):
170
+ value = json.dumps(value)
171
+
172
+ self._settings.setValue(key, value)
173
+
174
+ # Emit signal
175
+ self.settingChanged.emit(key, value)
176
+
177
+ # Notify listeners
178
+ if key in self._listeners:
179
+ for callback in self._listeners[key]:
180
+ try:
181
+ callback(value)
182
+ except Exception:
183
+ pass
184
+
185
+ def remove(self, key: str) -> None:
186
+ """Remove a setting."""
187
+ # Invalidate cache
188
+ for cache_key in list(self._cache.keys()):
189
+ if cache_key.startswith(f"{key}:"):
190
+ del self._cache[cache_key]
191
+
192
+ self._settings.remove(key)
193
+
194
+ def contains(self, key: str) -> bool:
195
+ """Check if a setting exists."""
196
+ return self._settings.contains(key)
197
+
198
+ def all_keys(self) -> List[str]:
199
+ """Get all setting keys."""
200
+ return self._settings.allKeys()
201
+
202
+ def clear(self) -> None:
203
+ """Clear all settings."""
204
+ self._cache.clear()
205
+ self._settings.clear()
206
+
207
+ def sync(self) -> None:
208
+ """Force sync to disk."""
209
+ self._settings.sync()
210
+
211
+ def add_listener(self, key: str, callback: Callable[[Any], None]) -> None:
212
+ """
213
+ Add a listener for changes to a specific key.
214
+
215
+ Args:
216
+ key: The setting key to watch
217
+ callback: Function to call when value changes
218
+ """
219
+ if key not in self._listeners:
220
+ self._listeners[key] = []
221
+ self._listeners[key].append(callback)
222
+
223
+ def remove_listener(self, key: str, callback: Callable[[Any], None]) -> None:
224
+ """Remove a listener."""
225
+ if key in self._listeners and callback in self._listeners[key]:
226
+ self._listeners[key].remove(callback)
227
+
228
+ def begin_group(self, prefix: str) -> None:
229
+ """Begin a settings group (for hierarchical settings)."""
230
+ self._settings.beginGroup(prefix)
231
+
232
+ def end_group(self) -> None:
233
+ """End the current settings group."""
234
+ self._settings.endGroup()
235
+
236
+ def get_group(self, prefix: str) -> Dict[str, Any]:
237
+ """
238
+ Get all settings in a group as a dictionary.
239
+
240
+ Args:
241
+ prefix: The group prefix
242
+
243
+ Returns:
244
+ Dictionary of key -> value for all settings in the group
245
+ """
246
+ result = {}
247
+ self._settings.beginGroup(prefix)
248
+ for key in self._settings.childKeys():
249
+ result[key] = self._settings.value(key)
250
+ self._settings.endGroup()
251
+ return result
252
+
253
+ def set_group(self, prefix: str, values: Dict[str, Any]) -> None:
254
+ """
255
+ Set multiple settings in a group.
256
+
257
+ Args:
258
+ prefix: The group prefix
259
+ values: Dictionary of key -> value to set
260
+ """
261
+ self._settings.beginGroup(prefix)
262
+ for key, value in values.items():
263
+ if isinstance(value, (list, dict)):
264
+ value = json.dumps(value)
265
+ self._settings.setValue(key, value)
266
+ self._settings.endGroup()
267
+ self._cache.clear() # Invalidate all cache
268
+
269
+
270
+ # Convenience function
271
+ def get_config() -> ConfigManager:
272
+ """Get the global configuration manager."""
273
+ return ConfigManager.instance()
274
+
275
+
276
+ # ============================================================================
277
+ # Application-specific configuration class with typed properties
278
+ # ============================================================================
279
+
280
+ class AppConfig(ConfigManager):
281
+ """
282
+ Application configuration with typed properties.
283
+
284
+ Usage:
285
+ config = AppConfig.instance()
286
+ print(config.chunk_height)
287
+ config.chunk_height = 1024
288
+ """
289
+
290
+ # Stacking settings
291
+ chunk_height = ConfigValue("stacking/chunk_height", default=512, type_=int)
292
+ chunk_width = ConfigValue("stacking/chunk_width", default=512, type_=int)
293
+ use_gpu = ConfigValue("stacking/use_gpu", default=True, type_=bool)
294
+ rejection_algorithm = ConfigValue("stacking/rejection_algorithm", default="sigma_clip", type_=str)
295
+
296
+ # UI settings
297
+ theme = ConfigValue("ui/theme", default="dark", type_=str)
298
+ show_toolbar = ConfigValue("ui/show_toolbar", default=True, type_=bool)
299
+ recent_files_max = ConfigValue("ui/recent_files_max", default=10, type_=int)
300
+
301
+ # Performance settings
302
+ max_threads = ConfigValue("performance/max_threads", default=0, type_=int) # 0 = auto
303
+ memory_limit_mb = ConfigValue("performance/memory_limit_mb", default=0, type_=int) # 0 = no limit
304
+ use_memmap = ConfigValue("performance/use_memmap", default=True, type_=bool)
305
+
306
+ # Paths
307
+ last_open_dir = ConfigValue("paths/last_open_dir", default="", type_=str)
308
+ last_save_dir = ConfigValue("paths/last_save_dir", default="", type_=str)
309
+ output_dir = ConfigValue("paths/output_dir", default="", type_=str)
310
+
311
+
312
+ def get_app_config() -> AppConfig:
313
+ """Get the application configuration with typed properties."""
314
+ if AppConfig._instance is None:
315
+ AppConfig._instance = AppConfig()
316
+ return AppConfig._instance # type: ignore