-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathconfig.py
More file actions
170 lines (143 loc) · 4.94 KB
/
Copy pathconfig.py
File metadata and controls
170 lines (143 loc) · 4.94 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
"""Configuration loading and validation.
Scope
-----
Owns the on-disk YAML config schema, defaults, deep-merge logic and
validation warnings. The app calls `load_config()` at startup and uses
the returned dict; nothing else in the codebase reads `config.yaml`
directly.
Boundaries
----------
- Does NOT depend on Textual or any UI module.
- Does NOT mutate the loaded YAML — returns a new merged dict.
- Does NOT enforce schema beyond key presence + value types.
Freeze criteria
---------------
- `load_config()` returns `(config_dict, warnings)` even when
`config.yaml` is missing or malformed.
- Unknown keys yield warnings, never crash.
- New top-level sections require an entry in `DEFAULT_CONFIG` plus an
entry in the corresponding `_KNOWN_*` set.
"""
from __future__ import annotations
from pathlib import Path
from typing import Any
import yaml
BASE_DIR = Path(__file__).resolve().parent
CONFIG_PATH = BASE_DIR / "config.yaml"
DEFAULT_CONFIG: dict[str, Any] = {
"app": {
"title": "Medlang",
"subtitle": "Vim-like JSON editor",
},
"editor": {
"placeholder": "Modo NORMAL. Pulsa i para escribir, : para comandos.",
"soft_wrap": False,
"show_line_numbers": True,
"vim_start_mode": "normal",
"theme": "textual-dark",
},
"storage": {
"vault_path": "files",
"document_path": "files/document.json",
"auto_save": True,
},
"panels": {
"files": {
"visible_on_start": True,
},
},
"commands": {
"theme-toggle": {
"enabled": True,
"description": "Toggle between dark and light theme.",
"hotkeys": [],
},
"theme-pick": {
"enabled": True,
"description": "Open the theme picker.",
"hotkeys": [],
},
"doc-rename": {
"enabled": True,
"description": "Rename the current JSON document.",
"hotkeys": [],
},
"doc-save": {
"enabled": True,
"description": "Force-save the document now.",
"hotkeys": [],
},
"pane-files-toggle": {
"enabled": True,
"description": "Toggle the file panel visibility.",
"hotkeys": [],
},
"pane-focus-files": {
"enabled": True,
"description": "Focus the file panel.",
"hotkeys": [],
},
"pane-focus-editor": {
"enabled": True,
"description": "Focus the editor.",
"hotkeys": [],
},
"doc-new": {
"enabled": True,
"description": "Create a new JSON document.",
"hotkeys": [],
},
"doc-open": {
"enabled": True,
"description": "Open a document from the vault.",
"hotkeys": [],
},
},
}
_KNOWN_TOP_LEVEL_KEYS = set(DEFAULT_CONFIG.keys())
_KNOWN_EDITOR_KEYS = set(DEFAULT_CONFIG["editor"].keys())
_KNOWN_STORAGE_KEYS = set(DEFAULT_CONFIG["storage"].keys())
_KNOWN_PANEL_KEYS = set(DEFAULT_CONFIG["panels"].keys())
def _deep_merge(base: dict[str, Any], override: dict[str, Any]) -> dict[str, Any]:
"""Recursively merge `override` on top of `base` without mutating either."""
merged = dict(base)
for key, value in override.items():
if isinstance(value, dict) and isinstance(merged.get(key), dict):
merged[key] = _deep_merge(merged[key], value)
else:
merged[key] = value
return merged
def _validate_config(cfg: dict[str, Any]) -> list[str]:
"""Return human-readable warnings for unexpected keys or types."""
warnings: list[str] = []
for key in cfg:
if key not in _KNOWN_TOP_LEVEL_KEYS:
warnings.append(f"unknown config section: {key!r}")
editor = cfg.get("editor", {})
if isinstance(editor, dict):
for key in editor:
if key not in _KNOWN_EDITOR_KEYS:
warnings.append(f"unknown editor.{key}")
storage = cfg.get("storage", {})
if isinstance(storage, dict):
for key in storage:
if key not in _KNOWN_STORAGE_KEYS:
warnings.append(f"unknown storage.{key}")
panels = cfg.get("panels", {})
if isinstance(panels, dict):
for key in panels:
if key not in _KNOWN_PANEL_KEYS:
warnings.append(f"unknown panels.{key}")
return warnings
def load_config() -> tuple[dict[str, Any], list[str]]:
"""Load config.yaml. Returns (config_dict, warnings)."""
if not CONFIG_PATH.exists():
return DEFAULT_CONFIG, []
try:
loaded = yaml.safe_load(CONFIG_PATH.read_text())
except yaml.YAMLError as exc:
return DEFAULT_CONFIG, [f"config.yaml malformed: {exc}"]
if not isinstance(loaded, dict):
return DEFAULT_CONFIG, ["config.yaml must be a mapping; using defaults"]
warnings = _validate_config(loaded)
return _deep_merge(DEFAULT_CONFIG, loaded), warnings