hueman-cli 0.3.0__tar.gz → 0.3.1__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 (125) hide show
  1. {hueman_cli-0.3.0 → hueman_cli-0.3.1}/CONTRIBUTING.md +19 -10
  2. {hueman_cli-0.3.0/hueman_cli.egg-info → hueman_cli-0.3.1}/PKG-INFO +9 -6
  3. {hueman_cli-0.3.0 → hueman_cli-0.3.1}/README.md +8 -5
  4. {hueman_cli-0.3.0 → hueman_cli-0.3.1}/hueman/cli.py +216 -56
  5. hueman_cli-0.3.1/hueman/data/hueman.config.example.yaml +157 -0
  6. {hueman_cli-0.3.0 → hueman_cli-0.3.1}/hueman/engine/config.py +17 -17
  7. {hueman_cli-0.3.0 → hueman_cli-0.3.1}/hueman/engine/palettes.py +7 -7
  8. {hueman_cli-0.3.0 → hueman_cli-0.3.1}/hueman/engine/reload.py +19 -10
  9. {hueman_cli-0.3.0 → hueman_cli-0.3.1}/hueman/engine/render.py +56 -34
  10. {hueman_cli-0.3.0 → hueman_cli-0.3.1}/hueman/engine/statesource.py +19 -19
  11. {hueman_cli-0.3.0 → hueman_cli-0.3.1}/hueman/engine/targets.py +5 -5
  12. hueman_cli-0.3.1/hueman/templates/flowlauncher_theme.xaml.j2 +199 -0
  13. hueman_cli-0.3.1/hueman/templates/telegram.attheme.j2 +157 -0
  14. hueman_cli-0.3.1/hueman/templates/yasb_root.css.j2 +29 -0
  15. {hueman_cli-0.3.0 → hueman_cli-0.3.1/hueman_cli.egg-info}/PKG-INFO +9 -6
  16. {hueman_cli-0.3.0 → hueman_cli-0.3.1}/hueman_cli.egg-info/SOURCES.txt +10 -1
  17. {hueman_cli-0.3.0 → hueman_cli-0.3.1}/hueman_cli.egg-info/scm_file_list.json +54 -0
  18. hueman_cli-0.3.1/hueman_cli.egg-info/scm_version.json +8 -0
  19. hueman_cli-0.3.1/scripts/freeze-docs.sh +92 -0
  20. hueman_cli-0.3.1/tests/test_cli_help_contract.py +56 -0
  21. {hueman_cli-0.3.0 → hueman_cli-0.3.1}/tests/test_colors_and_cli.py +43 -2
  22. {hueman_cli-0.3.0 → hueman_cli-0.3.1}/tests/test_config_and_palettes.py +1 -1
  23. {hueman_cli-0.3.0 → hueman_cli-0.3.1}/tests/test_config_validation.py +3 -3
  24. {hueman_cli-0.3.0 → hueman_cli-0.3.1}/tests/test_config_vars.py +2 -2
  25. {hueman_cli-0.3.0 → hueman_cli-0.3.1}/tests/test_init_command.py +1 -1
  26. {hueman_cli-0.3.0 → hueman_cli-0.3.1}/tests/test_palette_names.py +1 -1
  27. {hueman_cli-0.3.0 → hueman_cli-0.3.1}/tests/test_reload_exit_codes.py +11 -6
  28. {hueman_cli-0.3.0 → hueman_cli-0.3.1}/tests/test_render_modes.py +8 -6
  29. hueman_cli-0.3.1/tests/test_set_output_contract.py +88 -0
  30. {hueman_cli-0.3.0 → hueman_cli-0.3.1}/tests/test_state_and_reload.py +2 -2
  31. {hueman_cli-0.3.0 → hueman_cli-0.3.1}/tests/test_sync_status.py +2 -2
  32. hueman_cli-0.3.1/tests/test_telegram_template.py +78 -0
  33. hueman_cli-0.3.1/tests/test_windows_examples_config.py +80 -0
  34. hueman_cli-0.3.1/tests/test_yasb_flowlauncher_templates.py +120 -0
  35. hueman_cli-0.3.0/hueman/data/hueman.config.example.yaml +0 -111
  36. hueman_cli-0.3.0/hueman_cli.egg-info/scm_version.json +0 -8
  37. {hueman_cli-0.3.0 → hueman_cli-0.3.1}/LICENSE +0 -0
  38. {hueman_cli-0.3.0 → hueman_cli-0.3.1}/MANIFEST.in +0 -0
  39. {hueman_cli-0.3.0 → hueman_cli-0.3.1}/SECURITY.md +0 -0
  40. {hueman_cli-0.3.0 → hueman_cli-0.3.1}/hueman/__init__.py +0 -0
  41. {hueman_cli-0.3.0 → hueman_cli-0.3.1}/hueman/__main__.py +0 -0
  42. {hueman_cli-0.3.0 → hueman_cli-0.3.1}/hueman/engine/__init__.py +0 -0
  43. {hueman_cli-0.3.0 → hueman_cli-0.3.1}/hueman/engine/colors.py +0 -0
  44. {hueman_cli-0.3.0 → hueman_cli-0.3.1}/hueman/engine/fsutil.py +0 -0
  45. {hueman_cli-0.3.0 → hueman_cli-0.3.1}/hueman/palettes/arctic-ice.yaml +0 -0
  46. {hueman_cli-0.3.0 → hueman_cli-0.3.1}/hueman/palettes/autumn.yaml +0 -0
  47. {hueman_cli-0.3.0 → hueman_cli-0.3.1}/hueman/palettes/catppuccin-latte.yaml +0 -0
  48. {hueman_cli-0.3.0 → hueman_cli-0.3.1}/hueman/palettes/catppuccin-mocha.yaml +0 -0
  49. {hueman_cli-0.3.0 → hueman_cli-0.3.1}/hueman/palettes/copperwave.yaml +0 -0
  50. {hueman_cli-0.3.0 → hueman_cli-0.3.1}/hueman/palettes/crimson-vivid.yaml +0 -0
  51. {hueman_cli-0.3.0 → hueman_cli-0.3.1}/hueman/palettes/dracula.yaml +0 -0
  52. {hueman_cli-0.3.0 → hueman_cli-0.3.1}/hueman/palettes/ember-red.yaml +0 -0
  53. {hueman_cli-0.3.0 → hueman_cli-0.3.1}/hueman/palettes/everforest-dark.yaml +0 -0
  54. {hueman_cli-0.3.0 → hueman_cli-0.3.1}/hueman/palettes/galaxy.yaml +0 -0
  55. {hueman_cli-0.3.0 → hueman_cli-0.3.1}/hueman/palettes/gruvbox-light.yaml +0 -0
  56. {hueman_cli-0.3.0 → hueman_cli-0.3.1}/hueman/palettes/gruvbox.yaml +0 -0
  57. {hueman_cli-0.3.0 → hueman_cli-0.3.1}/hueman/palettes/kanagawa-dragon.yaml +0 -0
  58. {hueman_cli-0.3.0 → hueman_cli-0.3.1}/hueman/palettes/matrix.yaml +0 -0
  59. {hueman_cli-0.3.0 → hueman_cli-0.3.1}/hueman/palettes/mono-contrast.yaml +0 -0
  60. {hueman_cli-0.3.0 → hueman_cli-0.3.1}/hueman/palettes/nord.yaml +0 -0
  61. {hueman_cli-0.3.0 → hueman_cli-0.3.1}/hueman/palettes/ocean-deep.yaml +0 -0
  62. {hueman_cli-0.3.0 → hueman_cli-0.3.1}/hueman/palettes/one-light.yaml +0 -0
  63. {hueman_cli-0.3.0 → hueman_cli-0.3.1}/hueman/palettes/onyx-gold.yaml +0 -0
  64. {hueman_cli-0.3.0 → hueman_cli-0.3.1}/hueman/palettes/rose-pine-dawn.yaml +0 -0
  65. {hueman_cli-0.3.0 → hueman_cli-0.3.1}/hueman/palettes/solarized-light.yaml +0 -0
  66. {hueman_cli-0.3.0 → hueman_cli-0.3.1}/hueman/palettes/synthwave.yaml +0 -0
  67. {hueman_cli-0.3.0 → hueman_cli-0.3.1}/hueman/palettes/tokyonight.yaml +0 -0
  68. {hueman_cli-0.3.0 → hueman_cli-0.3.1}/hueman/palettes/tuxedo.yaml +0 -0
  69. {hueman_cli-0.3.0 → hueman_cli-0.3.1}/hueman/palettes/vaporwave.yaml +0 -0
  70. {hueman_cli-0.3.0 → hueman_cli-0.3.1}/hueman/resources.py +0 -0
  71. {hueman_cli-0.3.0 → hueman_cli-0.3.1}/hueman/templates/atuin_theme.toml.j2 +0 -0
  72. {hueman_cli-0.3.0 → hueman_cli-0.3.1}/hueman/templates/bottom_theme.toml.j2 +0 -0
  73. {hueman_cli-0.3.0 → hueman_cli-0.3.1}/hueman/templates/bspwm_colors.conf.j2 +0 -0
  74. {hueman_cli-0.3.0 → hueman_cli-0.3.1}/hueman/templates/claude_statusline.sh.j2 +0 -0
  75. {hueman_cli-0.3.0 → hueman_cli-0.3.1}/hueman/templates/dunst_colors.conf.j2 +0 -0
  76. {hueman_cli-0.3.0 → hueman_cli-0.3.1}/hueman/templates/fzf_opts.j2 +0 -0
  77. {hueman_cli-0.3.0 → hueman_cli-0.3.1}/hueman/templates/fzf_theme.j2 +0 -0
  78. {hueman_cli-0.3.0 → hueman_cli-0.3.1}/hueman/templates/herdr_theme_custom.toml.j2 +0 -0
  79. {hueman_cli-0.3.0 → hueman_cli-0.3.1}/hueman/templates/k9s_theme.yaml.j2 +0 -0
  80. {hueman_cli-0.3.0 → hueman_cli-0.3.1}/hueman/templates/kitty.conf.j2 +0 -0
  81. {hueman_cli-0.3.0 → hueman_cli-0.3.1}/hueman/templates/lazydocker_theme.yml.j2 +0 -0
  82. {hueman_cli-0.3.0 → hueman_cli-0.3.1}/hueman/templates/leaf_theme.toml.j2 +0 -0
  83. {hueman_cli-0.3.0 → hueman_cli-0.3.1}/hueman/templates/newt_colors.j2 +0 -0
  84. {hueman_cli-0.3.0 → hueman_cli-0.3.1}/hueman/templates/nvim_nightfox_theme.lua.j2 +0 -0
  85. {hueman_cli-0.3.0 → hueman_cli-0.3.1}/hueman/templates/omp.omp.json.j2 +0 -0
  86. {hueman_cli-0.3.0 → hueman_cli-0.3.1}/hueman/templates/opencode_theme.json.j2 +0 -0
  87. {hueman_cli-0.3.0 → hueman_cli-0.3.1}/hueman/templates/pi_theme.json.j2 +0 -0
  88. {hueman_cli-0.3.0 → hueman_cli-0.3.1}/hueman/templates/polybar_colors.ini.j2 +0 -0
  89. {hueman_cli-0.3.0 → hueman_cli-0.3.1}/hueman/templates/posting_theme.yaml.j2 +0 -0
  90. {hueman_cli-0.3.0 → hueman_cli-0.3.1}/hueman/templates/qutebrowser_github.user.js.j2 +0 -0
  91. {hueman_cli-0.3.0 → hueman_cli-0.3.1}/hueman/templates/qutebrowser_google.user.js.j2 +0 -0
  92. {hueman_cli-0.3.0 → hueman_cli-0.3.1}/hueman/templates/qutebrowser_hteam.user.js.j2 +0 -0
  93. {hueman_cli-0.3.0 → hueman_cli-0.3.1}/hueman/templates/qutebrowser_theme.py.j2 +0 -0
  94. {hueman_cli-0.3.0 → hueman_cli-0.3.1}/hueman/templates/rofi_theme.rasi.j2 +0 -0
  95. {hueman_cli-0.3.0 → hueman_cli-0.3.1}/hueman/templates/sshub_theme.toml.j2 +0 -0
  96. {hueman_cli-0.3.0 → hueman_cli-0.3.1}/hueman/templates/starship_block.toml.j2 +0 -0
  97. {hueman_cli-0.3.0 → hueman_cli-0.3.1}/hueman/templates/termux_colors.properties.j2 +0 -0
  98. {hueman_cli-0.3.0 → hueman_cli-0.3.1}/hueman/templates/tmux.conf.j2 +0 -0
  99. {hueman_cli-0.3.0 → hueman_cli-0.3.1}/hueman/templates/tuicr_theme.toml.j2 +0 -0
  100. {hueman_cli-0.3.0 → hueman_cli-0.3.1}/hueman/templates/wezterm_colors.lua.j2 +0 -0
  101. {hueman_cli-0.3.0 → hueman_cli-0.3.1}/hueman/templates/windhawk_taskbar.json.j2 +0 -0
  102. {hueman_cli-0.3.0 → hueman_cli-0.3.1}/hueman/templates/yazi_theme.toml.j2 +0 -0
  103. {hueman_cli-0.3.0 → hueman_cli-0.3.1}/hueman/templates/zen_theme.json.j2 +0 -0
  104. {hueman_cli-0.3.0 → hueman_cli-0.3.1}/hueman_cli.egg-info/dependency_links.txt +0 -0
  105. {hueman_cli-0.3.0 → hueman_cli-0.3.1}/hueman_cli.egg-info/entry_points.txt +0 -0
  106. {hueman_cli-0.3.0 → hueman_cli-0.3.1}/hueman_cli.egg-info/requires.txt +0 -0
  107. {hueman_cli-0.3.0 → hueman_cli-0.3.1}/hueman_cli.egg-info/top_level.txt +0 -0
  108. {hueman_cli-0.3.0 → hueman_cli-0.3.1}/pyproject.toml +0 -0
  109. {hueman_cli-0.3.0 → hueman_cli-0.3.1}/requirements.txt +0 -0
  110. {hueman_cli-0.3.0 → hueman_cli-0.3.1}/setup.cfg +0 -0
  111. {hueman_cli-0.3.0 → hueman_cli-0.3.1}/tests/conftest.py +0 -0
  112. {hueman_cli-0.3.0 → hueman_cli-0.3.1}/tests/test_atomic_writes.py +0 -0
  113. {hueman_cli-0.3.0 → hueman_cli-0.3.1}/tests/test_command_substitution.py +0 -0
  114. {hueman_cli-0.3.0 → hueman_cli-0.3.1}/tests/test_error_paths.py +0 -0
  115. {hueman_cli-0.3.0 → hueman_cli-0.3.1}/tests/test_file_source.py +0 -0
  116. {hueman_cli-0.3.0 → hueman_cli-0.3.1}/tests/test_idempotence.py +0 -0
  117. {hueman_cli-0.3.0 → hueman_cli-0.3.1}/tests/test_packaging.py +0 -0
  118. {hueman_cli-0.3.0 → hueman_cli-0.3.1}/tests/test_real_config.py +0 -0
  119. {hueman_cli-0.3.0 → hueman_cli-0.3.1}/tests/test_render.py +0 -0
  120. {hueman_cli-0.3.0 → hueman_cli-0.3.1}/tests/test_sandbox.py +0 -0
  121. {hueman_cli-0.3.0 → hueman_cli-0.3.1}/tests/test_starship_marker.py +0 -0
  122. {hueman_cli-0.3.0 → hueman_cli-0.3.1}/tests/test_state_branch.py +0 -0
  123. {hueman_cli-0.3.0 → hueman_cli-0.3.1}/tests/test_state_source_contract.py +0 -0
  124. {hueman_cli-0.3.0 → hueman_cli-0.3.1}/tests/test_sync_network.py +0 -0
  125. {hueman_cli-0.3.0 → hueman_cli-0.3.1}/tests/test_termux_template.py +0 -0
