hueman-cli 0.2.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.
Files changed (78) hide show
  1. hueman/__init__.py +11 -0
  2. hueman/__main__.py +3 -0
  3. hueman/cli.py +353 -0
  4. hueman/data/hueman.config.example.yaml +111 -0
  5. hueman/engine/__init__.py +0 -0
  6. hueman/engine/colors.py +30 -0
  7. hueman/engine/config.py +186 -0
  8. hueman/engine/fsutil.py +41 -0
  9. hueman/engine/palettes.py +66 -0
  10. hueman/engine/reload.py +60 -0
  11. hueman/engine/render.py +243 -0
  12. hueman/engine/statesource.py +370 -0
  13. hueman/engine/targets.py +95 -0
  14. hueman/palettes/arctic-ice.yaml +102 -0
  15. hueman/palettes/autumn.yaml +102 -0
  16. hueman/palettes/catppuccin-latte.yaml +102 -0
  17. hueman/palettes/catppuccin-mocha.yaml +102 -0
  18. hueman/palettes/copperwave.yaml +103 -0
  19. hueman/palettes/crimson-vivid.yaml +111 -0
  20. hueman/palettes/dracula.yaml +102 -0
  21. hueman/palettes/ember-red.yaml +111 -0
  22. hueman/palettes/everforest-dark.yaml +102 -0
  23. hueman/palettes/galaxy.yaml +102 -0
  24. hueman/palettes/gruvbox-light.yaml +102 -0
  25. hueman/palettes/gruvbox.yaml +102 -0
  26. hueman/palettes/kanagawa-dragon.yaml +102 -0
  27. hueman/palettes/matrix.yaml +102 -0
  28. hueman/palettes/mono-contrast.yaml +102 -0
  29. hueman/palettes/nord.yaml +102 -0
  30. hueman/palettes/ocean-deep.yaml +102 -0
  31. hueman/palettes/one-light.yaml +102 -0
  32. hueman/palettes/onyx-gold.yaml +103 -0
  33. hueman/palettes/rose-pine-dawn.yaml +102 -0
  34. hueman/palettes/solarized-light.yaml +102 -0
  35. hueman/palettes/synthwave.yaml +102 -0
  36. hueman/palettes/tokyonight.yaml +106 -0
  37. hueman/palettes/tuxedo.yaml +103 -0
  38. hueman/palettes/vaporwave.yaml +102 -0
  39. hueman/resources.py +18 -0
  40. hueman/templates/atuin_theme.toml.j2 +19 -0
  41. hueman/templates/bottom_theme.toml.j2 +43 -0
  42. hueman/templates/bspwm_colors.conf.j2 +5 -0
  43. hueman/templates/claude_statusline.sh.j2 +216 -0
  44. hueman/templates/dunst_colors.conf.j2 +15 -0
  45. hueman/templates/fzf_opts.j2 +9 -0
  46. hueman/templates/fzf_theme.j2 +17 -0
  47. hueman/templates/herdr_theme_custom.toml.j2 +11 -0
  48. hueman/templates/k9s_theme.yaml.j2 +83 -0
  49. hueman/templates/kitty.conf.j2 +51 -0
  50. hueman/templates/lazydocker_theme.yml.j2 +19 -0
  51. hueman/templates/leaf_theme.toml.j2 +89 -0
  52. hueman/templates/newt_colors.j2 +32 -0
  53. hueman/templates/nvim_nightfox_theme.lua.j2 +70 -0
  54. hueman/templates/omp.omp.json.j2 +107 -0
  55. hueman/templates/opencode_theme.json.j2 +71 -0
  56. hueman/templates/pi_theme.json.j2 +65 -0
  57. hueman/templates/polybar_colors.ini.j2 +11 -0
  58. hueman/templates/posting_theme.yaml.j2 +47 -0
  59. hueman/templates/qutebrowser_github.user.js.j2 +121 -0
  60. hueman/templates/qutebrowser_google.user.js.j2 +132 -0
  61. hueman/templates/qutebrowser_hteam.user.js.j2 +962 -0
  62. hueman/templates/qutebrowser_theme.py.j2 +78 -0
  63. hueman/templates/rofi_theme.rasi.j2 +132 -0
  64. hueman/templates/sshub_theme.toml.j2 +49 -0
  65. hueman/templates/starship_block.toml.j2 +11 -0
  66. hueman/templates/termux_colors.properties.j2 +20 -0
  67. hueman/templates/tmux.conf.j2 +33 -0
  68. hueman/templates/tuicr_theme.toml.j2 +51 -0
  69. hueman/templates/wezterm_colors.lua.j2 +14 -0
  70. hueman/templates/windhawk_taskbar.json.j2 +4 -0
  71. hueman/templates/yazi_theme.toml.j2 +130 -0
  72. hueman/templates/zen_theme.json.j2 +11 -0
  73. hueman_cli-0.2.0.dist-info/METADATA +119 -0
  74. hueman_cli-0.2.0.dist-info/RECORD +78 -0
  75. hueman_cli-0.2.0.dist-info/WHEEL +5 -0
  76. hueman_cli-0.2.0.dist-info/entry_points.txt +2 -0
  77. hueman_cli-0.2.0.dist-info/licenses/LICENSE +21 -0
  78. hueman_cli-0.2.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,370 @@
