hueman-cli 0.2.0__tar.gz
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.
- hueman_cli-0.2.0/CONTRIBUTING.md +48 -0
- hueman_cli-0.2.0/LICENSE +21 -0
- hueman_cli-0.2.0/MANIFEST.in +12 -0
- hueman_cli-0.2.0/PKG-INFO +119 -0
- hueman_cli-0.2.0/README.md +87 -0
- hueman_cli-0.2.0/SECURITY.md +23 -0
- hueman_cli-0.2.0/hueman/__init__.py +11 -0
- hueman_cli-0.2.0/hueman/__main__.py +3 -0
- hueman_cli-0.2.0/hueman/cli.py +353 -0
- hueman_cli-0.2.0/hueman/data/hueman.config.example.yaml +111 -0
- hueman_cli-0.2.0/hueman/engine/__init__.py +0 -0
- hueman_cli-0.2.0/hueman/engine/colors.py +30 -0
- hueman_cli-0.2.0/hueman/engine/config.py +186 -0
- hueman_cli-0.2.0/hueman/engine/fsutil.py +41 -0
- hueman_cli-0.2.0/hueman/engine/palettes.py +66 -0
- hueman_cli-0.2.0/hueman/engine/reload.py +60 -0
- hueman_cli-0.2.0/hueman/engine/render.py +243 -0
- hueman_cli-0.2.0/hueman/engine/statesource.py +370 -0
- hueman_cli-0.2.0/hueman/engine/targets.py +95 -0
- hueman_cli-0.2.0/hueman/palettes/arctic-ice.yaml +102 -0
- hueman_cli-0.2.0/hueman/palettes/autumn.yaml +102 -0
- hueman_cli-0.2.0/hueman/palettes/catppuccin-latte.yaml +102 -0
- hueman_cli-0.2.0/hueman/palettes/catppuccin-mocha.yaml +102 -0
- hueman_cli-0.2.0/hueman/palettes/copperwave.yaml +103 -0
- hueman_cli-0.2.0/hueman/palettes/crimson-vivid.yaml +111 -0
- hueman_cli-0.2.0/hueman/palettes/dracula.yaml +102 -0
- hueman_cli-0.2.0/hueman/palettes/ember-red.yaml +111 -0
- hueman_cli-0.2.0/hueman/palettes/everforest-dark.yaml +102 -0
- hueman_cli-0.2.0/hueman/palettes/galaxy.yaml +102 -0
- hueman_cli-0.2.0/hueman/palettes/gruvbox-light.yaml +102 -0
- hueman_cli-0.2.0/hueman/palettes/gruvbox.yaml +102 -0
- hueman_cli-0.2.0/hueman/palettes/kanagawa-dragon.yaml +102 -0
- hueman_cli-0.2.0/hueman/palettes/matrix.yaml +102 -0
- hueman_cli-0.2.0/hueman/palettes/mono-contrast.yaml +102 -0
- hueman_cli-0.2.0/hueman/palettes/nord.yaml +102 -0
- hueman_cli-0.2.0/hueman/palettes/ocean-deep.yaml +102 -0
- hueman_cli-0.2.0/hueman/palettes/one-light.yaml +102 -0
- hueman_cli-0.2.0/hueman/palettes/onyx-gold.yaml +103 -0
- hueman_cli-0.2.0/hueman/palettes/rose-pine-dawn.yaml +102 -0
- hueman_cli-0.2.0/hueman/palettes/solarized-light.yaml +102 -0
- hueman_cli-0.2.0/hueman/palettes/synthwave.yaml +102 -0
- hueman_cli-0.2.0/hueman/palettes/tokyonight.yaml +106 -0
- hueman_cli-0.2.0/hueman/palettes/tuxedo.yaml +103 -0
- hueman_cli-0.2.0/hueman/palettes/vaporwave.yaml +102 -0
- hueman_cli-0.2.0/hueman/resources.py +18 -0
- hueman_cli-0.2.0/hueman/templates/atuin_theme.toml.j2 +19 -0
- hueman_cli-0.2.0/hueman/templates/bottom_theme.toml.j2 +43 -0
- hueman_cli-0.2.0/hueman/templates/bspwm_colors.conf.j2 +5 -0
- hueman_cli-0.2.0/hueman/templates/claude_statusline.sh.j2 +216 -0
- hueman_cli-0.2.0/hueman/templates/dunst_colors.conf.j2 +15 -0
- hueman_cli-0.2.0/hueman/templates/fzf_opts.j2 +9 -0
- hueman_cli-0.2.0/hueman/templates/fzf_theme.j2 +17 -0
- hueman_cli-0.2.0/hueman/templates/herdr_theme_custom.toml.j2 +11 -0
- hueman_cli-0.2.0/hueman/templates/k9s_theme.yaml.j2 +83 -0
- hueman_cli-0.2.0/hueman/templates/kitty.conf.j2 +51 -0
- hueman_cli-0.2.0/hueman/templates/lazydocker_theme.yml.j2 +19 -0
- hueman_cli-0.2.0/hueman/templates/leaf_theme.toml.j2 +89 -0
- hueman_cli-0.2.0/hueman/templates/newt_colors.j2 +32 -0
- hueman_cli-0.2.0/hueman/templates/nvim_nightfox_theme.lua.j2 +70 -0
- hueman_cli-0.2.0/hueman/templates/omp.omp.json.j2 +107 -0
- hueman_cli-0.2.0/hueman/templates/opencode_theme.json.j2 +71 -0
- hueman_cli-0.2.0/hueman/templates/pi_theme.json.j2 +65 -0
- hueman_cli-0.2.0/hueman/templates/polybar_colors.ini.j2 +11 -0
- hueman_cli-0.2.0/hueman/templates/posting_theme.yaml.j2 +47 -0
- hueman_cli-0.2.0/hueman/templates/qutebrowser_github.user.js.j2 +121 -0
- hueman_cli-0.2.0/hueman/templates/qutebrowser_google.user.js.j2 +132 -0
- hueman_cli-0.2.0/hueman/templates/qutebrowser_hteam.user.js.j2 +962 -0
- hueman_cli-0.2.0/hueman/templates/qutebrowser_theme.py.j2 +78 -0
- hueman_cli-0.2.0/hueman/templates/rofi_theme.rasi.j2 +132 -0
- hueman_cli-0.2.0/hueman/templates/sshub_theme.toml.j2 +49 -0
- hueman_cli-0.2.0/hueman/templates/starship_block.toml.j2 +11 -0
- hueman_cli-0.2.0/hueman/templates/termux_colors.properties.j2 +20 -0
- hueman_cli-0.2.0/hueman/templates/tmux.conf.j2 +33 -0
- hueman_cli-0.2.0/hueman/templates/tuicr_theme.toml.j2 +51 -0
- hueman_cli-0.2.0/hueman/templates/wezterm_colors.lua.j2 +14 -0
- hueman_cli-0.2.0/hueman/templates/windhawk_taskbar.json.j2 +4 -0
- hueman_cli-0.2.0/hueman/templates/yazi_theme.toml.j2 +130 -0
- hueman_cli-0.2.0/hueman/templates/zen_theme.json.j2 +11 -0
- hueman_cli-0.2.0/hueman_cli.egg-info/PKG-INFO +119 -0
- hueman_cli-0.2.0/hueman_cli.egg-info/SOURCES.txt +112 -0
- hueman_cli-0.2.0/hueman_cli.egg-info/dependency_links.txt +1 -0
- hueman_cli-0.2.0/hueman_cli.egg-info/entry_points.txt +2 -0
- hueman_cli-0.2.0/hueman_cli.egg-info/requires.txt +5 -0
- hueman_cli-0.2.0/hueman_cli.egg-info/scm_file_list.json +205 -0
- hueman_cli-0.2.0/hueman_cli.egg-info/scm_version.json +8 -0
- hueman_cli-0.2.0/hueman_cli.egg-info/top_level.txt +1 -0
- hueman_cli-0.2.0/pyproject.toml +61 -0
- hueman_cli-0.2.0/requirements.txt +3 -0
- hueman_cli-0.2.0/setup.cfg +4 -0
- hueman_cli-0.2.0/tests/conftest.py +12 -0
- hueman_cli-0.2.0/tests/test_atomic_writes.py +179 -0
- hueman_cli-0.2.0/tests/test_colors_and_cli.py +145 -0
- hueman_cli-0.2.0/tests/test_command_substitution.py +51 -0
- hueman_cli-0.2.0/tests/test_config_and_palettes.py +104 -0
- hueman_cli-0.2.0/tests/test_config_validation.py +156 -0
- hueman_cli-0.2.0/tests/test_config_vars.py +228 -0
- hueman_cli-0.2.0/tests/test_error_paths.py +68 -0
- hueman_cli-0.2.0/tests/test_file_source.py +127 -0
- hueman_cli-0.2.0/tests/test_idempotence.py +85 -0
- hueman_cli-0.2.0/tests/test_init_command.py +87 -0
- hueman_cli-0.2.0/tests/test_packaging.py +60 -0
- hueman_cli-0.2.0/tests/test_palette_names.py +112 -0
- hueman_cli-0.2.0/tests/test_real_config.py +52 -0
- hueman_cli-0.2.0/tests/test_reload_exit_codes.py +240 -0
- hueman_cli-0.2.0/tests/test_render.py +401 -0
- hueman_cli-0.2.0/tests/test_render_modes.py +252 -0
- hueman_cli-0.2.0/tests/test_sandbox.py +71 -0
- hueman_cli-0.2.0/tests/test_starship_marker.py +71 -0
- hueman_cli-0.2.0/tests/test_state_and_reload.py +125 -0
- hueman_cli-0.2.0/tests/test_state_branch.py +224 -0
- hueman_cli-0.2.0/tests/test_state_source_contract.py +102 -0
- hueman_cli-0.2.0/tests/test_sync_network.py +76 -0
- hueman_cli-0.2.0/tests/test_sync_status.py +281 -0
- hueman_cli-0.2.0/tests/test_termux_template.py +61 -0
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# Contributing
|
|
2
|
+
|
|
3
|
+
## Setup
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
pip install -e ".[dev]"
|
|
7
|
+
python -m pytest -q
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
## Workflow
|
|
11
|
+
|
|
12
|
+
- Branch from `dev` (`feat/...`, `fix/...`, `docs/...`) and open the PR against `dev`.
|
|
13
|
+
- `main` only carries releases; do not target it.
|
|
14
|
+
- Use [Conventional Commits](https://www.conventionalcommits.org/)
|
|
15
|
+
(`feat:`, `fix:`, `docs:`, `ci:`, `chore:`, `test:`).
|
|
16
|
+
- `CHANGELOG.md` is generated from commit messages. Do not edit it by hand.
|
|
17
|
+
|
|
18
|
+
## Tests
|
|
19
|
+
|
|
20
|
+
Behavior changes need tests. Writing the failing test first is welcome.
|
|
21
|
+
Run `python -m pytest -q` before opening the PR.
|
|
22
|
+
|
|
23
|
+
## Documentation
|
|
24
|
+
|
|
25
|
+
The docs live in `website/content/latest` (written in Spanish).
|
|
26
|
+
|
|
27
|
+
- Add a tool or target: [add-a-tool](website/content/latest/guides/add-a-tool.md)
|
|
28
|
+
- Add a palette: [add-a-palette](website/content/latest/guides/add-a-palette.md)
|
|
29
|
+
|
|
30
|
+
Targets stay declarative: tool-specific logic belongs in YAML and Jinja
|
|
31
|
+
templates, not in Python.
|
|
32
|
+
|
|
33
|
+
### Freezing docs for a release
|
|
34
|
+
|
|
35
|
+
The site is static (`website/`, no build step). `content/latest` is the editable
|
|
36
|
+
tip; each release gets a frozen copy. Page ids in `content/*/nav.json` are used
|
|
37
|
+
as URLs (`?p=<id>`), so do not rename them.
|
|
38
|
+
|
|
39
|
+
1. Cut the release as usual.
|
|
40
|
+
2. Copy `website/content/latest/` to `website/content/vX.Y.Z/`.
|
|
41
|
+
3. Add an entry to `website/data/versions.json` with `channel: release`, the
|
|
42
|
+
`semver` and the release date as `updated`; bump `updated` on `latest`.
|
|
43
|
+
4. Check that every nav id resolves, internal links work and the changelog has a
|
|
44
|
+
section for that tag.
|
|
45
|
+
|
|
46
|
+
## Security
|
|
47
|
+
|
|
48
|
+
Report vulnerabilities privately; see [SECURITY.md](SECURITY.md).
|
hueman_cli-0.2.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 4DRIAN0RTIZ
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
include LICENSE README.md
|
|
2
|
+
recursive-include hueman/data *.yaml
|
|
3
|
+
recursive-include hueman/palettes *.yaml
|
|
4
|
+
recursive-include hueman/templates *.j2
|
|
5
|
+
recursive-include tests *.py
|
|
6
|
+
global-exclude __pycache__ *.py[cod]
|
|
7
|
+
prune website
|
|
8
|
+
prune openwiki
|
|
9
|
+
prune skills
|
|
10
|
+
prune .github
|
|
11
|
+
prune examples
|
|
12
|
+
exclude CHANGELOG.md cliff.toml .gitignore
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: hueman-cli
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Centralized theme manager: one YAML palette feeds Jinja2 templates that generate each tool's config.
|
|
5
|
+
Author: 4DRIAN0RTIZ
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://hueman.cuevaneander.tech
|
|
8
|
+
Project-URL: Source, https://github.com/NeanderTeam/hueman
|
|
9
|
+
Project-URL: Issues, https://github.com/NeanderTeam/hueman/issues
|
|
10
|
+
Keywords: theme,colors,palette,dotfiles,jinja2,terminal,cli
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Topic :: Desktop Environment
|
|
23
|
+
Classifier: Topic :: Utilities
|
|
24
|
+
Requires-Python: >=3.9
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
License-File: LICENSE
|
|
27
|
+
Requires-Dist: Jinja2>=3.1.6
|
|
28
|
+
Requires-Dist: PyYAML>=6.0.1
|
|
29
|
+
Provides-Extra: dev
|
|
30
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
31
|
+
Dynamic: license-file
|
|
32
|
+
|
|
33
|
+
# hueman
|
|
34
|
+
|
|
35
|
+
Gestor centralizado de temas: una **paleta de color (YAML)** alimenta
|
|
36
|
+
**templates Jinja2** que generan la configuración real de tus herramientas
|
|
37
|
+
(kitty, tmux, nvim, starship, Windows Terminal, lo que declares en tu propio
|
|
38
|
+
config). Sin dependencias de ningún gestor de dotfiles: todo el mapeo de rutas
|
|
39
|
+
de salida vive en un `hueman.config.yaml` propio, fuera de este repo.
|
|
40
|
+
|
|
41
|
+
## Quickstart
|
|
42
|
+
|
|
43
|
+
```sh
|
|
44
|
+
pipx install hueman-cli # o: pip install hueman-cli
|
|
45
|
+
hueman init # escribe un config de ejemplo en ~/.config/hueman/config.yaml
|
|
46
|
+
# ajusta tus targets en ese archivo, luego:
|
|
47
|
+
hueman set <paleta> --dry-run
|
|
48
|
+
hueman set <paleta>
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Desde el código fuente (requiere Python >= 3.9):
|
|
52
|
+
|
|
53
|
+
```sh
|
|
54
|
+
git clone https://github.com/NeanderTeam/hueman.git
|
|
55
|
+
cd hueman && pip install -e .
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Actualizar: `pipx upgrade hueman-cli` (o `pip install -U hueman-cli`).
|
|
59
|
+
Desinstalar: `pipx uninstall hueman-cli` (o `pip uninstall hueman-cli`); tu config
|
|
60
|
+
y los archivos generados no se borran (detalle en la
|
|
61
|
+
[guía de instalación](https://hueman.cuevaneander.tech/?p=install)).
|
|
62
|
+
|
|
63
|
+
Otros comandos: `init` (config de ejemplo), `list` (paletas), `status` (tema activo) y `sync` (estado
|
|
64
|
+
compartido entre máquinas).
|
|
65
|
+
|
|
66
|
+
## Ideas centrales
|
|
67
|
+
|
|
68
|
+
- Una paleta, muchos targets; el mapeo de salidas es declarativo.
|
|
69
|
+
- Templates y paletas empaquetados son defaults; los directorios custom ganan.
|
|
70
|
+
- `overwrite` para archivos generados enteros, `patch` para bloques dentro de
|
|
71
|
+
configs editadas a mano.
|
|
72
|
+
- `reload` por target y `post_apply` global, best-effort, con política
|
|
73
|
+
`warn` / `error`.
|
|
74
|
+
- Sync opcional de la paleta activa entre máquinas (`state_sync`).
|
|
75
|
+
- Agregar una tool nueva es un template + un target, no un cambio al motor.
|
|
76
|
+
|
|
77
|
+
## Herramientas empaquetadas
|
|
78
|
+
|
|
79
|
+
`hueman/palettes/` y `hueman/templates/` traen 25 paletas estéticas genéricas y templates
|
|
80
|
+
para las herramientas más comunes (kitty, tmux, polybar, nvim/nightfox, dunst,
|
|
81
|
+
bspwm, yazi, qutebrowser, rofi, fzf, atuin, oh-my-posh, wezterm, starship,
|
|
82
|
+
Termux). Para targets idiosincráticos usa `template_dirs` en tu config en vez
|
|
83
|
+
de pedir que se agreguen aquí.
|
|
84
|
+
|
|
85
|
+
## Documentación
|
|
86
|
+
|
|
87
|
+
Documentación completa: **[hueman.cuevaneander.tech](https://hueman.cuevaneander.tech)**
|
|
88
|
+
|
|
89
|
+
Para verla en local (las fuentes están en `website/`):
|
|
90
|
+
|
|
91
|
+
```sh
|
|
92
|
+
python3 -m http.server 8765 --directory website
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
y abre `http://localhost:8765`.
|
|
96
|
+
|
|
97
|
+
## Soporte
|
|
98
|
+
|
|
99
|
+
¿Un bug o una idea? Abre un issue en
|
|
100
|
+
[github.com/NeanderTeam/hueman/issues](https://github.com/NeanderTeam/hueman/issues).
|
|
101
|
+
|
|
102
|
+
## Estructura del repo
|
|
103
|
+
|
|
104
|
+
| Ruta | Contenido |
|
|
105
|
+
|---|---|
|
|
106
|
+
| `hueman/cli.py` | CLI y orquestación (comando `hueman`) |
|
|
107
|
+
| `hueman/engine/` | Config, paletas, targets, render, reload, estado compartido |
|
|
108
|
+
| `hueman/palettes/`, `hueman/templates/` | Paletas YAML y plantillas Jinja2 empaquetadas |
|
|
109
|
+
| `hueman/data/hueman.config.example.yaml` | Ejemplo comentado de config externa (lo escribe `hueman init`) |
|
|
110
|
+
| `tests/` | Suite de tests |
|
|
111
|
+
| `website/` | Sitio de documentación |
|
|
112
|
+
|
|
113
|
+
El repo es autocontenido: nada dentro importa código fuera de sí mismo, salvo
|
|
114
|
+
el config externo que cada usuario provee. Un repo de dotfiles solo necesita
|
|
115
|
+
tener `hueman` instalado y su propio `hueman.config.yaml`.
|
|
116
|
+
|
|
117
|
+
## Licencia
|
|
118
|
+
|
|
119
|
+
MIT, ver [LICENSE](LICENSE).
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# hueman
|
|
2
|
+
|
|
3
|
+
Gestor centralizado de temas: una **paleta de color (YAML)** alimenta
|
|
4
|
+
**templates Jinja2** que generan la configuración real de tus herramientas
|
|
5
|
+
(kitty, tmux, nvim, starship, Windows Terminal, lo que declares en tu propio
|
|
6
|
+
config). Sin dependencias de ningún gestor de dotfiles: todo el mapeo de rutas
|
|
7
|
+
de salida vive en un `hueman.config.yaml` propio, fuera de este repo.
|
|
8
|
+
|
|
9
|
+
## Quickstart
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
pipx install hueman-cli # o: pip install hueman-cli
|
|
13
|
+
hueman init # escribe un config de ejemplo en ~/.config/hueman/config.yaml
|
|
14
|
+
# ajusta tus targets en ese archivo, luego:
|
|
15
|
+
hueman set <paleta> --dry-run
|
|
16
|
+
hueman set <paleta>
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Desde el código fuente (requiere Python >= 3.9):
|
|
20
|
+
|
|
21
|
+
```sh
|
|
22
|
+
git clone https://github.com/NeanderTeam/hueman.git
|
|
23
|
+
cd hueman && pip install -e .
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Actualizar: `pipx upgrade hueman-cli` (o `pip install -U hueman-cli`).
|
|
27
|
+
Desinstalar: `pipx uninstall hueman-cli` (o `pip uninstall hueman-cli`); tu config
|
|
28
|
+
y los archivos generados no se borran (detalle en la
|
|
29
|
+
[guía de instalación](https://hueman.cuevaneander.tech/?p=install)).
|
|
30
|
+
|
|
31
|
+
Otros comandos: `init` (config de ejemplo), `list` (paletas), `status` (tema activo) y `sync` (estado
|
|
32
|
+
compartido entre máquinas).
|
|
33
|
+
|
|
34
|
+
## Ideas centrales
|
|
35
|
+
|
|
36
|
+
- Una paleta, muchos targets; el mapeo de salidas es declarativo.
|
|
37
|
+
- Templates y paletas empaquetados son defaults; los directorios custom ganan.
|
|
38
|
+
- `overwrite` para archivos generados enteros, `patch` para bloques dentro de
|
|
39
|
+
configs editadas a mano.
|
|
40
|
+
- `reload` por target y `post_apply` global, best-effort, con política
|
|
41
|
+
`warn` / `error`.
|
|
42
|
+
- Sync opcional de la paleta activa entre máquinas (`state_sync`).
|
|
43
|
+
- Agregar una tool nueva es un template + un target, no un cambio al motor.
|
|
44
|
+
|
|
45
|
+
## Herramientas empaquetadas
|
|
46
|
+
|
|
47
|
+
`hueman/palettes/` y `hueman/templates/` traen 25 paletas estéticas genéricas y templates
|
|
48
|
+
para las herramientas más comunes (kitty, tmux, polybar, nvim/nightfox, dunst,
|
|
49
|
+
bspwm, yazi, qutebrowser, rofi, fzf, atuin, oh-my-posh, wezterm, starship,
|
|
50
|
+
Termux). Para targets idiosincráticos usa `template_dirs` en tu config en vez
|
|
51
|
+
de pedir que se agreguen aquí.
|
|
52
|
+
|
|
53
|
+
## Documentación
|
|
54
|
+
|
|
55
|
+
Documentación completa: **[hueman.cuevaneander.tech](https://hueman.cuevaneander.tech)**
|
|
56
|
+
|
|
57
|
+
Para verla en local (las fuentes están en `website/`):
|
|
58
|
+
|
|
59
|
+
```sh
|
|
60
|
+
python3 -m http.server 8765 --directory website
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
y abre `http://localhost:8765`.
|
|
64
|
+
|
|
65
|
+
## Soporte
|
|
66
|
+
|
|
67
|
+
¿Un bug o una idea? Abre un issue en
|
|
68
|
+
[github.com/NeanderTeam/hueman/issues](https://github.com/NeanderTeam/hueman/issues).
|
|
69
|
+
|
|
70
|
+
## Estructura del repo
|
|
71
|
+
|
|
72
|
+
| Ruta | Contenido |
|
|
73
|
+
|---|---|
|
|
74
|
+
| `hueman/cli.py` | CLI y orquestación (comando `hueman`) |
|
|
75
|
+
| `hueman/engine/` | Config, paletas, targets, render, reload, estado compartido |
|
|
76
|
+
| `hueman/palettes/`, `hueman/templates/` | Paletas YAML y plantillas Jinja2 empaquetadas |
|
|
77
|
+
| `hueman/data/hueman.config.example.yaml` | Ejemplo comentado de config externa (lo escribe `hueman init`) |
|
|
78
|
+
| `tests/` | Suite de tests |
|
|
79
|
+
| `website/` | Sitio de documentación |
|
|
80
|
+
|
|
81
|
+
El repo es autocontenido: nada dentro importa código fuera de sí mismo, salvo
|
|
82
|
+
el config externo que cada usuario provee. Un repo de dotfiles solo necesita
|
|
83
|
+
tener `hueman` instalado y su propio `hueman.config.yaml`.
|
|
84
|
+
|
|
85
|
+
## Licencia
|
|
86
|
+
|
|
87
|
+
MIT, ver [LICENSE](LICENSE).
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Security Policy
|
|
2
|
+
|
|
3
|
+
## Supported versions
|
|
4
|
+
|
|
5
|
+
Only the latest release receives security fixes.
|
|
6
|
+
|
|
7
|
+
## Reporting a vulnerability
|
|
8
|
+
|
|
9
|
+
Please do not open a public issue. Report it privately through GitHub's
|
|
10
|
+
private vulnerability reporting: open the repository's **Security** tab and
|
|
11
|
+
choose **Report a vulnerability**.
|
|
12
|
+
|
|
13
|
+
Include the affected version, steps to reproduce, and the impact you see.
|
|
14
|
+
You can expect an acknowledgement and a first assessment; fixes land in a
|
|
15
|
+
patch release once confirmed, and you will be credited if you wish.
|
|
16
|
+
|
|
17
|
+
## Scope
|
|
18
|
+
|
|
19
|
+
hueman writes files and runs best-effort `reload` / `post_apply` commands
|
|
20
|
+
that are defined in the user's own configuration. That configuration is
|
|
21
|
+
trusted input: running commands you wrote yourself is not a vulnerability.
|
|
22
|
+
Issues such as path escapes from palette or template names, or commands
|
|
23
|
+
executed from untrusted palette data, are in scope.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""hueman: gestor centralizado de temas basado en paletas YAML y plantillas Jinja2."""
|
|
2
|
+
|
|
3
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
4
|
+
|
|
5
|
+
try:
|
|
6
|
+
# Nombre de la distribución en PyPI (único lugar a cambiar junto con pyproject.toml).
|
|
7
|
+
__version__ = version("hueman-cli")
|
|
8
|
+
except PackageNotFoundError: # checkout sin instalar
|
|
9
|
+
__version__ = "0+unknown"
|
|
10
|
+
|
|
11
|
+
__all__ = ["__version__"]
|
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
"""Gestor centralizado de temas (standalone): una paleta por nombre en
|
|
2
|
+
palettes/*.yaml (empaquetadas o custom) alimenta templates Jinja2 que
|
|
3
|
+
generan la config real de cada herramienta declarada en un
|
|
4
|
+
hueman.config.yaml propio del usuario. No depende de ningún gestor de
|
|
5
|
+
dotfiles externo — todo el mapeo de rutas es explícito en ese config.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import argparse
|
|
9
|
+
import fnmatch
|
|
10
|
+
import logging
|
|
11
|
+
import sys
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
from . import __version__
|
|
15
|
+
from .engine.config import VAR_NAME_RE, ConfigError, HuemanConfig
|
|
16
|
+
from .engine.fsutil import atomic_write_text
|
|
17
|
+
from .engine.palettes import PaletteLoader
|
|
18
|
+
from .engine.reload import PlaceholderError, expand_command, run_post_apply, run_reload
|
|
19
|
+
from .engine.statesource import ReadResult, State, build_source
|
|
20
|
+
from .engine.render import TargetRenderError, make_jinja_env, render_target
|
|
21
|
+
from .resources import packaged_example_config, packaged_palettes_dir, packaged_templates_dir
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class UnknownTargetError(ValueError):
|
|
25
|
+
"""Un patron de --only no coincide con ningun target."""
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class Hueman:
|
|
29
|
+
def __init__(self, config: HuemanConfig):
|
|
30
|
+
self.config = config
|
|
31
|
+
self.palettes = PaletteLoader(packaged_palettes_dir(), config.palette_dirs, config.logger)
|
|
32
|
+
self._jinja_envs = {}
|
|
33
|
+
|
|
34
|
+
def _jinja_env(self, delimiters):
|
|
35
|
+
key = delimiters or ()
|
|
36
|
+
if key not in self._jinja_envs:
|
|
37
|
+
self._jinja_envs[key] = make_jinja_env(
|
|
38
|
+
self.config.template_dirs, packaged_templates_dir(), delimiters
|
|
39
|
+
)
|
|
40
|
+
return self._jinja_envs[key]
|
|
41
|
+
|
|
42
|
+
def list_palettes(self) -> list:
|
|
43
|
+
return self.palettes.list_palettes()
|
|
44
|
+
|
|
45
|
+
def current_theme(self):
|
|
46
|
+
return self.config.state_file.read_text().strip() if self.config.state_file.exists() else None
|
|
47
|
+
|
|
48
|
+
def _select_targets(self, only):
|
|
49
|
+
targets = self.config.targets
|
|
50
|
+
if not only:
|
|
51
|
+
return targets
|
|
52
|
+
names = [t.name for t in targets]
|
|
53
|
+
missing = [p for p in only if not fnmatch.filter(names, p)]
|
|
54
|
+
if missing:
|
|
55
|
+
raise UnknownTargetError(
|
|
56
|
+
f"--only sin coincidencias: {', '.join(missing)}. "
|
|
57
|
+
f"Targets validos: {', '.join(names)}"
|
|
58
|
+
)
|
|
59
|
+
return [t for t in targets if any(fnmatch.fnmatchcase(t.name, p) for p in only)]
|
|
60
|
+
|
|
61
|
+
def set_theme(self, name: str, dry_run: bool = False, only=None, publish: bool = True) -> list:
|
|
62
|
+
"""Con `only` (patrones fnmatch) se renderizan solo los targets que
|
|
63
|
+
coinciden y se dispara solo su reload; NO se escribe el state file ni
|
|
64
|
+
corre post_apply (es una re-renderizacion parcial, no un cambio de tema).
|
|
65
|
+
|
|
66
|
+
Un target cuyo render falla por una clave ausente en la paleta se
|
|
67
|
+
omite con un warning y el resto continua; devuelve los nombres de los
|
|
68
|
+
targets fallidos (el CLI sale con codigo 1 si hay alguno).
|
|
69
|
+
|
|
70
|
+
`publish=False` (usado por `sync`) aplica sin publicar estado nuevo."""
|
|
71
|
+
selected = self._select_targets(only)
|
|
72
|
+
palette = self.palettes.load(name)
|
|
73
|
+
# Validar placeholders de reload/post_apply antes de escribir nada.
|
|
74
|
+
for target in selected:
|
|
75
|
+
if target.reload:
|
|
76
|
+
expand_command(target.reload, name, palette)
|
|
77
|
+
post_apply = None
|
|
78
|
+
if not only:
|
|
79
|
+
post_apply = [expand_command(c, name, palette) for c in self.config.post_apply]
|
|
80
|
+
failed = []
|
|
81
|
+
for target in selected:
|
|
82
|
+
env = self._jinja_env(target.jinja_delimiters)
|
|
83
|
+
try:
|
|
84
|
+
wrote = render_target(env, target, name, palette, dry_run, self.config.logger)
|
|
85
|
+
except TargetRenderError as exc:
|
|
86
|
+
self.config.logger.warning(f"{exc}; target omitido")
|
|
87
|
+
failed.append(target.name)
|
|
88
|
+
continue
|
|
89
|
+
if not dry_run and wrote and target.reload:
|
|
90
|
+
cmd = expand_command(target.reload, name, palette)
|
|
91
|
+
rc = run_reload(cmd, self.config.logger)
|
|
92
|
+
if rc:
|
|
93
|
+
policy = target.reload_failure or self.config.reload_failure
|
|
94
|
+
self.config.logger.log(
|
|
95
|
+
logging.ERROR if policy == "error" else logging.WARNING,
|
|
96
|
+
f"{target.name}: reload fallo (exit {rc}): {cmd}",
|
|
97
|
+
)
|
|
98
|
+
if policy == "error":
|
|
99
|
+
failed.append(target.name)
|
|
100
|
+
|
|
101
|
+
if only:
|
|
102
|
+
return failed
|
|
103
|
+
|
|
104
|
+
if dry_run:
|
|
105
|
+
for cmd in post_apply:
|
|
106
|
+
print(f"[DRY RUN] post_apply: {cmd}")
|
|
107
|
+
return failed
|
|
108
|
+
|
|
109
|
+
self.config.state_file.parent.mkdir(parents=True, exist_ok=True)
|
|
110
|
+
atomic_write_text(self.config.state_file, name + "\n")
|
|
111
|
+
self.config.logger.info(f"Tema activo: {name}")
|
|
112
|
+
post_failed = run_post_apply(post_apply, self.config.logger) or []
|
|
113
|
+
for cmd, rc in post_failed:
|
|
114
|
+
error = self.config.reload_failure == "error"
|
|
115
|
+
self.config.logger.log(
|
|
116
|
+
logging.ERROR if error else logging.WARNING,
|
|
117
|
+
f"post_apply fallo (exit {rc}): {cmd}",
|
|
118
|
+
)
|
|
119
|
+
if post_failed and self.config.reload_failure == "error":
|
|
120
|
+
failed.append("post_apply")
|
|
121
|
+
if publish:
|
|
122
|
+
self._publish_state(name)
|
|
123
|
+
return failed
|
|
124
|
+
|
|
125
|
+
@property
|
|
126
|
+
def revision_file(self) -> Path:
|
|
127
|
+
return self.config.state_file.with_name(self.config.state_file.name + ".revision")
|
|
128
|
+
|
|
129
|
+
def local_revision(self):
|
|
130
|
+
"""Revision of the shared state this node last applied or published.
|
|
131
|
+
|
|
132
|
+
Kept beside the plain state file (which only holds the palette name)
|
|
133
|
+
so the legacy format is untouched. None when absent or unreadable."""
|
|
134
|
+
try:
|
|
135
|
+
value = int(self.revision_file.read_text().strip())
|
|
136
|
+
except (OSError, ValueError):
|
|
137
|
+
return None
|
|
138
|
+
return value if value >= 0 else None
|
|
139
|
+
|
|
140
|
+
def _save_revision(self, revision: int) -> None:
|
|
141
|
+
atomic_write_text(self.revision_file, f"{revision}\n")
|
|
142
|
+
|
|
143
|
+
def _publish_state(self, name: str) -> None:
|
|
144
|
+
"""Best-effort: share the active palette; never raises or fails `set`."""
|
|
145
|
+
settings = self.config.state_sync
|
|
146
|
+
if settings is None:
|
|
147
|
+
return
|
|
148
|
+
try:
|
|
149
|
+
source = build_source(settings, self.config.logger)
|
|
150
|
+
current = source.read()
|
|
151
|
+
base = max(current.revision if current else 0, self.local_revision() or 0)
|
|
152
|
+
revision = base + 1
|
|
153
|
+
if source.write(State(name, revision)):
|
|
154
|
+
self._save_revision(revision)
|
|
155
|
+
else:
|
|
156
|
+
self.config.logger.warning("estado compartido no publicado; paleta aplicada solo en local")
|
|
157
|
+
except Exception as exc: # noqa: BLE001 - sync must never break `set`
|
|
158
|
+
self.config.logger.warning(f"estado compartido no publicado: {exc}")
|
|
159
|
+
|
|
160
|
+
def sync(self):
|
|
161
|
+
"""Pull the shared state and apply it only if its revision is newer.
|
|
162
|
+
|
|
163
|
+
Returns (exit_code, outcome). Safe for shell startup: no change means
|
|
164
|
+
no writes and no reload; an unreachable remote is not an error (exit 0)
|
|
165
|
+
and cached data is never applied."""
|
|
166
|
+
settings = self.config.state_sync
|
|
167
|
+
if settings is None:
|
|
168
|
+
self.config.logger.warning("sync: 'state_sync' no configurado; nada que hacer")
|
|
169
|
+
return 0, "disabled"
|
|
170
|
+
try:
|
|
171
|
+
res = build_source(settings, self.config.logger).read_result()
|
|
172
|
+
except Exception as exc: # noqa: BLE001 - sync must not crash shell startup
|
|
173
|
+
self.config.logger.warning(f"sync: estado compartido ilegible: {exc}")
|
|
174
|
+
return 0, "unreachable"
|
|
175
|
+
if not res.remote:
|
|
176
|
+
return 0, "unreachable"
|
|
177
|
+
remote = res.state
|
|
178
|
+
if remote is None:
|
|
179
|
+
return 0, "no-remote-state"
|
|
180
|
+
local = self.local_revision() or 0
|
|
181
|
+
if remote.revision == local:
|
|
182
|
+
return 0, "in-sync"
|
|
183
|
+
if remote.revision < local:
|
|
184
|
+
return 0, "ahead"
|
|
185
|
+
self.config.logger.info(f"sync: r{local} -> r{remote.revision} ({remote.palette})")
|
|
186
|
+
try:
|
|
187
|
+
failed = self.set_theme(remote.palette, publish=False)
|
|
188
|
+
except PlaceholderError as e:
|
|
189
|
+
# set_theme valida antes de escribir: no hay nada aplicado ni revision que guardar
|
|
190
|
+
print(f"Config invalida: {e}", file=sys.stderr)
|
|
191
|
+
return 1, "failed"
|
|
192
|
+
except SystemExit:
|
|
193
|
+
# load() ya logueo el motivo; la paleta remota no existe aqui (o es invalida).
|
|
194
|
+
# Sync no debe matar el arranque del shell ni avanzar la revision.
|
|
195
|
+
self.config.logger.warning(
|
|
196
|
+
f"sync: la paleta remota '{remote.palette}' no esta disponible localmente; "
|
|
197
|
+
"se omite")
|
|
198
|
+
return 0, "unknown-palette"
|
|
199
|
+
if failed:
|
|
200
|
+
print(f"Targets con errores: {', '.join(failed)}", file=sys.stderr)
|
|
201
|
+
return 1, "failed"
|
|
202
|
+
self._save_revision(remote.revision)
|
|
203
|
+
return 0, "applied"
|
|
204
|
+
|
|
205
|
+
def _drift_lines(self):
|
|
206
|
+
settings = self.config.state_sync
|
|
207
|
+
local = self.local_revision()
|
|
208
|
+
lines = [f"Revision local: {'r%d' % local if local is not None else '(ninguna)'}"]
|
|
209
|
+
try:
|
|
210
|
+
res = build_source(settings, self.config.logger).read_result()
|
|
211
|
+
except Exception: # noqa: BLE001
|
|
212
|
+
res = ReadResult(None, False)
|
|
213
|
+
remote = res.state
|
|
214
|
+
if remote is None:
|
|
215
|
+
lines.append("Revision remota: (desconocida)" if not res.remote else "Revision remota: (vacia)")
|
|
216
|
+
else:
|
|
217
|
+
origin = "remote" if res.remote else "local cache - remote unreachable"
|
|
218
|
+
lines.append(f"Revision remota: r{remote.revision} ({origin}, paleta {remote.palette})")
|
|
219
|
+
if not res.remote:
|
|
220
|
+
drift = "remote unreachable"
|
|
221
|
+
elif remote is None:
|
|
222
|
+
drift = "no remote state"
|
|
223
|
+
elif (local or 0) < remote.revision:
|
|
224
|
+
drift = "behind"
|
|
225
|
+
elif (local or 0) > remote.revision:
|
|
226
|
+
drift = "ahead"
|
|
227
|
+
else:
|
|
228
|
+
drift = "in sync"
|
|
229
|
+
lines.append(f"Drift: {drift}")
|
|
230
|
+
return lines
|
|
231
|
+
|
|
232
|
+
def status(self) -> None:
|
|
233
|
+
print(f"Tema activo: {self.current_theme() or '(ninguno)'}")
|
|
234
|
+
if self.config.state_sync is not None:
|
|
235
|
+
print("\n".join(self._drift_lines()))
|
|
236
|
+
print(f"Paletas disponibles: {', '.join(self.list_palettes())}")
|
|
237
|
+
total = sum(len(t.outputs) for t in self.config.targets)
|
|
238
|
+
existing = sum(1 for t in self.config.targets for o in t.outputs if o.exists())
|
|
239
|
+
print(f"Targets configurados: {len(self.config.targets)} ({existing}/{total} outputs existentes en disco)")
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
DOCS_URL = "https://hueman.cuevaneander.tech"
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def init_config(path: Path, force: bool) -> int:
|
|
246
|
+
"""Copy the packaged example config to `path`; returns the exit code."""
|
|
247
|
+
if path.exists() and not force:
|
|
248
|
+
print(f"Ya existe: {path} (usa --force para sobrescribir)", file=sys.stderr)
|
|
249
|
+
return 1
|
|
250
|
+
try:
|
|
251
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
252
|
+
atomic_write_text(path, packaged_example_config().read_text())
|
|
253
|
+
except OSError as exc:
|
|
254
|
+
print(f"No se pudo escribir {path}: {exc}", file=sys.stderr)
|
|
255
|
+
return 1
|
|
256
|
+
print(f"Config de ejemplo escrito en {path}")
|
|
257
|
+
return 0
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def _var_arg(text: str):
|
|
261
|
+
"""argparse type for --var NAME=VALUE; returns (name, value)."""
|
|
262
|
+
name, sep, value = text.partition("=")
|
|
263
|
+
if not sep or not VAR_NAME_RE.fullmatch(name):
|
|
264
|
+
raise argparse.ArgumentTypeError(
|
|
265
|
+
f"invalid form {text!r}: expected NAME=VALUE with NAME matching [A-Za-z_][A-Za-z0-9_]*"
|
|
266
|
+
)
|
|
267
|
+
return name, value
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def main():
|
|
271
|
+
parser = argparse.ArgumentParser(description="Gestor centralizado de temas (standalone)")
|
|
272
|
+
parser.add_argument("--version", action="version", version=f"hueman {__version__}")
|
|
273
|
+
parser.add_argument("--config", help="Ruta al config (por defecto: $HUEMAN_CONFIG o ~/.config/hueman/config.yaml)")
|
|
274
|
+
parser.add_argument(
|
|
275
|
+
"--var", action="append", type=_var_arg, default=[], metavar="NAME=VALUE",
|
|
276
|
+
help="Define o sobreescribe una variable de 'vars:' para ${NAME} en las rutas "
|
|
277
|
+
"del config (repetible).",
|
|
278
|
+
)
|
|
279
|
+
sub = parser.add_subparsers(dest="command")
|
|
280
|
+
|
|
281
|
+
set_p = sub.add_parser("set", help="Aplicar un tema")
|
|
282
|
+
set_p.add_argument("name")
|
|
283
|
+
set_p.add_argument("--dry-run", action="store_true")
|
|
284
|
+
set_p.add_argument(
|
|
285
|
+
"--only", action="append", metavar="NAME[,NAME...]",
|
|
286
|
+
help="Limitar a targets por nombre o glob (repetible). No actualiza "
|
|
287
|
+
"el state file ni corre post_apply.",
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
sync_p = sub.add_parser(
|
|
291
|
+
"sync", help="Aplicar el estado compartido si es mas nuevo (seguro en el arranque del shell)")
|
|
292
|
+
sync_p.add_argument("-q", "--quiet", action="store_true",
|
|
293
|
+
help="Solo errores reales (sin avisos de red o config)")
|
|
294
|
+
|
|
295
|
+
init_p = sub.add_parser(
|
|
296
|
+
"init", help="Escribir un config de ejemplo en la ruta de config por defecto")
|
|
297
|
+
init_p.add_argument("--force", action="store_true",
|
|
298
|
+
help="Sobrescribir el config si ya existe")
|
|
299
|
+
|
|
300
|
+
sub.add_parser("list", help="Listar paletas disponibles")
|
|
301
|
+
sub.add_parser("status", help="Mostrar tema activo y estado de los targets")
|
|
302
|
+
|
|
303
|
+
args = parser.parse_args()
|
|
304
|
+
if not args.command:
|
|
305
|
+
parser.print_help()
|
|
306
|
+
return
|
|
307
|
+
|
|
308
|
+
config_path = HuemanConfig.resolve_path(args.config)
|
|
309
|
+
if args.command == "init":
|
|
310
|
+
sys.exit(init_config(config_path, args.force))
|
|
311
|
+
if not config_path.exists():
|
|
312
|
+
if args.command == "list":
|
|
313
|
+
# Las paletas empaquetadas no dependen del config del usuario.
|
|
314
|
+
loader = PaletteLoader(packaged_palettes_dir(), [], logging.getLogger("hueman"))
|
|
315
|
+
print("\n".join(loader.list_palettes()))
|
|
316
|
+
return
|
|
317
|
+
print(
|
|
318
|
+
f"Config no encontrada: {config_path}\n"
|
|
319
|
+
f"Crea uno con `hueman init`, o usa --config / HUEMAN_CONFIG (docs: {DOCS_URL})",
|
|
320
|
+
file=sys.stderr,
|
|
321
|
+
)
|
|
322
|
+
sys.exit(1)
|
|
323
|
+
|
|
324
|
+
try:
|
|
325
|
+
tc = Hueman(HuemanConfig(config_path, dict(args.var)))
|
|
326
|
+
except ConfigError as e:
|
|
327
|
+
print(f"Config invalida: {e}", file=sys.stderr)
|
|
328
|
+
sys.exit(1)
|
|
329
|
+
if args.command == "set":
|
|
330
|
+
only = [p.strip() for v in args.only or [] for p in v.split(",") if p.strip()] or None
|
|
331
|
+
try:
|
|
332
|
+
failed = tc.set_theme(args.name, args.dry_run, only=only)
|
|
333
|
+
except UnknownTargetError as e:
|
|
334
|
+
print(str(e), file=sys.stderr)
|
|
335
|
+
sys.exit(1)
|
|
336
|
+
except PlaceholderError as e:
|
|
337
|
+
print(f"Config invalida: {e}", file=sys.stderr)
|
|
338
|
+
sys.exit(1)
|
|
339
|
+
if failed:
|
|
340
|
+
print(f"Targets con errores: {', '.join(failed)}", file=sys.stderr)
|
|
341
|
+
sys.exit(1)
|
|
342
|
+
elif args.command == "sync":
|
|
343
|
+
if args.quiet:
|
|
344
|
+
tc.config.logger.setLevel(logging.ERROR)
|
|
345
|
+
sys.exit(tc.sync()[0])
|
|
346
|
+
elif args.command == "list":
|
|
347
|
+
print("\n".join(tc.list_palettes()))
|
|
348
|
+
elif args.command == "status":
|
|
349
|
+
tc.status()
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
if __name__ == "__main__":
|
|
353
|
+
main()
|