@@ -22,7 +22,7 @@ Run `python -m pytest -q` before opening the PR.
22
22
 
23
23
  ## Documentation
24
24
 
25
- The docs live in `website/content/latest` (written in Spanish).
25
+ The docs live in `website/content/latest` (English, public product language).
26
26
 
27
27
  - Add a tool or target: [add-a-tool](website/content/latest/guides/add-a-tool.md)
28
28
  - Add a palette: [add-a-palette](website/content/latest/guides/add-a-palette.md)
@@ -33,15 +33,24 @@ templates, not in Python.
33
33
  ### Freezing docs for a release
34
34
 
35
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.
36
+ tip. Page ids in `content/*/nav.json` are used as URLs (`?p=<id>`), so do not
37
+ rename them.
38
+
39
+ Docs freezes are **automatic** on release. When `release.yml` cuts a version it
40
+ runs `scripts/freeze-docs.sh vX.Y.Z` before tagging, in the same
41
+ `chore(release): vX.Y.Z` commit as the changelog. That script:
42
+
43
+ 1. Copies `website/content/latest/` → `website/content/vX.Y.Z/`
44
+ 2. Copies `website/skills/latest/` → `website/skills/vX.Y.Z/` when present
45
+ 3. Inserts the release entry in `website/data/versions.json` and marks the
46
+ previous release snapshot as `supported`
47
+
48
+ Do **not** open a follow-up PR just to freeze docs. To dry-run locally:
49
+
50
+ ```bash
51
+ ./scripts/freeze-docs.sh vX.Y.Z
52
+ # inspect, then discard the generated paths if you are not cutting a release
53
+ ```
45
54
 
