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)
CONFIG_PATH = PosixPath('/home/pepesan/.config/temario/temario.yaml')
DEFAULT_COURSES_DIR = PosixPath('/home/pepesan/cursos')
DEFAULT_LOGS_DIR = PosixPath('/home/pepesan/.local/share/temario/logs')
def get_courses_dir() -> pathlib.Path | None:
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.

def save_courses_dir(path: pathlib.Path) -> None:
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.

def get_logs_dir() -> pathlib.Path | None:
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
def save_logs_dir(path: pathlib.Path | None) -> None:
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).

def get_theme() -> str:
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).

def save_theme(name: str) -> None:
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.

def get_theme_overrides(theme_name: str | None = None) -> dict[str, str]:
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.

def save_theme_overrides(overrides: dict[str, str], theme_name: str) -> None:
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.

def save_theme_override(key: str, value: str, theme_name: str = 'dark') -> None:
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.

def clear_theme_overrides(theme_name: str | None = None) -> None:
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).

def get_last_session() -> tuple[str | None, str | None]:
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).

def save_last_session(course: str | None, yaml: str | None) -> None:
181def save_last_session(course: str | None, yaml: str | None) -> None:
182    data = _load()
183    data.setdefault('session', {})
184    if course is not None:
185        data['session']['last_course'] = course
186    if yaml is not None:
187        data['session']['last_yaml'] = yaml
188    _save(data)
def get_generation_config() -> dict:
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.

def save_generation_param(key: str, value) -> None:
217def save_generation_param(key: str, value) -> None:
218    if key not in _GENERATION_DEFAULTS:
219        raise ValueError(f'Clave de generación desconocida: {key!r}')
220    data = _load()
221    data.setdefault('generation', {})[key] = value
222    _save(data)
def get_autosave_seconds() -> int:
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.

def save_autosave_seconds(value: int) -> None:
234def save_autosave_seconds(value: int) -> None:
235    data = _load()
236    data['autosave_seconds'] = max(0, int(value))
237    _save(data)
def get_language() -> str | None:
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.

def save_language(lang: str) -> None:
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.

def get_display_settings() -> dict[str, str]:
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.

def save_display_setting(key: str, value: str) -> None:
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.