Skip to content

Commit 9c921dd

Browse files
authored
CLI Colors (#1006)
- Add `_colors.py` module with `ColorMode` enum and `Colors` class - Add global `--color` flag (auto/always/never) to CLI root parser - Support `NO_COLOR` and `FORCE_COLOR` environment variables (CPython-style) - Update `load.py` with semantic colors (info, success, error, highlight, etc.) - Update `ls.py` and `debug_info.py` to use Colors class
2 parents df78a17 + 8b41806 commit 9c921dd

53 files changed

Lines changed: 9161 additions & 362 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

AGENTS.md

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,3 +195,69 @@ $ uv run pytest --cov
195195
- **QA every edit**: Run formatting and tests before committing
196196
- **Minimum Python**: 3.10+ (per pyproject.toml)
197197
- **Minimum tmux**: 3.2+ (as per README)
198+
199+
## CLI Color Semantics (Revision 1, 2026-01-04)
200+
201+
The CLI uses semantic colors via the `Colors` class in `src/tmuxp/_internal/colors.py`. Colors are chosen based on **hierarchy level** and **semantic meaning**, not just data type.
202+
203+
### Design Principles
204+
205+
1. **Structural hierarchy**: Headers > Items > Details
206+
2. **Semantic meaning**: What IS this element?
207+
3. **Visual weight**: What should draw the eye first?
208+
4. **Depth separation**: Parent elements should visually contain children
209+
210+
Inspired by patterns from **jq** (object keys vs values), **ripgrep** (path/line/match distinction), and **mise/just** (semantic method names).
211+
212+
### Hierarchy-Based Colors
213+
214+
| Level | Element Type | Method | Color | Examples |
215+
|-------|--------------|--------|-------|----------|
216+
| **L0** | Section headers | `heading()` | Bright cyan + bold | "Local workspaces:", "Global workspaces:" |
217+
| **L1** | Primary content | `highlight()` | Magenta + bold | Workspace names (braintree, .tmuxp) |
218+
| **L2** | Supplementary info | `info()` | Cyan | Paths (~/.tmuxp, ~/project/.tmuxp.yaml) |
219+
| **L3** | Metadata/labels | `muted()` | Blue | Source labels (Legacy:, XDG default:) |
220+
221+
### Status-Based Colors (Override hierarchy when applicable)
222+
223+
| Status | Method | Color | Examples |
224+
|--------|--------|-------|----------|
225+
| Success/Active | `success()` | Green | "active", "18 workspaces" |
226+
| Warning | `warning()` | Yellow | Deprecation notices |
227+
| Error | `error()` | Red | Error messages |
228+
229+
### Example Output
230+
231+
```
232+
Local workspaces: ← heading() bright_cyan+bold
233+
.tmuxp ~/work/python/tmuxp/.tmuxp.yaml ← highlight() + info()
234+
235+
Global workspaces (~/.tmuxp): ← heading() + info()
236+
braintree ← highlight()
237+
cihai ← highlight()
238+
239+
Global workspace directories: ← heading()
240+
Legacy: ~/.tmuxp (18 workspaces, active) ← muted() + info() + success()
241+
XDG default: ~/.config/tmuxp (not found) ← muted() + info() + muted()
242+
```
243+
244+
### Available Methods
245+
246+
```python
247+
colors = Colors()
248+
colors.heading("Section:") # Cyan + bold (section headers)
249+
colors.highlight("item") # Magenta + bold (primary content)
250+
colors.info("/path/to/file") # Cyan (paths, supplementary info)
251+
colors.muted("label:") # Blue (metadata, labels)
252+
colors.success("ok") # Green (success states)
253+
colors.warning("caution") # Yellow (warnings)
254+
colors.error("failed") # Red (errors)
255+
```
256+
257+
### Key Rules
258+
259+
**Never use the same color for adjacent hierarchy levels.** If headers and items are both blue, they blend together. Each level must be visually distinct.
260+
261+
**Avoid dim/faint styling.** The ANSI dim attribute (`\x1b[2m`) is too dark to read on black terminal backgrounds. This includes both standard and bright color variants with dim.
262+
263+
**Bold may not render distinctly.** Some terminal/font combinations don't differentiate bold from normal weight. Don't rely on bold alone for visual distinction - pair it with color differences.

CHANGES

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,45 @@ $ pipx install --suffix=@next 'tmuxp' --pip-args '\--pre' --force
3131

3232
<!-- To maintainers and contributors: Please add notes for the forthcoming version below -->
3333

34+
### Features
35+
36+
#### CLI Colors (#1006)
37+
38+
New semantic color output for all CLI commands:
39+
40+
- New `--color` flag (auto/always/never) on root CLI for controlling color output
41+
- Respects `NO_COLOR` and `FORCE_COLOR` environment variables per [no-color.org](https://no-color.org) standard
42+
- Semantic color methods: `success()` (green), `warning()` (yellow), `error()` (red), `info()` (cyan), `highlight()` (magenta), `muted()` (blue)
43+
- All commands updated with colored output: `load`, `ls`, `freeze`, `convert`, `import`, `edit`, `shell`, `debug-info`
44+
- Interactive prompts enhanced with color support
45+
- `PrivatePath` utility masks home directory as `~` for privacy protection in output
46+
- Beautiful `--help` output with usage examples
47+
48+
#### Search Command (#1006)
49+
50+
New `tmuxp search` command for finding workspace files:
51+
52+
- Field-scoped search with prefixes: `name:`, `session:`, `path:`, `window:`, `pane:`
53+
- Matching options: `-i` (ignore-case), `-S` (smart-case), `-F` (fixed-strings), `-w` (word)
54+
- Logic operators: `--any` for OR, `-v` for invert match
55+
- Output formats: human (with match highlighting), `--json`, `--ndjson` for automation and piping to `jq`
56+
- Searches local (cwd and parents) and global (~/.tmuxp/) workspaces
57+
58+
#### Enhanced ls Command (#1006)
59+
60+
New output options for `tmuxp ls`:
61+
62+
- `--tree`: Display workspaces grouped by directory
63+
- `--full`: Include complete parsed config content
64+
- `--json` / `--ndjson`: Machine-readable output for automation and piping to `jq`
65+
- Local workspace discovery from current directory and parents
66+
- Source field distinguishes "local" vs "global" workspaces
67+
- "Global workspace directories" section shows XDG vs legacy paths with status
68+
69+
#### JSON Output for debug-info (#1006)
70+
71+
- `tmuxp debug-info --json`: Structured JSON output for automation, issue reporting, and piping to `jq`
72+
3473
### Development
3574

3675
#### Makefile -> Justfile (#1005)

conftest.py

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -98,18 +98,31 @@ def socket_name(request: pytest.FixtureRequest) -> str:
9898
return f"tmuxp_test{next(namer)}"
9999

100100

101+
# Modules that actually need tmux fixtures in their doctests
102+
DOCTEST_NEEDS_TMUX = {
103+
"tmuxp.workspace.builder",
104+
}
105+
106+
101107
@pytest.fixture(autouse=True)
102108
def add_doctest_fixtures(
103109
request: pytest.FixtureRequest,
104110
doctest_namespace: dict[str, t.Any],
105111
tmp_path: pathlib.Path,
112+
monkeypatch: pytest.MonkeyPatch,
106113
) -> None:
107114
"""Harness pytest fixtures to doctests namespace."""
108-
if isinstance(request._pyfuncitem, DoctestItem) and shutil.which("tmux"):
109-
doctest_namespace["server"] = request.getfixturevalue("server")
110-
session: Session = request.getfixturevalue("session")
111-
doctest_namespace["session"] = session
112-
doctest_namespace["window"] = session.active_window
113-
doctest_namespace["pane"] = session.active_pane
115+
if isinstance(request._pyfuncitem, DoctestItem):
116+
# Always provide lightweight fixtures
114117
doctest_namespace["test_utils"] = test_utils
115118
doctest_namespace["tmp_path"] = tmp_path
119+
doctest_namespace["monkeypatch"] = monkeypatch
120+
121+
# Only load expensive tmux fixtures for modules that need them
122+
module_name = request._pyfuncitem.dtest.globs.get("__name__", "")
123+
if module_name in DOCTEST_NEEDS_TMUX and shutil.which("tmux"):
124+
doctest_namespace["server"] = request.getfixturevalue("server")
125+
session: Session = request.getfixturevalue("session")
126+
doctest_namespace["session"] = session
127+
doctest_namespace["window"] = session.active_window
128+
doctest_namespace["pane"] = session.active_pane

docs/api/cli/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ freeze
1616
import_config
1717
load
1818
ls
19+
search
1920
shell
2021
utils
2122
```

docs/api/cli/search.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# tmuxp search - `tmuxp.cli.search`
2+
3+
```{eval-rst}
4+
.. automodule:: tmuxp.cli.search
5+
:members:
6+
:show-inheritance:
7+
:undoc-members:
8+
```

docs/api/internals/colors.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# Colors - `tmuxp._internal.colors`
2+
3+
:::{warning}
4+
Be careful with these! Internal APIs are **not** covered by version policies. They can break or be removed between minor versions!
5+
6+
If you need an internal API stabilized please [file an issue](https://github.com/tmux-python/tmuxp/issues).
7+
:::
8+
9+
```{eval-rst}
10+
.. automodule:: tmuxp._internal.colors
11+
:members:
12+
:show-inheritance:
13+
:undoc-members:
14+
```

docs/api/internals/index.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ If you need an internal API stabilized please [file an issue](https://github.com
99
:::
1010

1111
```{toctree}
12+
colors
1213
config_reader
14+
private_path
1315
types
1416
```

docs/api/internals/private_path.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# Private path - `tmuxp._internal.private_path`
2+
3+
:::{warning}
4+
Be careful with these! Internal APIs are **not** covered by version policies. They can break or be removed between minor versions!
5+
6+
If you need an internal API stabilized please [file an issue](https://github.com/tmux-python/tmuxp/issues).
7+
:::
8+
9+
```{eval-rst}
10+
.. automodule:: tmuxp._internal.private_path
11+
:members:
12+
:show-inheritance:
13+
:undoc-members:
14+
```

docs/cli/convert.md

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@
22

33
# tmuxp convert
44

5-
Convert between YAML and JSON
6-
75
```{eval-rst}
86
.. argparse::
97
:module: tmuxp.cli

docs/cli/debug-info.md

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,6 @@
44

55
# tmuxp debug-info
66

7-
Use to collect all relevant information for submitting an issue to
8-
the project.
9-
107
```{eval-rst}
118
.. argparse::
129
:module: tmuxp.cli

0 commit comments

Comments
 (0)