46
55
  ## Security
47
56
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hueman-cli
3
- Version: 0.3.0
3
+ Version: 0.3.1
4
4
  Summary: Centralized theme manager: one YAML palette feeds Jinja2 templates that generate each tool's config.
5
5
  Author: 4DRIAN0RTIZ
6
6
  License-Expression: MIT
@@ -40,7 +40,7 @@ Dynamic: license-file
40
40
 
41
41
  <p align="center">
42
42
  One theme for your terminal, editor, prompt, and the rest of your setup.<br />
43
- You own the map — which file each app uses — and one command paints it everywhere.<br />
43
+ You say which file each app uses, and one command paints them all.<br />
44
44
  Not a dotfiles manager: output paths live in your own <code>hueman.config.yaml</code>,
45
45
  outside this repo.
46
46
  </p>
@@ -70,6 +70,8 @@ config and generated files are left alone (details in the
70
70
  Other commands: `init` (sample config), `list` (palettes), `status` (active
71
71
  theme), `sync` (shared state across machines).
72
72
 
73
+ Quick reference on the machine: `hueman --help` and `hueman <command> --help`.
74
+
73
75
  ## Core ideas
74
76
 
75
77
  - One palette, many targets; output mapping is declarative.
@@ -79,20 +81,21 @@ theme), `sync` (shared state across machines).
79
81
  - Per-target `reload` and global `post_apply`, best-effort, with `warn` /