1
+ """Shared palette state behind a small `StateSource` interface.
2
+
3
+ The engine only ever talks to `StateSource` (see its docstring for the
4
+ contract) and obtains one through `build_source(settings)`; the backend is
5
+ chosen declaratively by `state_sync.type` and looked up in a registry.
6
+ Backends: `git` (default) and `file` (a state file in a synced folder).
7
+
8
+ `GitBranchSource` keeps the state in an orphan branch (`hueman-state`) holding
9
+ a single parentless commit with `hueman.state.yaml`. All git work happens in a
10
+ hueman-owned bare repository (default: XDG state dir), built with plumbing
11
+ commands, so the user's dotfiles repo and working tree are never touched; the
12
+ dotfiles repo is only consulted to resolve the remote URL.
13
+
14
+ Rewriting the single commit with `commit-tree` (no parent) is the plumbing
15
+ equivalent of `commit --amend`; it is published with `push --force-with-lease`.
16
+ Every failure mode (no git, no remote, no network) degrades to None / False
17
+ and never raises, so callers can keep applying palettes locally.
18
+ """
19
+
20
+ import hashlib
21
+ import logging
22
+ import os
23
+ import shutil
24
+ import subprocess
25
+ from abc import ABC, abstractmethod
26
+ from dataclasses import dataclass
27
+ from pathlib import Path
28
+ from typing import Callable, ClassVar, Optional
29
+
30
+ import yaml
31
+
32
+ from .fsutil import atomic_write_text
33
+ from .palettes import is_valid_palette_name
34
+
35
+ STATE_FILE_NAME = "hueman.state.yaml"
36
+ DEFAULT_BRANCH = "hueman-state"
37
+ GIT_TIMEOUT = 15 # seconds per git call; keeps shell-startup sync bounded
38
+ _REMOTE_REF = "refs/hueman/remote"
39
+ _IDENTITY = ["-c", "user.name=hueman", "-c", "user.email=hueman@localhost",
40
+ "-c", "commit.gpgsign=false"]
41
+
42
+
43
+ @dataclass(frozen=True)
44
+ class State:
45
+ palette: str
46
+ revision: int
47
+
48
+
49
+ @dataclass(frozen=True)
50
+ class ReadResult:
51
+ """Outcome of a read. `remote` is True only when the state was just
52
+ fetched from the remote (an empty remote is still remote=True, state=None);
53
+ False means the remote was unreachable and `state` is the local cache."""
54
+ state: Optional[State]
55
+ remote: bool
56
+
57
+
58
+ @dataclass(frozen=True)
59
+ class StateSyncSettings:
60
+ """Settings of the `git` backend (`state_sync.type: git`, the default)."""
61
+ type: ClassVar[str] = "git"
62
+ repo: Path
63
+ remote: str = "origin"
64
+ branch: str = DEFAULT_BRANCH
65
+ workdir: Optional[Path] = None
66
+ timeout: int = GIT_TIMEOUT
67
+
68
+
69
+ class StateSource(ABC):
70
+ """Backend holding the shared palette state.
71
+
72
+ Contract every backend must honour (checked by
73
+ tests/test_state_source_contract.py):
74
+
75
+ * Never raise: every failure (missing tool, unreachable store, corrupt
76
+ data, I/O error, timeout) degrades to the "unreachable"/False results
77
+ below so callers can keep applying palettes locally.
78
+ * Bounded time: every blocking operation has a timeout; a backend must
79
+ not hang shell startup.
80
+ * `revision` is a monotonic integer. Higher revision wins; on a tie the
81
+ accepted write wins (last writer), mirroring the git push rule.
82
+ """
83
+
84
+ @abstractmethod
85
+ def read_result(self) -> ReadResult:
86
+ """Read the shared state.
87
+
88
+ `ReadResult(state, remote=True)`: the store was reached; `state` is
89
+ None when it is empty or its content is unparseable.
90
+ `ReadResult(state, remote=False)`: the store was unreachable; `state`
91
+ is a possibly stale local cache or None, and callers must not apply it.
92
+ """
93
+
94
+ def read(self) -> Optional[State]:
95
+ """Convenience: just the state of `read_result()`."""
96
+ return self.read_result().state
97
+
98
+ @abstractmethod
99
+ def write(self, state: State) -> bool:
100
+ """Publish `state`; True only if it is now the shared state.
101
+
102
+ Returns True when the store already holds exactly `state`. Returns
103
+ False, without overwriting, when the store holds a strictly higher
104
+ revision, and False when the store is unreachable or the write fails.
105
+ """
106
+
107
+
108
+ def dump_state(state: State) -> str:
109
+ return yaml.safe_dump({"palette": state.palette, "revision": state.revision},
110
+ sort_keys=False)
111
+
112
+
113
+ def parse_state(text: str) -> Optional[State]:
114
+ try:
115
+ raw = yaml.safe_load(text)
116
+ state = State(str(raw["palette"]), int(raw["revision"]))
117
+ except (yaml.YAMLError, KeyError, TypeError, ValueError):
118
+ return None
119
+ # el nombre viene del remoto: uno invalido (ruta, '..') es dato inutilizable
120
+ return state if is_valid_palette_name(state.palette) else None
121
+
122
+
123
+ class GitBranchSource(StateSource):
124
+ def __init__(self, repo, remote="origin", branch=DEFAULT_BRANCH, workdir=None,
125
+ max_retries=3, logger=None, timeout=GIT_TIMEOUT):
126
+ self.timeout = timeout
127
+ self.repo = Path(repo).expanduser()
128
+ self.remote = remote
129
+ self.branch = branch
130
+ self.max_retries = max_retries
131
+ self.log = logger or logging.getLogger("hueman")
132
+ self._workdir = Path(workdir).expanduser() if workdir else None
133
+ self._url = None
134
+
135
+ @classmethod
136
+ def from_settings(cls, s: StateSyncSettings, logger=None):
137
+ return cls(s.repo, s.remote, s.branch, s.workdir, logger=logger, timeout=s.timeout)
138
+
139
+ # -- git plumbing -------------------------------------------------------
140
+
141
+ def _run(self, args, git_dir=True, input=None, cwd=None):
142
+ cmd = ["git"]
143
+ if git_dir:
144
+ cmd += ["--git-dir", str(self._workdir)]
145
+ env = {k: v for k, v in os.environ.items() if not k.startswith("GIT_")}
146
+ env["GIT_TERMINAL_PROMPT"] = "0"
147
+ for keep in ("GIT_SSH", "GIT_SSH_COMMAND"): # user's transport choice
148
+ if keep in os.environ:
149
+ env[keep] = os.environ[keep]
150
+ if "GIT_SSH" not in env:
151
+ env.setdefault("GIT_SSH_COMMAND", "ssh -o BatchMode=yes") # never prompt
152
+ try:
153
+ return subprocess.run(cmd + args, input=input, cwd=cwd, env=env,
154
+ capture_output=True, text=True, timeout=self.timeout)
155
+ except (OSError, subprocess.SubprocessError) as exc:
156
+ self.log.debug(f"git {' '.join(args)}: {exc}")
157
+ return None
158
+
159
+ def _ok(self, args, **kw):
160
+ res = self._run(args, **kw)
161
+ return res.stdout.strip() if res is not None and res.returncode == 0 else None
162
+
163
+ def _prepare(self) -> bool:
164
+ """Resolve the remote URL and make sure the private repo exists."""
165
+ if shutil.which("git") is None:
166
+ self.log.warning("git no disponible: estado compartido deshabilitado")
167
+ return False
168
+ url = self._ok(["-C", str(self.repo), "remote", "get-url", self.remote],
169
+ git_dir=False) if self.repo.is_dir() else None
170
+ if not url:
171
+ self.log.warning(f"remoto '{self.remote}' no disponible en {self.repo}")
172
+ return False
173
+ self._url = url
174
+ if self._workdir is None:
175
+ base = Path(os.environ.get("XDG_STATE_HOME") or Path.home() / ".local/state")
176
+ digest = hashlib.sha1(f"{url}\0{self.branch}".encode()).hexdigest()[:12]
177
+ self._workdir = base / "hueman" / "state-sync" / f"{digest}.git"
178
+ if not (self._workdir / "HEAD").exists():
179
+ self._workdir.parent.mkdir(parents=True, exist_ok=True)
180
+ if self._ok(["init", "--bare", "-q", str(self._workdir)], git_dir=False) is None:
181
+ self.log.warning(f"no se pudo crear el repo de estado en {self._workdir}")
182
+ return False
183
+ return True
184
+
185
+ def _fetch(self):
186
+ """Refresh the remote tip. Returns (ok, sha-or-None)."""
187
+ ls = self._ok(["ls-remote", self._url, f"refs/heads/{self.branch}"])
188
+ if ls is None:
189
+ self.log.warning(f"remoto inalcanzable: {self._url}")
190
+ return False, None
191
+ if not ls:
192
+ self._run(["update-ref", "-d", _REMOTE_REF])
193
+ return True, None
194
+ spec = f"+refs/heads/{self.branch}:{_REMOTE_REF}"
195
+ if self._ok(["fetch", "-q", "--no-tags", self._url, spec]) is None:
196
+ self.log.warning(f"fetch fallo desde {self._url}")
197
+ return False, None
198
+ return True, self._ok(["rev-parse", _REMOTE_REF])
199
+
200
+ def _state_at(self, ref) -> Optional[State]:
201
+ text = self._ok(["cat-file", "-p", f"{ref}:{STATE_FILE_NAME}"])
202
+ return parse_state(text) if text else None
203
+
204
+ def _commit(self, state: State) -> Optional[str]:
205
+ blob = self._ok(["hash-object", "-w", "--stdin"], input=dump_state(state))
206
+ tree = self._ok(["mktree"], input=f"100644 blob {blob}\t{STATE_FILE_NAME}\n") if blob else None
207
+ if not tree:
208
+ return None
209
+ return self._ok(_IDENTITY + ["commit-tree", tree, "-m", f"hueman state r{state.revision}"])
210
+
211
+ def _push(self, sha: str, expected: Optional[str]) -> bool:
212
+ ref = f"refs/heads/{self.branch}"
213
+ res = self._run(["push", "-q", f"--force-with-lease={ref}:{expected or ''}",
214
+ self._url, f"{sha}:{ref}"])
215
+ return res is not None and res.returncode == 0
216
+
217
+ # -- StateSource --------------------------------------------------------
218
+
219
+ def read_result(self) -> ReadResult:
220
+ if not self._prepare():
221
+ return ReadResult(None, False)
222
+ ok, sha = self._fetch()
223
+ if ok:
224
+ return ReadResult(self._state_at(sha) if sha else None, True)
225
+ # remote unreachable: last known local copy, flagged as such
226
+ # (last fetched tip; readers never write refs/heads, only publishers do)
227
+ cached = self._state_at(_REMOTE_REF) or self._state_at(f"refs/heads/{self.branch}")
228
+ return ReadResult(cached, False)
229
+
230
+ def write(self, state: State) -> bool:
231
+ if not self._prepare():
232
+ return False
233
+ for _ in range(self.max_retries + 1):
234
+ ok, remote_sha = self._fetch()
235
+ if not ok:
236
+ return False
237
+ remote = self._state_at(remote_sha) if remote_sha else None
238
+ if remote is not None:
239
+ if remote.revision > state.revision:
240
+ self.log.warning(
241
+ f"estado remoto mas nuevo (r{remote.revision} > r{state.revision}, "
242
+ f"paleta {remote.palette}); no se publica")
243
+ return False
244
+ if remote == state:
245
+ return True
246
+ sha = self._commit(state)
247
+ if sha is None:
248
+ self.log.warning("no se pudo construir el commit de estado")
249
+ return False
250
+ if self._push(sha, remote_sha):
251
+ self._run(["update-ref", f"refs/heads/{self.branch}", sha])
252
+ return True
253
+ self.log.warning("push de estado rechazado repetidamente; se desiste")
254
+ return False
255
+
256
+
257
+ class FileSource(StateSource):
258
+ """State file in a folder synced by something else (Syncthing, Dropbox...).
259
+
260
+ Same YAML format as the git backend. The write is atomic (temp file +
261
+ rename via `engine.fsutil`), so the sync tool never uploads a partial file.
262
+ There is no cross-node lock: simultaneous writers are reconciled by the
263
+ sync tool (e.g. conflict copies) and by the higher-revision-wins rule on
264
+ the next read/write. A missing parent folder means the synced folder is
265
+ not mounted: unreachable, and it is never created.
266
+ """
267
+
268
+ def __init__(self, path, logger=None):
269
+ self.path = Path(path).expanduser()
270
+ self.log = logger or logging.getLogger("hueman")
271
+
272
+ @classmethod
273
+ def from_settings(cls, s: "FileSettings", logger=None):
274
+ return cls(s.path, logger=logger)
275
+
276
+ def read_result(self) -> ReadResult:
277
+ try:
278
+ if not self.path.parent.is_dir():
279
+ return ReadResult(None, False)
280
+ try:
281
+ text = self.path.read_text(encoding="utf-8")
282
+ except FileNotFoundError:
283
+ return ReadResult(None, True)
284
+ return ReadResult(parse_state(text), True)
285
+ except (OSError, UnicodeDecodeError) as exc:
286
+ self.log.debug(f"{self.path}: {exc}")
287
+ return ReadResult(None, False)
288
+
289
+ def write(self, state: State) -> bool:
290
+ res = self.read_result()
291
+ if not res.remote:
292
+ self.log.warning(f"carpeta de estado no disponible: {self.path.parent}")
293
+ return False
294
+ current = res.state
295
+ if current is not None:
296
+ if current.revision > state.revision:
297
+ self.log.warning(
298
+ f"estado compartido mas nuevo (r{current.revision} > r{state.revision}, "
299
+ f"paleta {current.palette}); no se publica")
300
+ return False
301
+ if current == state:
302
+ return True
303
+ try:
304
+ atomic_write_text(self.path, dump_state(state))
305
+ except OSError as exc:
306
+ self.log.warning(f"no se pudo escribir {self.path}: {exc}")
307
+ return False
308
+ return self.read() == state
309
+
310
+
311
+ @dataclass(frozen=True)
312
+ class FileSettings:
313
+ """Settings of the `file` backend (`state_sync.type: file`)."""
314
+ type: ClassVar[str] = "file"
315
+ path: Path
316
+
317
+
318
+ # -- registry -----------------------------------------------------------------
319
+
320
+ class StateSyncConfigError(ValueError):
321
+ """Invalid `state_sync` mapping (caller adds file context)."""
322
+
323
+
324
+ def _parse_git(raw: dict) -> StateSyncSettings:
325
+ if not isinstance(raw.get("repo"), str) or not raw["repo"]:
326
+ raise StateSyncConfigError("'state_sync' (type git) debe ser un mapeo con 'repo'")
327
+ for key in ("remote", "branch", "workdir"):
328
+ if key in raw and not isinstance(raw[key], str):
329
+ raise StateSyncConfigError(f"state_sync.{key} debe ser texto")
330
+ timeout = raw.get("timeout", GIT_TIMEOUT)
331
+ if isinstance(timeout, bool) or not isinstance(timeout, int) or timeout <= 0:
332
+ raise StateSyncConfigError("state_sync.timeout debe ser un entero > 0 (segundos)")
333
+ workdir = raw.get("workdir")
334
+ return StateSyncSettings(
335
+ repo=Path(raw["repo"]).expanduser(),
336
+ remote=raw.get("remote", "origin"),
337
+ branch=raw.get("branch", DEFAULT_BRANCH),
338
+ workdir=Path(workdir).expanduser() if workdir else None,
339
+ timeout=timeout,
340
+ )
341
+
342
+
343
+ def _parse_file(raw: dict) -> FileSettings:
344
+ if not isinstance(raw.get("path"), str) or not raw["path"]:
345
+ raise StateSyncConfigError("'state_sync' (type file) debe ser un mapeo con 'path'")
346
+ return FileSettings(path=Path(raw["path"]).expanduser())
347
+
348
+
349
+ # type name -> (parse raw mapping into settings, build source from settings)
350
+ SOURCE_TYPES: dict = {
351
+ "git": (_parse_git, GitBranchSource.from_settings),
352
+ "file": (_parse_file, FileSource.from_settings),
353
+ }
354
+ DEFAULT_TYPE = "git"
355
+
356
+
357
+ def parse_settings(raw):
358
+ """Validate a `state_sync` mapping and return the backend's settings."""
359
+ if not isinstance(raw, dict):
360
+ raise StateSyncConfigError("'state_sync' debe ser un mapeo")
361
+ kind = raw.get("type", DEFAULT_TYPE)
362
+ if not isinstance(kind, str) or kind not in SOURCE_TYPES:
363
+ raise StateSyncConfigError(
364
+ f"state_sync.type desconocido: {kind!r} (validos: {', '.join(sorted(SOURCE_TYPES))})")
365
+ return SOURCE_TYPES[kind][0](raw)
366
+
367
+
368
+ def build_source(settings, logger=None) -> StateSource:
369
+ """Instantiate the backend that `settings` belongs to."""
370
+ return SOURCE_TYPES[settings.type][1](settings, logger)
@@ -0,0 +1,95 @@
1
+ """Definición declarativa de targets: qué template/paleta renderiza a qué
2
+ archivo(s), con qué modo (overwrite/patch) y qué comando de reload dispara."""
3
+
4
+ from dataclasses import dataclass, field
5
+ from pathlib import Path
6
+
7
+ FAILURE_POLICIES = ("warn", "error")
8
+ MODES = ("overwrite", "patch")
9
+
10
+
11
+ @dataclass
12
+ class PatchSpec:
13
+ strategy: str = "regex_splice" # "regex_splice" | "json_scheme_upsert"
14
+
15
+ # regex_splice, variante "bloque delimitado por un único regex" (ej. una
16
+ # sección TOML entera como [theme.custom] o [palettes.dotfiles]):
17
+ marker_regex: str = None
18
+ insert_after_section_regex: str = None # fallback si el bloque no existe aún
19
+
20
+ # regex_splice, variante "marcadores begin/end literales" (ej. comentarios
21
+ # -- BEGIN ... / -- END ... envolviendo un bloque de código):
22
+ marker_begin: str = None
23
+ marker_end: str = None
24
+ marker_end_regex: str = None # alternativa a marker_end cuando el cierre no es literal
25
+
26
+ # post-procesamiento aplicado sobre el contenido ya parcheado, ANTES de
27
+ # escribir — son datos (contenido específico del usuario), no lógica:
28
+ replacements: list = field(default_factory=list) # [[viejo, nuevo], ...]
29
+ ensure_after: list = field(default_factory=list) # [{if_missing_regex, anchor, insert}, ...]
30
+
31
+ # solo para strategy == "json_scheme_upsert":
32
+ scheme_name: str = None
33
+ palette_section: str = "kitty"
34
+
35
+
36
+ @dataclass
37
+ class Target:
38
+ name: str
39
+ mode: str = "overwrite" # "overwrite" | "patch"
40
+ template: str = None
41
+ palette_key: str = None
42
+ outputs: list = field(default_factory=list)
43
+ reload: str = None
44
+ reload_failure: str = None # None = inherit global; "warn" | "error"
45
+ make_executable: bool = False
46
+ skip_if_missing_parent: bool = False
47
+ # Rutas (archivo o directorio) que deben existir todas para que el target
48
+ # corra; si falta alguna se omite sin escribir ni disparar reload.
49
+ skip_unless_path_exists: list = field(default_factory=list)
50
+ jinja_delimiters: tuple = None
51
+ extra_vars: dict = field(default_factory=dict)
52
+ patch: PatchSpec = None
53
+
54
+ @classmethod
55
+ def from_dict(cls, name: str, data: dict) -> "Target":
56
+ mode = data.get("mode", "overwrite")
57
+ if mode not in MODES:
58
+ raise TypeError(f"{name}: mode debe ser 'overwrite' o 'patch', no {mode!r}")
59
+ patch_data = data.get("patch")
60
+ if patch_data is not None and not isinstance(patch_data, dict):
61
+ raise TypeError(f"{name}: 'patch' debe ser un mapeo")
62
+ if mode == "patch" and not patch_data:
63
+ raise TypeError(f"{name}: mode 'patch' requiere una seccion 'patch'")
64
+ patch = PatchSpec(**patch_data) if patch_data else None
65
+ raw_outputs = data.get("outputs") or []
66
+ if not isinstance(raw_outputs, list) or not all(isinstance(o, str) for o in raw_outputs):
67
+ raise TypeError(f"{name}: 'outputs' debe ser una lista de rutas (strings)")
68
+ delimiters = data.get("jinja_delimiters")
69
+ required = data.get("skip_unless_path_exists") or []
70
+ if isinstance(required, str):
71
+ required = [required]
72
+ policy = data.get("reload_failure")
73
+ if policy is not None and policy not in FAILURE_POLICIES:
74
+ raise TypeError(
75
+ f"{name}: reload_failure debe ser 'warn' o 'error', no {policy!r}"
76
+ )
77
+ return cls(
78
+ name=name,
79
+ mode=mode,
80
+ template=data.get("template"),
81
+ palette_key=data.get("palette_key"),
82
+ outputs=[Path(o).expanduser() for o in raw_outputs],
83
+ reload=data.get("reload"),
84
+ reload_failure=policy,
85
+ make_executable=data.get("make_executable", False),
86
+ skip_if_missing_parent=data.get("skip_if_missing_parent", False),
87
+ skip_unless_path_exists=[Path(p).expanduser() for p in required],
88
+ jinja_delimiters=tuple(delimiters) if delimiters else None,
89
+ extra_vars=data.get("extra_vars", {}),
90
+ patch=patch,
91
+ )
92
+
93
+
94
+ def load_targets(raw: dict) -> list:
95
+ return [Target.from_dict(name, cfg) for name, cfg in raw.items()]
@@ -0,0 +1,102 @@
1
+ meta:
2
+ name: arctic-ice
3
+
4
+ kitty:
5
+ foreground: "#2d3540"
6
+ background: "#eef4f8"
7
+ selection_foreground: "#eef4f8"
8
+ selection_background: "#4a90a4"
9
+ url_color: "#2f6f83"
10
+ color0: "#dce7ee"
11
+ color1: "#ee6c6c"
12
+ color2: "#5fa88f"
13
+ color3: "#d9a441"
14
+ color4: "#4a90a4"
15
+ color5: "#7d6b9e"
16
+ color6: "#2f6f83"
17
+ color7: "#2d3540"
18
+ color8: "#8a97a3"
19
+ color9: "#ee6c6c"
20
+ color10: "#5fa88f"
21
+ color11: "#d9a441"
22
+ color12: "#4a90a4"
23
+ color13: "#7d6b9e"
24
+ color14: "#2f6f83"
25
+ color15: "#45505c"
26
+ cursor: "#4a90a4"
27
+ cursor_text_color: "#eef4f8"
28
+ active_tab_foreground: "#eef4f8"
29
+ active_tab_background: "#4a90a4"
30
+ inactive_tab_foreground: "#8a97a3"
31
+ inactive_tab_background: "#dce7ee"
32
+ tab_bar_background: "#f7fbfd"
33
+ tab_bar_margin_color: "#eef4f8"
34
+ mark1_foreground: "#eef4f8"
35
+ mark1_background: "#ee6c6c"
36
+ active_border_color: "#4a90a4"
37
+ inactive_border_color: "#d1dee6"
38
+ bell_border_color: "#ee6c6c"
39
+ visual_bell_color: "#ee6c6c"
40
+ search_result_foreground: "#eef4f8"
41
+ search_result_background: "#d9a441"
42
+ search_current_match_foreground: "#eef4f8"
43
+ search_current_match_background: "#4a90a4"
44
+
45
+ tmux:
46
+ status_bg: "#f7fbfd"
47
+ status_fg: "#2d3540"
48
+ pane_border_fg: "#d1dee6"
49
+ pane_active_border_fg: "#4a90a4"
50
+ segment_session_bg: "#4a90a4"
51
+ segment_session_fg: "#eef4f8"
52
+ separator_fg: "#8a97a3"
53
+ segment_time_fg: "#2f6f83"
54
+ segment_date_fg: "#7d6b9e"
55
+ segment_user_fg: "#5fa88f"
56
+ segment_battery_fg: "#d9a441"
57
+ segment_alert_fg: "#ee6c6c"
58
+ window_inactive_fg: "#45505c"
59
+
60
+ polybar:
61
+ background: "#eef4f8"
62
+ background_alt: "#f7fbfd"
63
+ foreground: "#2d3540"
64
+ primary: "#4a90a4"
65
+ secondary: "#2f6f83"
66
+ alert: "#ee6c6c"
67
+ success: "#5fa88f"
68
+ warning: "#d9a441"
69
+ muted: "#8a97a3"
70
+
71
+ pi:
72
+ vars:
73
+ orange: "#d9a441"
74
+ black: "#2d3540"
75
+ green: "#5fa88f"
76
+ sand: "#2f6f83"
77
+ brown: "#8a97a3"
78
+ redEarth: "#ee6c6c"
79
+ baseBg: "#eef4f8"
80
+ elevatedBg: "#f7fbfd"
81
+ floatingBg: "#dce7ee"
82
+ textPrimary: "#2d3540"
83
+ textSecondary: "#45505c"
84
+ textMuted: "#8a97a3"
85
+ dimGray: "#c3d2db"
86
+ thinkingColor: "#4a90a4"
87
+
88
+ omp:
89
+ separator_fg: "#8a97a3"
90
+ session_fg: "#d9a441"
91
+ git_fg: "#2f6f83"
92
+ git_working_fg: "#d9a441"
93
+ git_staging_fg: "#5fa88f"
94
+ aws_fg: "#d9a441"
95
+ kubectl_fg: "#2f6f83"
96
+ root_fg: "#ee6c6c"
97
+ battery_fg: "#5fa88f"
98
+ status_fg: "#ee6c6c"
99
+ path_fg: "#4a90a4"
100
+ ticket_fg: "#7d6b9e"
101
+ venv_fg: "#d9a441"
102
+ prompt_char_fg: "#2f6f83"
@@ -0,0 +1,102 @@
1
+ meta:
2
+ name: autumn
3
+
4
+ kitty:
5
+ foreground: "#f0e3cc"
6
+ background: "#1c130b"
7
+ selection_foreground: "#1c130b"
8
+ selection_background: "#d68c45"
9
+ url_color: "#b98d4a"
10
+ color0: "#241a10"
11
+ color1: "#b3462c"
12
+ color2: "#7a8a4f"
13
+ color3: "#d9a441"
14
+ color4: "#5b7b8a"
15
+ color5: "#8a5a54"
16
+ color6: "#6f9a8d"
17
+ color7: "#f0e3cc"
18
+ color8: "#5a4632"
19
+ color9: "#d1684a"
20
+ color10: "#9cae6b"
21
+ color11: "#f0c674"
22
+ color12: "#7fa0ad"
23
+ color13: "#ab7c6d"
24
+ color14: "#8fbdb0"
25
+ color15: "#fbf1de"
26
+ cursor: "#d68c45"
27
+ cursor_text_color: "#1c130b"
28
+ active_tab_foreground: "#1c130b"
29
+ active_tab_background: "#d68c45"
30
+ inactive_tab_foreground: "#a3906f"
31
+ inactive_tab_background: "#2a1e12"
32
+ tab_bar_background: "#241a10"
33
+ tab_bar_margin_color: "#1c130b"
34
+ mark1_foreground: "#1c130b"
35
+ mark1_background: "#b3462c"
36
+ active_border_color: "#d9a441"
37
+ inactive_border_color: "#3d2e1c"
38
+ bell_border_color: "#b3462c"
39
+ visual_bell_color: "#b3462c"
40
+ search_result_foreground: "#1c130b"
41
+ search_result_background: "#f0c674"
42
+ search_current_match_foreground: "#1c130b"
43
+ search_current_match_background: "#d68c45"
44
+
45
+ tmux:
46
+ status_bg: "#1c130b"
47
+ status_fg: "#f0e3cc"
48
+ pane_border_fg: "#3d2e1c"
49
+ pane_active_border_fg: "#d9a441"
50
+ segment_session_bg: "#d68c45"
51
+ segment_session_fg: "#1c130b"
52
+ separator_fg: "#5a4632"
53
+ segment_time_fg: "#d9a441"
54
+ segment_date_fg: "#8a5a54"
55
+ segment_user_fg: "#7a8a4f"
56
+ segment_battery_fg: "#f0c674"
57
+ segment_alert_fg: "#b3462c"
58
+ window_inactive_fg: "#a3906f"
59
+
60
+ polybar:
61
+ background: "#1c130b"
62
+ background_alt: "#2a1e12"
63
+ foreground: "#f0e3cc"
64
+ primary: "#d68c45"
65
+ secondary: "#d9a441"
66
+ alert: "#b3462c"
67
+ success: "#7a8a4f"
68
+ warning: "#f0c674"
69
+ muted: "#5a4632"
70
+
71
+ pi:
72
+ vars:
73
+ orange: "#d68c45"
74
+ black: "#1c130b"
75
+ green: "#7a8a4f"
76
+ sand: "#f0c674"
77
+ brown: "#5a4632"
78
+ redEarth: "#b3462c"
79
+ baseBg: "#1c130b"
80
+ elevatedBg: "#241a10"
81
+ floatingBg: "#2a1e12"
82
+ textPrimary: "#f0e3cc"
83
+ textSecondary: "#d3bd96"
84
+ textMuted: "#a3906f"
85
+ dimGray: "#5a4632"
86
+ thinkingColor: "#d68c45"
87
+
88
+ omp:
89
+ separator_fg: "#5a4632"
90
+ session_fg: "#f0c674"
91
+ git_fg: "#d9a441"
92
+ git_working_fg: "#d68c45"
93
+ git_staging_fg: "#7a8a4f"
94
+ aws_fg: "#d68c45"
95
+ kubectl_fg: "#6f9a8d"
96
+ root_fg: "#b3462c"
97
+ battery_fg: "#7a8a4f"
98
+ status_fg: "#b3462c"
99
+ path_fg: "#d68c45"
100
+ ticket_fg: "#8a5a54"
101
+ venv_fg: "#f0c674"
102
+ prompt_char_fg: "#d9a441"