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.
- {hueman_cli-0.3.0 → hueman_cli-0.3.1}/CONTRIBUTING.md +19 -10
- {hueman_cli-0.3.0/hueman_cli.egg-info → hueman_cli-0.3.1}/PKG-INFO +9 -6
- {hueman_cli-0.3.0 → hueman_cli-0.3.1}/README.md +8 -5
- {hueman_cli-0.3.0 → hueman_cli-0.3.1}/hueman/cli.py +216 -56
- hueman_cli-0.3.1/hueman/data/hueman.config.example.yaml +157 -0
- {hueman_cli-0.3.0 → hueman_cli-0.3.1}/hueman/engine/config.py +17 -17
- {hueman_cli-0.3.0 → hueman_cli-0.3.1}/hueman/engine/palettes.py +7 -7
- {hueman_cli-0.3.0 → hueman_cli-0.3.1}/hueman/engine/reload.py +19 -10
- {hueman_cli-0.3.0 → hueman_cli-0.3.1}/hueman/engine/render.py +56 -34
- {hueman_cli-0.3.0 → hueman_cli-0.3.1}/hueman/engine/statesource.py +19 -19
- {hueman_cli-0.3.0 → hueman_cli-0.3.1}/hueman/engine/targets.py +5 -5
- hueman_cli-0.3.1/hueman/templates/flowlauncher_theme.xaml.j2 +199 -0
- hueman_cli-0.3.1/hueman/templates/telegram.attheme.j2 +157 -0
- hueman_cli-0.3.1/hueman/templates/yasb_root.css.j2 +29 -0
- {hueman_cli-0.3.0 → hueman_cli-0.3.1/hueman_cli.egg-info}/PKG-INFO +9 -6
- {hueman_cli-0.3.0 → hueman_cli-0.3.1}/hueman_cli.egg-info/SOURCES.txt +10 -1
- {hueman_cli-0.3.0 → hueman_cli-0.3.1}/hueman_cli.egg-info/scm_file_list.json +54 -0
- hueman_cli-0.3.1/hueman_cli.egg-info/scm_version.json +8 -0
- hueman_cli-0.3.1/scripts/freeze-docs.sh +92 -0
- hueman_cli-0.3.1/tests/test_cli_help_contract.py +56 -0
- {hueman_cli-0.3.0 → hueman_cli-0.3.1}/tests/test_colors_and_cli.py +43 -2
- {hueman_cli-0.3.0 → hueman_cli-0.3.1}/tests/test_config_and_palettes.py +1 -1
- {hueman_cli-0.3.0 → hueman_cli-0.3.1}/tests/test_config_validation.py +3 -3
- {hueman_cli-0.3.0 → hueman_cli-0.3.1}/tests/test_config_vars.py +2 -2
- {hueman_cli-0.3.0 → hueman_cli-0.3.1}/tests/test_init_command.py +1 -1
- {hueman_cli-0.3.0 → hueman_cli-0.3.1}/tests/test_palette_names.py +1 -1
- {hueman_cli-0.3.0 → hueman_cli-0.3.1}/tests/test_reload_exit_codes.py +11 -6
- {hueman_cli-0.3.0 → hueman_cli-0.3.1}/tests/test_render_modes.py +8 -6
- hueman_cli-0.3.1/tests/test_set_output_contract.py +88 -0
- {hueman_cli-0.3.0 → hueman_cli-0.3.1}/tests/test_state_and_reload.py +2 -2
- {hueman_cli-0.3.0 → hueman_cli-0.3.1}/tests/test_sync_status.py +2 -2
- hueman_cli-0.3.1/tests/test_telegram_template.py +78 -0
- hueman_cli-0.3.1/tests/test_windows_examples_config.py +80 -0
- hueman_cli-0.3.1/tests/test_yasb_flowlauncher_templates.py +120 -0
- hueman_cli-0.3.0/hueman/data/hueman.config.example.yaml +0 -111
- hueman_cli-0.3.0/hueman_cli.egg-info/scm_version.json +0 -8
- {hueman_cli-0.3.0 → hueman_cli-0.3.1}/LICENSE +0 -0
- {hueman_cli-0.3.0 → hueman_cli-0.3.1}/MANIFEST.in +0 -0
- {hueman_cli-0.3.0 → hueman_cli-0.3.1}/SECURITY.md +0 -0
- {hueman_cli-0.3.0 → hueman_cli-0.3.1}/hueman/__init__.py +0 -0
- {hueman_cli-0.3.0 → hueman_cli-0.3.1}/hueman/__main__.py +0 -0
- {hueman_cli-0.3.0 → hueman_cli-0.3.1}/hueman/engine/__init__.py +0 -0
- {hueman_cli-0.3.0 → hueman_cli-0.3.1}/hueman/engine/colors.py +0 -0
- {hueman_cli-0.3.0 → hueman_cli-0.3.1}/hueman/engine/fsutil.py +0 -0
- {hueman_cli-0.3.0 → hueman_cli-0.3.1}/hueman/palettes/arctic-ice.yaml +0 -0
- {hueman_cli-0.3.0 → hueman_cli-0.3.1}/hueman/palettes/autumn.yaml +0 -0
- {hueman_cli-0.3.0 → hueman_cli-0.3.1}/hueman/palettes/catppuccin-latte.yaml +0 -0
- {hueman_cli-0.3.0 → hueman_cli-0.3.1}/hueman/palettes/catppuccin-mocha.yaml +0 -0
- {hueman_cli-0.3.0 → hueman_cli-0.3.1}/hueman/palettes/copperwave.yaml +0 -0
- {hueman_cli-0.3.0 → hueman_cli-0.3.1}/hueman/palettes/crimson-vivid.yaml +0 -0
- {hueman_cli-0.3.0 → hueman_cli-0.3.1}/hueman/palettes/dracula.yaml +0 -0
- {hueman_cli-0.3.0 → hueman_cli-0.3.1}/hueman/palettes/ember-red.yaml +0 -0
- {hueman_cli-0.3.0 → hueman_cli-0.3.1}/hueman/palettes/everforest-dark.yaml +0 -0
- {hueman_cli-0.3.0 → hueman_cli-0.3.1}/hueman/palettes/galaxy.yaml +0 -0
- {hueman_cli-0.3.0 → hueman_cli-0.3.1}/hueman/palettes/gruvbox-light.yaml +0 -0
- {hueman_cli-0.3.0 → hueman_cli-0.3.1}/hueman/palettes/gruvbox.yaml +0 -0
- {hueman_cli-0.3.0 → hueman_cli-0.3.1}/hueman/palettes/kanagawa-dragon.yaml +0 -0
- {hueman_cli-0.3.0 → hueman_cli-0.3.1}/hueman/palettes/matrix.yaml +0 -0
- {hueman_cli-0.3.0 → hueman_cli-0.3.1}/hueman/palettes/mono-contrast.yaml +0 -0
- {hueman_cli-0.3.0 → hueman_cli-0.3.1}/hueman/palettes/nord.yaml +0 -0
- {hueman_cli-0.3.0 → hueman_cli-0.3.1}/hueman/palettes/ocean-deep.yaml +0 -0
- {hueman_cli-0.3.0 → hueman_cli-0.3.1}/hueman/palettes/one-light.yaml +0 -0
- {hueman_cli-0.3.0 → hueman_cli-0.3.1}/hueman/palettes/onyx-gold.yaml +0 -0
- {hueman_cli-0.3.0 → hueman_cli-0.3.1}/hueman/palettes/rose-pine-dawn.yaml +0 -0
- {hueman_cli-0.3.0 → hueman_cli-0.3.1}/hueman/palettes/solarized-light.yaml +0 -0
- {hueman_cli-0.3.0 → hueman_cli-0.3.1}/hueman/palettes/synthwave.yaml +0 -0
- {hueman_cli-0.3.0 → hueman_cli-0.3.1}/hueman/palettes/tokyonight.yaml +0 -0
- {hueman_cli-0.3.0 → hueman_cli-0.3.1}/hueman/palettes/tuxedo.yaml +0 -0
- {hueman_cli-0.3.0 → hueman_cli-0.3.1}/hueman/palettes/vaporwave.yaml +0 -0
- {hueman_cli-0.3.0 → hueman_cli-0.3.1}/hueman/resources.py +0 -0
- {hueman_cli-0.3.0 → hueman_cli-0.3.1}/hueman/templates/atuin_theme.toml.j2 +0 -0
- {hueman_cli-0.3.0 → hueman_cli-0.3.1}/hueman/templates/bottom_theme.toml.j2 +0 -0
- {hueman_cli-0.3.0 → hueman_cli-0.3.1}/hueman/templates/bspwm_colors.conf.j2 +0 -0
- {hueman_cli-0.3.0 → hueman_cli-0.3.1}/hueman/templates/claude_statusline.sh.j2 +0 -0
- {hueman_cli-0.3.0 → hueman_cli-0.3.1}/hueman/templates/dunst_colors.conf.j2 +0 -0
- {hueman_cli-0.3.0 → hueman_cli-0.3.1}/hueman/templates/fzf_opts.j2 +0 -0
- {hueman_cli-0.3.0 → hueman_cli-0.3.1}/hueman/templates/fzf_theme.j2 +0 -0
- {hueman_cli-0.3.0 → hueman_cli-0.3.1}/hueman/templates/herdr_theme_custom.toml.j2 +0 -0
- {hueman_cli-0.3.0 → hueman_cli-0.3.1}/hueman/templates/k9s_theme.yaml.j2 +0 -0
- {hueman_cli-0.3.0 → hueman_cli-0.3.1}/hueman/templates/kitty.conf.j2 +0 -0
- {hueman_cli-0.3.0 → hueman_cli-0.3.1}/hueman/templates/lazydocker_theme.yml.j2 +0 -0
- {hueman_cli-0.3.0 → hueman_cli-0.3.1}/hueman/templates/leaf_theme.toml.j2 +0 -0
- {hueman_cli-0.3.0 → hueman_cli-0.3.1}/hueman/templates/newt_colors.j2 +0 -0
- {hueman_cli-0.3.0 → hueman_cli-0.3.1}/hueman/templates/nvim_nightfox_theme.lua.j2 +0 -0
- {hueman_cli-0.3.0 → hueman_cli-0.3.1}/hueman/templates/omp.omp.json.j2 +0 -0
- {hueman_cli-0.3.0 → hueman_cli-0.3.1}/hueman/templates/opencode_theme.json.j2 +0 -0
- {hueman_cli-0.3.0 → hueman_cli-0.3.1}/hueman/templates/pi_theme.json.j2 +0 -0
- {hueman_cli-0.3.0 → hueman_cli-0.3.1}/hueman/templates/polybar_colors.ini.j2 +0 -0
- {hueman_cli-0.3.0 → hueman_cli-0.3.1}/hueman/templates/posting_theme.yaml.j2 +0 -0
- {hueman_cli-0.3.0 → hueman_cli-0.3.1}/hueman/templates/qutebrowser_github.user.js.j2 +0 -0
- {hueman_cli-0.3.0 → hueman_cli-0.3.1}/hueman/templates/qutebrowser_google.user.js.j2 +0 -0
- {hueman_cli-0.3.0 → hueman_cli-0.3.1}/hueman/templates/qutebrowser_hteam.user.js.j2 +0 -0
- {hueman_cli-0.3.0 → hueman_cli-0.3.1}/hueman/templates/qutebrowser_theme.py.j2 +0 -0
- {hueman_cli-0.3.0 → hueman_cli-0.3.1}/hueman/templates/rofi_theme.rasi.j2 +0 -0
- {hueman_cli-0.3.0 → hueman_cli-0.3.1}/hueman/templates/sshub_theme.toml.j2 +0 -0
- {hueman_cli-0.3.0 → hueman_cli-0.3.1}/hueman/templates/starship_block.toml.j2 +0 -0
- {hueman_cli-0.3.0 → hueman_cli-0.3.1}/hueman/templates/termux_colors.properties.j2 +0 -0
- {hueman_cli-0.3.0 → hueman_cli-0.3.1}/hueman/templates/tmux.conf.j2 +0 -0
- {hueman_cli-0.3.0 → hueman_cli-0.3.1}/hueman/templates/tuicr_theme.toml.j2 +0 -0
- {hueman_cli-0.3.0 → hueman_cli-0.3.1}/hueman/templates/wezterm_colors.lua.j2 +0 -0
- {hueman_cli-0.3.0 → hueman_cli-0.3.1}/hueman/templates/windhawk_taskbar.json.j2 +0 -0
- {hueman_cli-0.3.0 → hueman_cli-0.3.1}/hueman/templates/yazi_theme.toml.j2 +0 -0
- {hueman_cli-0.3.0 → hueman_cli-0.3.1}/hueman/templates/zen_theme.json.j2 +0 -0
- {hueman_cli-0.3.0 → hueman_cli-0.3.1}/hueman_cli.egg-info/dependency_links.txt +0 -0
- {hueman_cli-0.3.0 → hueman_cli-0.3.1}/hueman_cli.egg-info/entry_points.txt +0 -0
- {hueman_cli-0.3.0 → hueman_cli-0.3.1}/hueman_cli.egg-info/requires.txt +0 -0
- {hueman_cli-0.3.0 → hueman_cli-0.3.1}/hueman_cli.egg-info/top_level.txt +0 -0
- {hueman_cli-0.3.0 → hueman_cli-0.3.1}/pyproject.toml +0 -0
- {hueman_cli-0.3.0 → hueman_cli-0.3.1}/requirements.txt +0 -0
- {hueman_cli-0.3.0 → hueman_cli-0.3.1}/setup.cfg +0 -0
- {hueman_cli-0.3.0 → hueman_cli-0.3.1}/tests/conftest.py +0 -0
- {hueman_cli-0.3.0 → hueman_cli-0.3.1}/tests/test_atomic_writes.py +0 -0
- {hueman_cli-0.3.0 → hueman_cli-0.3.1}/tests/test_command_substitution.py +0 -0
- {hueman_cli-0.3.0 → hueman_cli-0.3.1}/tests/test_error_paths.py +0 -0
- {hueman_cli-0.3.0 → hueman_cli-0.3.1}/tests/test_file_source.py +0 -0
- {hueman_cli-0.3.0 → hueman_cli-0.3.1}/tests/test_idempotence.py +0 -0
- {hueman_cli-0.3.0 → hueman_cli-0.3.1}/tests/test_packaging.py +0 -0
- {hueman_cli-0.3.0 → hueman_cli-0.3.1}/tests/test_real_config.py +0 -0
- {hueman_cli-0.3.0 → hueman_cli-0.3.1}/tests/test_render.py +0 -0
- {hueman_cli-0.3.0 → hueman_cli-0.3.1}/tests/test_sandbox.py +0 -0
- {hueman_cli-0.3.0 → hueman_cli-0.3.1}/tests/test_starship_marker.py +0 -0
- {hueman_cli-0.3.0 → hueman_cli-0.3.1}/tests/test_state_branch.py +0 -0
- {hueman_cli-0.3.0 → hueman_cli-0.3.1}/tests/test_state_source_contract.py +0 -0
- {hueman_cli-0.3.0 → hueman_cli-0.3.1}/tests/test_sync_network.py +0 -0
- {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` (
|
|
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
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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.
|
|
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
|
|
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
|
-
-
|
|
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
|
|
90
|
-
|
|
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
|
|
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
|
-
-
|
|
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
|
|
58
|
-
|
|
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
|
|
57
|
-
f"
|
|
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
|
-
|
|
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(
|
|
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
|
|
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
|
|
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
|
-
|
|
106
|
-
|
|
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
|
-
|
|
112
|
-
post_failed =
|
|
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
|
|
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("
|
|
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"
|
|
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'
|
|
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:
|
|
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"
|
|
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:
|
|
197
|
-
"
|
|
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"
|
|
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"
|
|
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("
|
|
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"
|
|
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"
|
|
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"
|
|
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"
|
|
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"
|
|
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"
|
|
339
|
+
print(f"Could not write {path}: {exc}", file=sys.stderr)
|
|
255
340
|
return 1
|
|
256
|
-
print(f"
|
|
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
|
|
271
|
-
|
|
272
|
-
parser
|
|
273
|
-
|
|
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="
|
|
277
|
-
|
|
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="
|
|
287
|
-
"
|
|
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",
|
|
292
|
-
|
|
293
|
-
|
|
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",
|
|
297
|
-
|
|
298
|
-
|
|
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
|
-
|
|
301
|
-
|
|
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
|
|
319
|
-
f"
|
|
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"
|
|
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"
|
|
497
|
+
print(f"Invalid config: {e}", file=sys.stderr)
|
|
338
498
|
sys.exit(1)
|
|
339
499
|
if failed:
|
|
340
|
-
print(f"
|
|
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:
|