80
82
  `error` policy.
81
83
  - Optional sync of the active palette across machines (`state_sync`).
82
- - Adding a tool is a template + a target, not an engine change.
84
+ - To support a new app, add a template and a target.
83
85
 
84
86
  ## Packaged tools
85
87
 
86
88
  `hueman/palettes/` and `hueman/templates/` ship 25 generic palettes and
87
89
  templates for common tools (kitty, tmux, polybar, nvim/nightfox, dunst, bspwm,
88
90
  yazi, qutebrowser, rofi, fzf, atuin, oh-my-posh, wezterm, starship, Termux,
89
- herdr, and more). For idiosyncratic targets use `template_dirs` in your config
90
- instead of asking them to be packaged here.
91
+ herdr, and more). For other tools, use `template_dirs` in your config instead of
92
+ asking for them to be packaged here.
91
93
 
92
94
  ## Docs
93
95
 
94
96
  Full site: **[hueman.cuevaneander.tech](https://hueman.cuevaneander.tech)**
95
- (`/` = landing, `?p=intro` = docs)
97
+ (`/` = landing, `?p=intro` = docs). For flags and short examples on your
98
+ machine, use `hueman --help` / `hueman set --help` rather than a man page.
96
99
 
97
100
  Local preview (sources under `website/`):
98
101
 
@@ -8,7 +8,7 @@
8
8
 
9
9
  <p align="center">
10
10
  One theme for your terminal, editor, prompt, and the rest of your setup.<br />
11
- You own the map — which file each app uses — and one command paints it everywhere.<br />
11
+ You say which file each app uses, and one command paints them all.<br />
12
12
  Not a dotfiles manager: output paths live in your own <code>hueman.config.yaml</code>,
13
13
  outside this repo.
14
14
  </p>
@@ -38,6 +38,8 @@ config and generated files are left alone (details in the
38
38
  Other commands: `init` (sample config), `list` (palettes), `status` (active
39
39
  theme), `sync` (shared state across machines).
40
40
 
41
+ Quick reference on the machine: `hueman --help` and `hueman <command> --help`.
42
+
41
43
  ## Core ideas
42
44
 
43
45
  - One palette, many targets; output mapping is declarative.
@@ -47,20 +49,21 @@ theme), `sync` (shared state across machines).
47
49
  - Per-target `reload` and global `post_apply`, best-effort, with `warn` /
48
50
  `error` policy.
49
51
  - Optional sync of the active palette across machines (`state_sync`).
50
- - Adding a tool is a template + a target, not an engine change.
52
+ - To support a new app, add a template and a target.
51
53
 
52
54
  ## Packaged tools
53
55
 
54
56
  `hueman/palettes/` and `hueman/templates/` ship 25 generic palettes and
55
57
  templates for common tools (kitty, tmux, polybar, nvim/nightfox, dunst, bspwm,
56
58
  yazi, qutebrowser, rofi, fzf, atuin, oh-my-posh, wezterm, starship, Termux,
57
- herdr, and more). For idiosyncratic targets use `template_dirs` in your config
58
- instead of asking them to be packaged here.
59
+ herdr, and more). For other tools, use `template_dirs` in your config instead of
60
+ asking for them to be packaged here.
59
61
 
