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.
Files changed (114) hide show
  1. hueman_cli-0.2.0/CONTRIBUTING.md +48 -0
  2. hueman_cli-0.2.0/LICENSE +21 -0
  3. hueman_cli-0.2.0/MANIFEST.in +12 -0
  4. hueman_cli-0.2.0/PKG-INFO +119 -0
  5. hueman_cli-0.2.0/README.md +87 -0
  6. hueman_cli-0.2.0/SECURITY.md +23 -0
  7. hueman_cli-0.2.0/hueman/__init__.py +11 -0
  8. hueman_cli-0.2.0/hueman/__main__.py +3 -0
  9. hueman_cli-0.2.0/hueman/cli.py +353 -0
  10. hueman_cli-0.2.0/hueman/data/hueman.config.example.yaml +111 -0
  11. hueman_cli-0.2.0/hueman/engine/__init__.py +0 -0
  12. hueman_cli-0.2.0/hueman/engine/colors.py +30 -0
  13. hueman_cli-0.2.0/hueman/engine/config.py +186 -0
  14. hueman_cli-0.2.0/hueman/engine/fsutil.py +41 -0
  15. hueman_cli-0.2.0/hueman/engine/palettes.py +66 -0
  16. hueman_cli-0.2.0/hueman/engine/reload.py +60 -0
  17. hueman_cli-0.2.0/hueman/engine/render.py +243 -0
  18. hueman_cli-0.2.0/hueman/engine/statesource.py +370 -0
  19. hueman_cli-0.2.0/hueman/engine/targets.py +95 -0
  20. hueman_cli-0.2.0/hueman/palettes/arctic-ice.yaml +102 -0
  21. hueman_cli-0.2.0/hueman/palettes/autumn.yaml +102 -0
  22. hueman_cli-0.2.0/hueman/palettes/catppuccin-latte.yaml +102 -0
  23. hueman_cli-0.2.0/hueman/palettes/catppuccin-mocha.yaml +102 -0
  24. hueman_cli-0.2.0/hueman/palettes/copperwave.yaml +103 -0
  25. hueman_cli-0.2.0/hueman/palettes/crimson-vivid.yaml +111 -0
  26. hueman_cli-0.2.0/hueman/palettes/dracula.yaml +102 -0
  27. hueman_cli-0.2.0/hueman/palettes/ember-red.yaml +111 -0
  28. hueman_cli-0.2.0/hueman/palettes/everforest-dark.yaml +102 -0
  29. hueman_cli-0.2.0/hueman/palettes/galaxy.yaml +102 -0
  30. hueman_cli-0.2.0/hueman/palettes/gruvbox-light.yaml +102 -0
  31. hueman_cli-0.2.0/hueman/palettes/gruvbox.yaml +102 -0
  32. hueman_cli-0.2.0/hueman/palettes/kanagawa-dragon.yaml +102 -0
  33. hueman_cli-0.2.0/hueman/palettes/matrix.yaml +102 -0
  34. hueman_cli-0.2.0/hueman/palettes/mono-contrast.yaml +102 -0
  35. hueman_cli-0.2.0/hueman/palettes/nord.yaml +102 -0
  36. hueman_cli-0.2.0/hueman/palettes/ocean-deep.yaml +102 -0
  37. hueman_cli-0.2.0/hueman/palettes/one-light.yaml +102 -0
  38. hueman_cli-0.2.0/hueman/palettes/onyx-gold.yaml +103 -0
  39. hueman_cli-0.2.0/hueman/palettes/rose-pine-dawn.yaml +102 -0
  40. hueman_cli-0.2.0/hueman/palettes/solarized-light.yaml +102 -0
  41. hueman_cli-0.2.0/hueman/palettes/synthwave.yaml +102 -0
  42. hueman_cli-0.2.0/hueman/palettes/tokyonight.yaml +106 -0
  43. hueman_cli-0.2.0/hueman/palettes/tuxedo.yaml +103 -0
  44. hueman_cli-0.2.0/hueman/palettes/vaporwave.yaml +102 -0
  45. hueman_cli-0.2.0/hueman/resources.py +18 -0
  46. hueman_cli-0.2.0/hueman/templates/atuin_theme.toml.j2 +19 -0
  47. hueman_cli-0.2.0/hueman/templates/bottom_theme.toml.j2 +43 -0
  48. hueman_cli-0.2.0/hueman/templates/bspwm_colors.conf.j2 +5 -0
  49. hueman_cli-0.2.0/hueman/templates/claude_statusline.sh.j2 +216 -0
  50. hueman_cli-0.2.0/hueman/templates/dunst_colors.conf.j2 +15 -0
  51. hueman_cli-0.2.0/hueman/templates/fzf_opts.j2 +9 -0
  52. hueman_cli-0.2.0/hueman/templates/fzf_theme.j2 +17 -0
  53. hueman_cli-0.2.0/hueman/templates/herdr_theme_custom.toml.j2 +11 -0
  54. hueman_cli-0.2.0/hueman/templates/k9s_theme.yaml.j2 +83 -0
  55. hueman_cli-0.2.0/hueman/templates/kitty.conf.j2 +51 -0
  56. hueman_cli-0.2.0/hueman/templates/lazydocker_theme.yml.j2 +19 -0
  57. hueman_cli-0.2.0/hueman/templates/leaf_theme.toml.j2 +89 -0
  58. hueman_cli-0.2.0/hueman/templates/newt_colors.j2 +32 -0
  59. hueman_cli-0.2.0/hueman/templates/nvim_nightfox_theme.lua.j2 +70 -0
  60. hueman_cli-0.2.0/hueman/templates/omp.omp.json.j2 +107 -0
  61. hueman_cli-0.2.0/hueman/templates/opencode_theme.json.j2 +71 -0
  62. hueman_cli-0.2.0/hueman/templates/pi_theme.json.j2 +65 -0
  63. hueman_cli-0.2.0/hueman/templates/polybar_colors.ini.j2 +11 -0
  64. hueman_cli-0.2.0/hueman/templates/posting_theme.yaml.j2 +47 -0
  65. hueman_cli-0.2.0/hueman/templates/qutebrowser_github.user.js.j2 +121 -0
  66. hueman_cli-0.2.0/hueman/templates/qutebrowser_google.user.js.j2 +132 -0
  67. hueman_cli-0.2.0/hueman/templates/qutebrowser_hteam.user.js.j2 +962 -0
  68. hueman_cli-0.2.0/hueman/templates/qutebrowser_theme.py.j2 +78 -0
  69. hueman_cli-0.2.0/hueman/templates/rofi_theme.rasi.j2 +132 -0
  70. hueman_cli-0.2.0/hueman/templates/sshub_theme.toml.j2 +49 -0
  71. hueman_cli-0.2.0/hueman/templates/starship_block.toml.j2 +11 -0
  72. hueman_cli-0.2.0/hueman/templates/termux_colors.properties.j2 +20 -0
  73. hueman_cli-0.2.0/hueman/templates/tmux.conf.j2 +33 -0
  74. hueman_cli-0.2.0/hueman/templates/tuicr_theme.toml.j2 +51 -0
  75. hueman_cli-0.2.0/hueman/templates/wezterm_colors.lua.j2 +14 -0
  76. hueman_cli-0.2.0/hueman/templates/windhawk_taskbar.json.j2 +4 -0
  77. hueman_cli-0.2.0/hueman/templates/yazi_theme.toml.j2 +130 -0
  78. hueman_cli-0.2.0/hueman/templates/zen_theme.json.j2 +11 -0
  79. hueman_cli-0.2.0/hueman_cli.egg-info/PKG-INFO +119 -0
  80. hueman_cli-0.2.0/hueman_cli.egg-info/SOURCES.txt +112 -0
  81. hueman_cli-0.2.0/hueman_cli.egg-info/dependency_links.txt +1 -0
  82. hueman_cli-0.2.0/hueman_cli.egg-info/entry_points.txt +2 -0
  83. hueman_cli-0.2.0/hueman_cli.egg-info/requires.txt +5 -0
  84. hueman_cli-0.2.0/hueman_cli.egg-info/scm_file_list.json +205 -0
  85. hueman_cli-0.2.0/hueman_cli.egg-info/scm_version.json +8 -0
  86. hueman_cli-0.2.0/hueman_cli.egg-info/top_level.txt +1 -0
  87. hueman_cli-0.2.0/pyproject.toml +61 -0
  88. hueman_cli-0.2.0/requirements.txt +3 -0
  89. hueman_cli-0.2.0/setup.cfg +4 -0
  90. hueman_cli-0.2.0/tests/conftest.py +12 -0
  91. hueman_cli-0.2.0/tests/test_atomic_writes.py +179 -0
  92. hueman_cli-0.2.0/tests/test_colors_and_cli.py +145 -0
  93. hueman_cli-0.2.0/tests/test_command_substitution.py +51 -0
  94. hueman_cli-0.2.0/tests/test_config_and_palettes.py +104 -0
  95. hueman_cli-0.2.0/tests/test_config_validation.py +156 -0
  96. hueman_cli-0.2.0/tests/test_config_vars.py +228 -0
  97. hueman_cli-0.2.0/tests/test_error_paths.py +68 -0
  98. hueman_cli-0.2.0/tests/test_file_source.py +127 -0
  99. hueman_cli-0.2.0/tests/test_idempotence.py +85 -0
  100. hueman_cli-0.2.0/tests/test_init_command.py +87 -0
  101. hueman_cli-0.2.0/tests/test_packaging.py +60 -0
  102. hueman_cli-0.2.0/tests/test_palette_names.py +112 -0
  103. hueman_cli-0.2.0/tests/test_real_config.py +52 -0
  104. hueman_cli-0.2.0/tests/test_reload_exit_codes.py +240 -0
  105. hueman_cli-0.2.0/tests/test_render.py +401 -0
  106. hueman_cli-0.2.0/tests/test_render_modes.py +252 -0
  107. hueman_cli-0.2.0/tests/test_sandbox.py +71 -0
  108. hueman_cli-0.2.0/tests/test_starship_marker.py +71 -0
  109. hueman_cli-0.2.0/tests/test_state_and_reload.py +125 -0
  110. hueman_cli-0.2.0/tests/test_state_branch.py +224 -0
  111. hueman_cli-0.2.0/tests/test_state_source_contract.py +102 -0
  112. hueman_cli-0.2.0/tests/test_sync_network.py +76 -0
  113. hueman_cli-0.2.0/tests/test_sync_status.py +281 -0
  114. 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).
@@ -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,3 @@
1
+ from .cli import main
2
+
3
+ main()
@@ -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()