lib.slides_config
Configuración de usuario compartida entre CLI y GUI. Fichero: ~/.config/temario/temario.yaml
1""" 2Configuración de usuario compartida entre CLI y GUI. 3Fichero: ~/.config/temario/temario.yaml 4""" 5 6from pathlib import Path 7import yaml 8 9CONFIG_PATH = Path.home() / '.config' / 'temario' / 'temario.yaml' 10DEFAULT_COURSES_DIR = Path.home() / 'cursos' 11DEFAULT_LOGS_DIR = Path.home() / '.local' / 'share' / 'temario' / 'logs' 12 13 14def _load() -> dict: 15 if not CONFIG_PATH.exists(): 16 return {} 17 try: 18 return yaml.safe_load(CONFIG_PATH.read_text(encoding='utf-8')) or {} 19 except Exception: 20 return {} 21 22 23def _save(data: dict) -> None: 24 CONFIG_PATH.parent.mkdir(parents=True, exist_ok=True) 25 CONFIG_PATH.write_text( 26 yaml.dump(data, allow_unicode=True, sort_keys=False), 27 encoding='utf-8', 28 ) 29 30 31def get_courses_dir() -> Path | None: 32 """Devuelve el directorio de cursos configurado, o None si no está definido.""" 33 value = _load().get('courses_dir') 34 return Path(value) if value else None 35 36 37def save_courses_dir(path: Path) -> None: 38 """Guarda el directorio de cursos en la configuración del usuario.""" 39 data = _load() 40 data['courses_dir'] = str(path) 41 _save(data) 42 43 44def get_logs_dir() -> Path | None: 45 """Devuelve el directorio de logs. 46 - No definido → DEFAULT_LOGS_DIR (~/.local/share/temario/logs/) 47 - logs_dir: "" → None (logs desactivados) 48 - logs_dir: /ruta → esa ruta 49 """ 50 value = _load().get('logs_dir', None) 51 if value is None: 52 return DEFAULT_LOGS_DIR 53 if not str(value).strip(): 54 return None 55 return Path(str(value)).expanduser() 56 57 58def save_logs_dir(path: Path | None) -> None: 59 """Guarda el directorio de logs. Path(None) lo desactiva (guarda cadena vacía).""" 60 data = _load() 61 data['logs_dir'] = str(path) if path else '' 62 _save(data) 63 64 65def get_theme() -> str: 66 """Devuelve el tema activo. 67 Si no está configurado, detecta la preferencia del escritorio (dark/light).""" 68 saved = _load().get('theme') 69 if saved in ('dark', 'light'): 70 return saved 71 return _detect_desktop_theme() 72 73 74def _detect_desktop_theme() -> str: 75 """Detecta si el escritorio usa dark o light mode. 76 Comprueba D-Bus (GNOME/KDE), variable de entorno y config de GTK.""" 77 # 1. PySide6 / Qt colorScheme (disponible a partir de Qt 6.5) 78 try: 79 from PySide6.QtGui import QGuiApplication, Qt 80 hints = QGuiApplication.styleHints() 81 if hasattr(hints, 'colorScheme'): 82 scheme = hints.colorScheme() 83 # Qt.ColorScheme.Dark = 2, Light = 1 84 if hasattr(Qt, 'ColorScheme'): 85 if scheme == Qt.ColorScheme.Dark: 86 return 'dark' 87 if scheme == Qt.ColorScheme.Light: 88 return 'light' 89 except Exception: 90 pass 91 92 # 2. D-Bus org.freedesktop.portal (GTK/GNOME/KDE/XFCE) 93 try: 94 import subprocess 95 result = subprocess.run( 96 ['gdbus', 'call', '--session', 97 '--dest', 'org.freedesktop.portal.Desktop', 98 '--object-path', '/org/freedesktop/portal/desktop', 99 '--method', 'org.freedesktop.portal.Settings.Read', 100 'org.freedesktop.appearance', 'color-scheme'], 101 capture_output=True, text=True, timeout=2, 102 ) 103 if result.returncode == 0: 104 out = result.stdout.strip() 105 # retorna <<uint32 1>> para dark, <<uint32 2>> para light, 0 para sin pref 106 if '1' in out: 107 return 'dark' 108 if '2' in out: 109 return 'light' 110 except Exception: 111 pass 112 113 # 3. GTK settings.ini 114 try: 115 from pathlib import Path 116 gtk_ini = Path.home() / '.config' / 'gtk-3.0' / 'settings.ini' 117 if gtk_ini.exists(): 118 text = gtk_ini.read_text(errors='ignore').lower() 119 if 'gtk-application-prefer-dark-theme=true' in text or \ 120 'gtk-application-prefer-dark-theme=1' in text: 121 return 'dark' 122 except Exception: 123 pass 124 125 # 4. Fallback: dark 126 return 'dark' 127 128 129def save_theme(name: str) -> None: 130 """Guarda el tema preferido.""" 131 data = _load() 132 data['theme'] = name 133 _save(data) 134 135 136def get_theme_overrides(theme_name: str | None = None) -> dict[str, str]: 137 """Devuelve las sobreescrituras de color. 138 Si theme_name es None devuelve todo; si es 'dark'/'light' devuelve solo las de ese tema.""" 139 raw = _load().get('theme_overrides') or {} 140 if theme_name is None: 141 return dict(raw) 142 return dict(raw.get(theme_name) or {}) 143 144 145def save_theme_overrides(overrides: dict[str, str], theme_name: str) -> None: 146 """Guarda el dict completo de sobreescrituras para un tema.""" 147 data = _load() 148 data.setdefault('theme_overrides', {})[theme_name] = dict(overrides) 149 _save(data) 150 151 152def save_theme_override(key: str, value: str, theme_name: str = 'dark') -> None: 153 """Guarda una sobreescritura de color individual para un tema.""" 154 data = _load() 155 data.setdefault('theme_overrides', {}).setdefault(theme_name, {})[key] = value 156 _save(data) 157 158 159def clear_theme_overrides(theme_name: str | None = None) -> None: 160 """Elimina todas las sobreescrituras (o solo las de un tema).""" 161 data = _load() 162 if theme_name: 163 (data.get('theme_overrides') or {}).pop(theme_name, None) 164 else: 165 data.pop('theme_overrides', None) 166 _save(data) 167 168 169_AUTOSAVE_DEFAULT = 60 170 171 172# ── Sesión (último curso y presentación activos) ────────────────────────────── 173 174def get_last_session() -> tuple[str | None, str | None]: 175 """Devuelve (last_course, last_yaml) guardados, o (None, None).""" 176 session = _load().get('session') or {} 177 return session.get('last_course'), session.get('last_yaml') 178 179 180def save_last_session(course: str | None, yaml: str | None) -> None: 181 data = _load() 182 data.setdefault('session', {}) 183 if course is not None: 184 data['session']['last_course'] = course 185 if yaml is not None: 186 data['session']['last_yaml'] = yaml 187 _save(data) 188 189 190# ── Configuración de generación de slides ───────────────────────────────────── 191 192_GENERATION_DEFAULTS: dict = { 193 'code_split_lines': 20, 194 'list_split_items': 14, 195 'compare_split_lines': 20, 196 'code_font_sizes': [[12, 16], [16, 14], [21, 12], [27, 10], [9999, 9]], 197} 198 199 200def get_generation_config() -> dict: 201 """Devuelve la configuración de generación con los valores por defecto si no están definidos.""" 202 raw = _load().get('generation') or {} 203 result = dict(_GENERATION_DEFAULTS) 204 for key, default in _GENERATION_DEFAULTS.items(): 205 if key in raw: 206 try: 207 if key == 'code_font_sizes': 208 result[key] = [[int(a), int(b)] for a, b in raw[key]] 209 else: 210 result[key] = max(1, int(raw[key])) 211 except (TypeError, ValueError): 212 pass 213 return result 214 215 216def save_generation_param(key: str, value) -> None: 217 if key not in _GENERATION_DEFAULTS: 218 raise ValueError(f'Clave de generación desconocida: {key!r}') 219 data = _load() 220 data.setdefault('generation', {})[key] = value 221 _save(data) 222 223 224def get_autosave_seconds() -> int: 225 """Devuelve el intervalo de autoguardado en segundos. 0 = desactivado.""" 226 value = _load().get('autosave_seconds', _AUTOSAVE_DEFAULT) 227 try: 228 return max(0, int(value)) 229 except (TypeError, ValueError): 230 return _AUTOSAVE_DEFAULT 231 232 233def save_autosave_seconds(value: int) -> None: 234 data = _load() 235 data['autosave_seconds'] = max(0, int(value)) 236 _save(data) 237 238 239def get_language() -> str | None: 240 """Devuelve el idioma configurado ('es', 'en', …), o None si no está definido.""" 241 return _load().get('language') or None 242 243 244def save_language(lang: str) -> None: 245 """Guarda el idioma preferido en la configuración del usuario.""" 246 data = _load() 247 data['language'] = lang 248 _save(data) 249 250 251# ── Configuración de pantalla (Qt scaling) ──────────────────────────────────── 252 253_DISPLAY_DEFAULTS: dict[str, str] = { 254 'scale_factor': '1.0', 255 'highdpi_scaling': '1', 256 'auto_screen_scale': '1', 257 'scale_rounding_policy': 'PassThrough', 258} 259 260 261def get_display_settings() -> dict[str, str]: 262 """Devuelve los ajustes de pantalla con sus valores por defecto si no están configurados.""" 263 display = _load().get('display') or {} 264 return {k: str(display.get(k, v)) for k, v in _DISPLAY_DEFAULTS.items()} 265 266 267def save_display_setting(key: str, value: str) -> None: 268 """Guarda un ajuste de pantalla individual.""" 269 if key not in _DISPLAY_DEFAULTS: 270 raise ValueError(f'Clave de pantalla desconocida: {key!r}') 271 data = _load() 272 data.setdefault('display', {})[key] = value 273 _save(data)
32def get_courses_dir() -> Path | None: 33 """Devuelve el directorio de cursos configurado, o None si no está definido.""" 34 value = _load().get('courses_dir') 35 return Path(value) if value else None
Devuelve el directorio de cursos configurado, o None si no está definido.
38def save_courses_dir(path: Path) -> None: 39 """Guarda el directorio de cursos en la configuración del usuario.""" 40 data = _load() 41 data['courses_dir'] = str(path) 42 _save(data)
Guarda el directorio de cursos en la configuración del usuario.
45def get_logs_dir() -> Path | None: 46 """Devuelve el directorio de logs. 47 - No definido → DEFAULT_LOGS_DIR (~/.local/share/temario/logs/) 48 - logs_dir: "" → None (logs desactivados) 49 - logs_dir: /ruta → esa ruta 50 """ 51 value = _load().get('logs_dir', None) 52 if value is None: 53 return DEFAULT_LOGS_DIR 54 if not str(value).strip(): 55 return None 56 return Path(str(value)).expanduser()
Devuelve el directorio de logs.
- No definido → DEFAULT_LOGS_DIR (~/.local/share/temario/logs/)
- logs_dir: "" → None (logs desactivados)
- logs_dir: /ruta → esa ruta
59def save_logs_dir(path: Path | None) -> None: 60 """Guarda el directorio de logs. Path(None) lo desactiva (guarda cadena vacía).""" 61 data = _load() 62 data['logs_dir'] = str(path) if path else '' 63 _save(data)
Guarda el directorio de logs. Path(None) lo desactiva (guarda cadena vacía).
66def get_theme() -> str: 67 """Devuelve el tema activo. 68 Si no está configurado, detecta la preferencia del escritorio (dark/light).""" 69 saved = _load().get('theme') 70 if saved in ('dark', 'light'): 71 return saved 72 return _detect_desktop_theme()
Devuelve el tema activo. Si no está configurado, detecta la preferencia del escritorio (dark/light).
130def save_theme(name: str) -> None: 131 """Guarda el tema preferido.""" 132 data = _load() 133 data['theme'] = name 134 _save(data)
Guarda el tema preferido.
137def get_theme_overrides(theme_name: str | None = None) -> dict[str, str]: 138 """Devuelve las sobreescrituras de color. 139 Si theme_name es None devuelve todo; si es 'dark'/'light' devuelve solo las de ese tema.""" 140 raw = _load().get('theme_overrides') or {} 141 if theme_name is None: 142 return dict(raw) 143 return dict(raw.get(theme_name) or {})
Devuelve las sobreescrituras de color. Si theme_name es None devuelve todo; si es 'dark'/'light' devuelve solo las de ese tema.
146def save_theme_overrides(overrides: dict[str, str], theme_name: str) -> None: 147 """Guarda el dict completo de sobreescrituras para un tema.""" 148 data = _load() 149 data.setdefault('theme_overrides', {})[theme_name] = dict(overrides) 150 _save(data)
Guarda el dict completo de sobreescrituras para un tema.
153def save_theme_override(key: str, value: str, theme_name: str = 'dark') -> None: 154 """Guarda una sobreescritura de color individual para un tema.""" 155 data = _load() 156 data.setdefault('theme_overrides', {}).setdefault(theme_name, {})[key] = value 157 _save(data)
Guarda una sobreescritura de color individual para un tema.
160def clear_theme_overrides(theme_name: str | None = None) -> None: 161 """Elimina todas las sobreescrituras (o solo las de un tema).""" 162 data = _load() 163 if theme_name: 164 (data.get('theme_overrides') or {}).pop(theme_name, None) 165 else: 166 data.pop('theme_overrides', None) 167 _save(data)
Elimina todas las sobreescrituras (o solo las de un tema).
175def get_last_session() -> tuple[str | None, str | None]: 176 """Devuelve (last_course, last_yaml) guardados, o (None, None).""" 177 session = _load().get('session') or {} 178 return session.get('last_course'), session.get('last_yaml')
Devuelve (last_course, last_yaml) guardados, o (None, None).
201def get_generation_config() -> dict: 202 """Devuelve la configuración de generación con los valores por defecto si no están definidos.""" 203 raw = _load().get('generation') or {} 204 result = dict(_GENERATION_DEFAULTS) 205 for key, default in _GENERATION_DEFAULTS.items(): 206 if key in raw: 207 try: 208 if key == 'code_font_sizes': 209 result[key] = [[int(a), int(b)] for a, b in raw[key]] 210 else: 211 result[key] = max(1, int(raw[key])) 212 except (TypeError, ValueError): 213 pass 214 return result
Devuelve la configuración de generación con los valores por defecto si no están definidos.
225def get_autosave_seconds() -> int: 226 """Devuelve el intervalo de autoguardado en segundos. 0 = desactivado.""" 227 value = _load().get('autosave_seconds', _AUTOSAVE_DEFAULT) 228 try: 229 return max(0, int(value)) 230 except (TypeError, ValueError): 231 return _AUTOSAVE_DEFAULT
Devuelve el intervalo de autoguardado en segundos. 0 = desactivado.
240def get_language() -> str | None: 241 """Devuelve el idioma configurado ('es', 'en', …), o None si no está definido.""" 242 return _load().get('language') or None
Devuelve el idioma configurado ('es', 'en', …), o None si no está definido.
245def save_language(lang: str) -> None: 246 """Guarda el idioma preferido en la configuración del usuario.""" 247 data = _load() 248 data['language'] = lang 249 _save(data)
Guarda el idioma preferido en la configuración del usuario.
262def get_display_settings() -> dict[str, str]: 263 """Devuelve los ajustes de pantalla con sus valores por defecto si no están configurados.""" 264 display = _load().get('display') or {} 265 return {k: str(display.get(k, v)) for k, v in _DISPLAY_DEFAULTS.items()}
Devuelve los ajustes de pantalla con sus valores por defecto si no están configurados.
268def save_display_setting(key: str, value: str) -> None: 269 """Guarda un ajuste de pantalla individual.""" 270 if key not in _DISPLAY_DEFAULTS: 271 raise ValueError(f'Clave de pantalla desconocida: {key!r}') 272 data = _load() 273 data.setdefault('display', {})[key] = value 274 _save(data)
Guarda un ajuste de pantalla individual.