60
62
  ## Docs
61
63
 
62
64
  Full site: **[hueman.cuevaneander.tech](https://hueman.cuevaneander.tech)**
63
- (`/` = landing, `?p=intro` = docs)
65
+ (`/` = landing, `?p=intro` = docs). For flags and short examples on your
66
+ machine, use `hueman --help` / `hueman set --help` rather than a man page.
64
67
 
65
68
  Local preview (sources under `website/`):
66
69
 
@@ -8,7 +8,9 @@ dotfiles externo — todo el mapeo de rutas es explícito en ese config.
8
8
  import argparse
9
9
  import fnmatch
10
10
  import logging
11
+ from collections import Counter
11
12
  import sys
13
+ import threading
12
14
  from pathlib import Path
13
15
 
14
16
  from . import __version__
@@ -25,6 +27,51 @@ class UnknownTargetError(ValueError):
25
27
  """Un patron de --only no coincide con ningun target."""
26
28
 
27
29
 
30
+ class _SetProgress:
31
+ """TTY-only progress bar for the quiet `set` path."""
32
+
33
+ _WIDTH = 24
34
+
35
+ def __init__(self, name: str, total: int, enabled: bool):
36
+ self.name = name
37
+ self.total = max(total, 1)
38
+ self.enabled = enabled
39
+ self.completed = 0
40
+ self.label = "Preparing"
41
+ self._stop = threading.Event()
42
+ self._lock = threading.Lock()
43
+ self._thread = None
44
+
45
+ def start(self) -> None:
46
+ if not self.enabled:
47
+ return
48
+ self._thread = threading.Thread(target=self._run, daemon=True)
49
+ self._thread.start()
50
+
51
+ def update(self, completed: int, label: str) -> None:
52
+ with self._lock:
53
+ self.completed = min(completed, self.total)
54
+ self.label = label
55
+
56
+ def _line(self) -> str:
57
+ with self._lock:
58
+ completed, label = self.completed, self.label
59
+ filled = round(self._WIDTH * completed / self.total)
60
+ bar = "█" * filled + "░" * (self._WIDTH - filled)
61
+ return f"Applying {self.name} [{bar}] {completed}/{self.total} · {label}"
62
+
63
+ def _run(self) -> None:
64
+ while not self._stop.wait(0.08):
65
+ print(f"\r\x1b[2K{self._line()}", end="", file=sys.stderr, flush=True)
66
+
67
+ def stop(self) -> None:
68
+ if not self.enabled:
69
+ return
70
+ self._stop.set()
71
+ self._thread.join()
72
+ print("\r\x1b[2K", end="", file=sys.stderr, flush=True)
73
+
74
+
28
75
  class Hueman:
29
76
  def __init__(self, config: HuemanConfig):
30
77
  self.config = config
@@ -53,12 +100,21 @@ class Hueman:
53
100
  missing = [p for p in only if not fnmatch.filter(names, p)]
54
101
  if missing:
55
102
  raise UnknownTargetError(
56
- f"--only sin coincidencias: {', '.join(missing)}. "
57
- f"Targets validos: {', '.join(names)}"
103
+ f"--only matched no targets: {', '.join(missing)}. "
104
+ f"Available targets: {', '.join(names)}"
58
105
  )
59
106
  return [t for t in targets if any(fnmatch.fnmatchcase(t.name, p) for p in only)]
60
107
 
61
- def set_theme(self, name: str, dry_run: bool = False, only=None, publish: bool = True) -> list:
108
+ @staticmethod
109
+ def _set_summary(name: str, stats: Counter, dry_run: bool) -> None:
110
+ action = "Dry run" if dry_run else "Applied"
111
+ print(
112
+ f"{action}: {name} "
113
+ f"(written: {stats['written']}, skipped: {stats['skipped']}, failed: {stats['failed']})"
114
+ )
115
+
116
+ def set_theme(self, name: str, dry_run: bool = False, only=None, publish: bool = True,
117
+ verbose: bool = False) -> list:
62
118
  """Con `only` (patrones fnmatch) se renderizan solo los targets que
63
119
  coinciden y se dispara solo su reload; NO se escribe el state file ni
64
120
  corre post_apply (es una re-renderizacion parcial, no un cambio de tema).
@@ -78,48 +134,77 @@ class Hueman:
78
134
  if not only:
79
135
  post_apply = [expand_command(c, name, palette) for c in self.config.post_apply]
80
136
  failed = []
137
+ stats = Counter()
138
+ total_steps = sum(max(len(target.outputs), 1) for target in selected) + len(post_apply or [])
139
+ progress = _SetProgress(name, total_steps, enabled=not verbose and sys.stderr.isatty())
140
+ progress.start()
141
+ completed = 0
81
142
  for target in selected:
143
+ target_steps = max(len(target.outputs), 1)
144
+ progress.update(completed, f"Rendering {target.name}")
82
145
  env = self._jinja_env(target.jinja_delimiters)
83
146
  try:
84
- wrote = render_target(env, target, name, palette, dry_run, self.config.logger)
147
+ wrote = render_target(
148
+ env, target, name, palette, dry_run, self.config.logger, stats, verbose
149
+ )
85
150
  except TargetRenderError as exc:
86
- self.config.logger.warning(f"{exc}; target omitido")
151
+ self.config.logger.warning(f"{exc}; target skipped")
152
+ stats["failed"] += 1
87
153
  failed.append(target.name)
154
+ completed += target_steps
88
155
  continue
156
+ completed += target_steps
89
157
  if not dry_run and wrote and target.reload:
158
+ progress.update(completed, f"Reloading {target.name}")
90
159
  cmd = expand_command(target.reload, name, palette)
91
- rc = run_reload(cmd, self.config.logger)
160
+ rc = run_reload(cmd, self.config.logger, verbose=verbose) if verbose else run_reload(
161
+ cmd, self.config.logger
162
+ )
92
163
  if rc:
164
+ stats["failed"] += 1
93
165
  policy = target.reload_failure or self.config.reload_failure
94
166
  self.config.logger.log(
95
167
  logging.ERROR if policy == "error" else logging.WARNING,
96
- f"{target.name}: reload fallo (exit {rc}): {cmd}",
168
+ f"{target.name}: reload failed (exit {rc}): {cmd}",
97
169
  )
98
170
  if policy == "error":
99
171
  failed.append(target.name)
100
172
 
101
173
  if only:
174
+ progress.stop()
175
+ self._set_summary(name, stats, dry_run)
102
176
  return failed
103
177
 
104
178
  if dry_run:
105
- for cmd in post_apply:
106
- print(f"[DRY RUN] post_apply: {cmd}")
179
+ if verbose:
180
+ for cmd in post_apply:
181
+ print(f"[DRY RUN] post_apply: {cmd}")
182
+ progress.stop()
183
+ self._set_summary(name, stats, dry_run)
107
184
  return failed
108
185
 
109
186
  self.config.state_file.parent.mkdir(parents=True, exist_ok=True)
110
187
  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 []
188
+ progress.update(completed, "Running post-apply hooks")
189
+ post_failed = (
190
+ run_post_apply(post_apply, self.config.logger, verbose=verbose)
191
+ if verbose else run_post_apply(post_apply, self.config.logger)
192
+ ) or []
113
193
  for cmd, rc in post_failed:
194
+ stats["failed"] += 1
114
195
  error = self.config.reload_failure == "error"
115
196
  self.config.logger.log(
116
197
  logging.ERROR if error else logging.WARNING,
117
- f"post_apply fallo (exit {rc}): {cmd}",
198
+ f"post_apply failed (exit {rc}): {cmd}",
118
199
  )
119
200
  if post_failed and self.config.reload_failure == "error":
120
201
  failed.append("post_apply")
202
+ completed += len(post_apply)
203
+ progress.update(completed, "Finishing")
121
204
  if publish:
122
205
  self._publish_state(name)
206
+ progress.stop()
207
+ self._set_summary(name, stats, dry_run=False)
123
208
  return failed
124
209
 
125
210
  @property
@@ -153,9 +238,9 @@ class Hueman:
153
238
  if source.write(State(name, revision)):
154
239
  self._save_revision(revision)
155
240
  else:
156
- self.config.logger.warning("estado compartido no publicado; paleta aplicada solo en local")
241
+ self.config.logger.warning("shared state was not published; theme applied locally only")
157
242
  except Exception as exc: # noqa: BLE001 - sync must never break `set`
158
- self.config.logger.warning(f"estado compartido no publicado: {exc}")
243
+ self.config.logger.warning(f"shared state was not published: {exc}")
159
244
 
160
245
  def sync(self):
161
246
  """Pull the shared state and apply it only if its revision is newer.
@@ -165,12 +250,12 @@ class Hueman:
165
250
  and cached data is never applied."""
166
251
  settings = self.config.state_sync
167
252
  if settings is None:
168
- self.config.logger.warning("sync: 'state_sync' no configurado; nada que hacer")
253
+ self.config.logger.warning("sync: 'state_sync' is not configured; nothing to do")
169
254
  return 0, "disabled"
170
255
  try:
171
256
  res = build_source(settings, self.config.logger).read_result()
172
257
  except Exception as exc: # noqa: BLE001 - sync must not crash shell startup
173
- self.config.logger.warning(f"sync: estado compartido ilegible: {exc}")
258
+ self.config.logger.warning(f"sync: shared state is unreadable: {exc}")
174
259
  return 0, "unreachable"
175
260
  if not res.remote:
176
261
  return 0, "unreachable"
@@ -187,17 +272,17 @@ class Hueman:
187
272
  failed = self.set_theme(remote.palette, publish=False)
188
273
  except PlaceholderError as e:
189
274
  # set_theme valida antes de escribir: no hay nada aplicado ni revision que guardar
190
- print(f"Config invalida: {e}", file=sys.stderr)
275
+ print(f"Invalid config: {e}", file=sys.stderr)
191
276
  return 1, "failed"
192
277
  except SystemExit:
193
278
  # load() ya logueo el motivo; la paleta remota no existe aqui (o es invalida).
194
279
  # Sync no debe matar el arranque del shell ni avanzar la revision.
195
280
  self.config.logger.warning(
196
- f"sync: la paleta remota '{remote.palette}' no esta disponible localmente; "
197
- "se omite")
281
+ f"sync: remote palette '{remote.palette}' is unavailable locally; "
282
+ "skipping")
198
283
  return 0, "unknown-palette"
199
284
  if failed:
200
- print(f"Targets con errores: {', '.join(failed)}", file=sys.stderr)
285
+ print(f"Failed targets: {', '.join(failed)}", file=sys.stderr)
201
286
  return 1, "failed"
202
287
  self._save_revision(remote.revision)
203
288
  return 0, "applied"
@@ -205,17 +290,17 @@ class Hueman:
205
290
  def _drift_lines(self):
206
291
  settings = self.config.state_sync
207
292
  local = self.local_revision()
208
- lines = [f"Revision local: {'r%d' % local if local is not None else '(ninguna)'}"]
293
+ lines = [f"Local revision: {'r%d' % local if local is not None else '(none)'}"]
209
294
  try:
210
295
  res = build_source(settings, self.config.logger).read_result()
211
296
  except Exception: # noqa: BLE001
212
297
  res = ReadResult(None, False)
213
298
  remote = res.state
214
299
  if remote is None:
215
- lines.append("Revision remota: (desconocida)" if not res.remote else "Revision remota: (vacia)")
300
+ lines.append("Remote revision: (unknown)" if not res.remote else "Remote revision: (empty)")
216
301
  else:
217
302
  origin = "remote" if res.remote else "local cache - remote unreachable"
218
- lines.append(f"Revision remota: r{remote.revision} ({origin}, paleta {remote.palette})")
303
+ lines.append(f"Remote revision: r{remote.revision} ({origin}, theme {remote.palette})")
219
304
  if not res.remote:
220
305
  drift = "remote unreachable"
221
306
  elif remote is None:
@@ -230,13 +315,13 @@ class Hueman:
230
315
  return lines
231
316
 
232
317
  def status(self) -> None:
233
- print(f"Tema activo: {self.current_theme() or '(ninguno)'}")
318
+ print(f"Active theme: {self.current_theme() or '(none)'}")
234
319
  if self.config.state_sync is not None:
235
320
  print("\n".join(self._drift_lines()))
236
- print(f"Paletas disponibles: {', '.join(self.list_palettes())}")
321
+ print(f"Available palettes: {', '.join(self.list_palettes())}")
237
322
  total = sum(len(t.outputs) for t in self.config.targets)
238
323
  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)")
324
+ print(f"Configured targets: {len(self.config.targets)} ({existing}/{total} outputs on disk)")
240
325
 
241
326
 
242
327
  DOCS_URL = "https://hueman.cuevaneander.tech"
@@ -245,15 +330,15 @@ DOCS_URL = "https://hueman.cuevaneander.tech"
245
330
  def init_config(path: Path, force: bool) -> int:
246
331
  """Copy the packaged example config to `path`; returns the exit code."""
247
332
  if path.exists() and not force:
248
- print(f"Ya existe: {path} (usa --force para sobrescribir)", file=sys.stderr)
333
+ print(f"Already exists: {path} (use --force to overwrite)", file=sys.stderr)
249
334
  return 1
250
335
  try:
251
336
  path.parent.mkdir(parents=True, exist_ok=True)
252
337
  atomic_write_text(path, packaged_example_config().read_text())
253
338
  except OSError as exc:
254
- print(f"No se pudo escribir {path}: {exc}", file=sys.stderr)
339
+ print(f"Could not write {path}: {exc}", file=sys.stderr)
255
340
  return 1
256
- print(f"Config de ejemplo escrito en {path}")
341
+ print(f"Example config written to {path}")
257
342
  return 0
258
343
 
259
344
 
@@ -267,38 +352,113 @@ def _var_arg(text: str):
267
352
  return name, value
268
353
 
269
354
 
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)")
355
+ def _build_parser() -> argparse.ArgumentParser:
356
+ formatter = argparse.RawDescriptionHelpFormatter
357
+ parser = argparse.ArgumentParser(
358
+ prog="hueman",
359
+ description="Apply named color palettes to declarative configuration targets.",
360
+ formatter_class=formatter,
361
+ epilog=(
362
+ "examples:\n"
363
+ " hueman init\n"
364
+ " hueman list\n"
365
+ " hueman set gruvbox --dry-run\n"
366
+ " hueman set gruvbox\n"
367
+ " hueman sync --quiet\n"
368
+ f"\nDocs: {DOCS_URL}"
369
+ ),
370
+ )
371
+ parser.add_argument(
372
+ "--version", action="version", version=f"hueman {__version__}",
373
+ help="Print the installed version and exit.",
374
+ )
375
+ parser.add_argument(
376
+ "--config",
377
+ metavar="PATH",
378
+ help="Use this config file instead of $HUEMAN_CONFIG or ~/.config/hueman/config.yaml.",
379
+ )
274
380
  parser.add_argument(
275
381
  "--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).",
382
+ help="Override a config vars: value for ${NAME} in path fields (repeatable).",
383
+ )
384
+ sub = parser.add_subparsers(dest="command", title="commands", metavar="COMMAND")
385
+
386
+ set_p = sub.add_parser(
387
+ "set",
388
+ help="Apply a palette to configured targets.",
389
+ description="Render a palette into configured targets, then run reload and post-apply hooks.",
390
+ formatter_class=formatter,
391
+ epilog=(
392
+ "examples:\n"
393
+ " hueman set gruvbox --dry-run\n"
394
+ " hueman set gruvbox\n"
395
+ " hueman set gruvbox --only kitty,starship\n"
396
+ " hueman set gruvbox --verbose\n"
397
+ ),
398
+ )
399
+ set_p.add_argument("name", metavar="PALETTE", help="Palette name. Run `hueman list` to see choices.")
400
+ set_p.add_argument(
401
+ "--dry-run", action="store_true",
402
+ help="Show what would change without writing files or running reload hooks.",
403
+ )
404
+ set_p.add_argument(
405
+ "-v", "--verbose", action="store_true",
406
+ help="Show each rendered path, reload command, and post-apply hook.",
278
407
  )
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
408
  set_p.add_argument(
285
409
  "--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.",
410
+ help="Limit to targets by name or glob (repeatable). Does not update "
411
+ "the state file or run post_apply.",
288
412
  )
289
413
 
290
414
  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)")
415
+ "sync",
416
+ help="Apply newer shared state.",
417
+ description="Apply shared state only when its revision is newer than the local revision.",
418
+ formatter_class=formatter,
419
+ epilog=(
420
+ "examples:\n"
421
+ " hueman sync\n"
422
+ " hueman sync --quiet\n"
423
+ ),
424
+ )
425
+ sync_p.add_argument(
426
+ "-q", "--quiet", action="store_true",
427
+ help="Hide network and configuration warnings; show only errors.",
428
+ )
294
429
 
295
430
  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")
431
+ "init",
432
+ help="Write an example config file.",
433
+ description="Write the packaged example config to the resolved config path.",
434
+ formatter_class=formatter,
435
+ epilog=(
436
+ "examples:\n"
437
+ " hueman init\n"
438
+ " hueman init --force\n"
439
+ ),
440
+ )
441
+ init_p.add_argument("--force", action="store_true", help="Overwrite an existing config file.")
442
+
443
+ sub.add_parser(
444
+ "list",
445
+ help="List available palettes.",
446
+ description="List packaged and configured custom palettes in alphabetical order.",
447
+ formatter_class=formatter,
448
+ epilog=("examples:\n hueman list\n"),
449
+ )
450
+ sub.add_parser(
451
+ "status",
452
+ help="Show the active palette and target status.",
453
+ description="Show the active palette, available palettes, and configured target counts.",
454
+ formatter_class=formatter,
455
+ epilog=("examples:\n hueman status\n"),
456
+ )
457
+ return parser
458
+
299
459
 
300
- sub.add_parser("list", help="Listar paletas disponibles")
301
- sub.add_parser("status", help="Mostrar tema activo y estado de los targets")
460
+ def main():
461
+ parser = _build_parser()
302
462
 
303
463
  args = parser.parse_args()
304
464
  if not args.command:
@@ -315,8 +475,8 @@ def main():
315
475
  print("\n".join(loader.list_palettes()))
316
476
  return
317
477
  print(
318
- f"Config no encontrada: {config_path}\n"
319
- f"Crea uno con `hueman init`, o usa --config / HUEMAN_CONFIG (docs: {DOCS_URL})",
478
+ f"Config not found: {config_path}\n"
479
+ f"Create one with `hueman init`, or use --config / HUEMAN_CONFIG (docs: {DOCS_URL})",
320
480
  file=sys.stderr,
321
481
  )
322
482
  sys.exit(1)
@@ -324,20 +484,20 @@ def main():
324
484
  try:
325
485
  tc = Hueman(HuemanConfig(config_path, dict(args.var)))
326
486
  except ConfigError as e:
327
- print(f"Config invalida: {e}", file=sys.stderr)
487
+ print(f"Invalid config: {e}", file=sys.stderr)
328
488
  sys.exit(1)
329
489
  if args.command == "set":
330
490
  only = [p.strip() for v in args.only or [] for p in v.split(",") if p.strip()] or None
331
491
  try:
332
- failed = tc.set_theme(args.name, args.dry_run, only=only)
492
+ failed = tc.set_theme(args.name, args.dry_run, only=only, verbose=args.verbose)
333
493
  except UnknownTargetError as e:
334
494
  print(str(e), file=sys.stderr)
335
495
  sys.exit(1)
336
496
  except PlaceholderError as e:
337
- print(f"Config invalida: {e}", file=sys.stderr)
497
+ print(f"Invalid config: {e}", file=sys.stderr)
338
498
  sys.exit(1)
339
499
  if failed:
340
- print(f"Targets con errores: {', '.join(failed)}", file=sys.stderr)
500
+ print(f"Failed targets: {', '.join(failed)}", file=sys.stderr)
341
501
  sys.exit(1)
342
502
  elif args.command == "sync":
343
503
  if args.quiet: