diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md new file mode 100644 index 0000000000..08fe30907c --- /dev/null +++ b/.github/CONTRIBUTING.md @@ -0,0 +1,335 @@ +# Contributing + +Thanks for looking. Bug reports with a reproduction, and notes on where the +documentation misled you, are the most useful thing you can bring right now. + +How this project writes prose — README, `CHANGES`, commit messages, +docstrings, and source comments — is set out separately in +[WRITING.md](WRITING.md). Read that before changing any of it. The constraints +every change is held to, and the map of what is where, are in +[AGENTS.md](../AGENTS.md). + +## Getting set up + +Check out the code from GitHub: + +```console +$ git clone git@github.com:tmux-python/tmuxp.git +``` + +```console +$ cd tmuxp +``` + +The easiest way to set up a dev environment is with [uv], which manages the +virtualenv and Python dependencies for you. + +Create the virtualenv and install everything locked in `uv.lock`: + +```console +$ uv sync --all-extras --dev +``` + +To refresh those packages later: + +```console +$ uv sync --all-extras --dev --upgrade +``` + +Then prefix any Python command with `uv run`: + +```console +$ uv run [command] +``` + +[uv]: https://docs.astral.sh/uv + +### Advanced: manual virtualenv + +Prefer to manage the virtualenv yourself? Create one: + +```console +$ virtualenv .venv +``` + +Activate it in your current shell: + +```console +$ source .venv/bin/activate +``` + +Install tmuxp in editable mode, so your edits take effect immediately: + +```console +$ pip install -e . +``` + +With a uv-managed project, add the checkout as an editable dev dependency +instead: + +```console +$ uv add --dev --editable . +``` + +Prefer a one-off, pipx-style run while you hack? Call tmuxp through [uvx]: + +```console +$ uvx tmuxp +``` + +[uvx]: https://docs.astral.sh/uv/guides/tools/ + +## The gates + +CI is the order of record; every gate it runs has to pass before a change is +done. + +Format: + +```console +$ uv run ruff format . +``` + +CI checks formatting without writing: + +```console +$ uv run ruff format --check . +``` + +Lint: + +```console +$ uv run ruff check . +``` + +Autofix what ruff can: + +```console +$ uv run ruff check . --fix --show-fixes +``` + +Type-check: + +```console +$ uv run mypy +``` + +Test: + +```console +$ uv run py.test +``` + +Documentation is a gate, not a courtesy. `[tool.pytest.ini_options]` in +`pyproject.toml` sets `testpaths = ["src/tmuxp", "tests", "docs"]` and +`addopts` includes `--doctest-modules`, so every `>>> ` example under +`src/tmuxp/**` and every doctest in a `.py` file under `docs/` runs as part of +`uv run py.test` — there is no separate doctest step, and a green test run is +the proof. `README.md` is not in `testpaths` and is never executed; hold its +examples correct by review. Which blocks qualify, and the one mistake that +silently removes a test, are in +[WRITING.md](WRITING.md#documented-examples-that-run). + +Before claiming a test or a gate works, show it failing. A gate that has +never been red is an assumption. + +### Imports and typing + +- **Namespace imports for the standard library**: `import pathlib`, then + `pathlib.Path`, not `from pathlib import Path`. Exception: + `from dataclasses import dataclass, field`, since both are used as + decorators/defaults, not namespaced. Third-party packages may use + `from X import Y`. +- **Typing**: `import typing as t`, access via the namespace — + `t.Optional`, `t.NamedTuple`. +- **Every file** starts with `from __future__ import annotations`; ruff's + `isort` config (`required-imports` in `pyproject.toml`) enforces it. + +Ruff's `select` is deliberately unset in `pyproject.toml` — 0.16's curated +default rule set stays enabled, and `extend-select` layers this project's +additional linters (pydocstyle, flake8-bugbear, and the rest) on top rather +than replacing the defaults. + +## Tests + +The suite lives in `tests/`, written with [pytest]. It runs against a real +tmux server on a separate socket (`tmux -L test_case`), so it never disturbs +your own sessions. + +[pytest]: https://pytest.org/ + +Write new tests as standalone functions, not `class TestFoo:` groupings — +descriptive function names and file organization carry the structure instead. +A couple of older suites still use classes; match the file you are in, prefer +functions in a new one. + +- Prefer the `server`, `session`, `window`, `pane` fixtures from + `tests/fixtures/` over manual setup, and real tmux fixtures over + `MagicMock`. +- Use `retry_until` (from `libtmux.test.retry`) for anything that waits on an + async tmux operation instead of a bare sleep. +- Use the `tmp_path` fixture instead of Python's `tempfile`, and + `monkeypatch` instead of `unittest.mock`. +- Plugin tests import mock packages from + `tests/fixtures/pluginsystem/plugins/` — six fixture plugins + (`tmuxp_test_plugin_bwb`, `_bs`, `_r`, `_owc`, `_awf`, `_fail`) exercising + each plugin hook and a deliberate failure path. +- Assert on `caplog.records` attributes, not string matching on + `caplog.text`: scope capture with + `caplog.at_level(logging.DEBUG, logger="libtmux.common")`, filter records + rather than index by position, and assert on schema + (`record.tmux_exit_code == 0`, not `"exit code 0" in caplog.text`). + `caplog.record_tuples` cannot access `extra` fields. + +### Rerun on file change + +```console +$ just start +``` + +Runs the suite once, then watches for changes via [pytest-watcher]. + +[pytest-watcher]: https://github.com/olzhasar/pytest-watcher + +### pytest options + +Pass extra arguments through `PYTEST_ADDOPTS`: + +```console +$ env PYTEST_ADDOPTS="--verbose" just start +``` + +Pick a file: + +```console +$ env PYTEST_ADDOPTS="tests/workspace/test_builder.py" just start +``` + +Drop into a single test and stop on the first error: + +```console +$ env PYTEST_ADDOPTS="-s -x -vv tests/workspace/test_builder.py::test_automatic_rename_option" \ + just start +``` + +Drop into `pdb` on the first error: + +```console +$ env PYTEST_ADDOPTS="-x -s --pdb" just start +``` + +Set `RETRY_TIMEOUT_SECONDS` if a workspace-builder test is stubborn on your +machine: + +```console +$ env RETRY_TIMEOUT_SECONDS=10 uv run py.test +``` + +### Manual invocation + +A single file: + +```console +$ uv run py.test tests/workspace/test_builder.py +``` + +A single test inside it: + +```console +$ uv run py.test tests/test_config.py::test_export_json +``` + +### Visual testing + +Watch the suite build sessions in real time by keeping a client open in a +second terminal. + +Terminal 1 — start a server on the test socket: + +```console +$ tmux -L test_case +``` + +Terminal 2 — from the checkout, run the builder tests: + +```console +$ uv run py.test tests/workspace/test_builder.py +``` + +Terminal 1 flickers as sessions build before your eyes — the building tmuxp +normally hides from users. + +## Documentation + +Rebuild the docs whenever a source file changes: + +```console +$ just watch-docs +``` + +Or build once: + +```console +$ just build-docs +``` + +Serve the built docs locally: + +```console +$ just serve-docs +``` + +`just dev-docs` runs the watcher and the server together; `just design-docs` +adds a static-file watch for theme work. `docs/_build/` is generated — +never hand-edit it. + +After you set up your environment, load the project's own workspace from the +checkout root to see a real multi-pane dev layout: + +```console +$ tmuxp load . +``` + +That loads `.tmuxp.yaml` at the project root. + +## Releasing + +Never create tags. Never push tags. The owner handles tagging and tag pushes, +because a tag triggers the publish workflow. See +[Release commits](WRITING.md#release-commits). + +The full release process — updating `CHANGES`, bumping the version, tagging, +and the CI publish to PyPI — is in +[Releasing](../docs/project/releasing.md). + +## Pull requests + +One subject per pull request. Unrelated cleanup found along the way belongs +in its own commit, and usually in its own pull request. + +Discuss a substantial change via an issue before making it. + +Run the gates above before opening a pull request; update documentation if +your change affects the public interface. A pull request merges once it has +the sign-off of one other developer — if you cannot merge it yourself, +request a reviewer to do so. + +Commit format is in [WRITING.md](WRITING.md#commits). + +## Decorum + +- Participants will be tolerant of opposing views. +- Participants must ensure that their language and actions are free of + personal attacks and disparaging personal remarks. +- When interpreting the words and actions of others, participants should + always assume good intentions. +- Behaviour which can be reasonably considered harassment will not be + tolerated. + +Based on [Ruby's Community Conduct Guideline](https://www.ruby-lang.org/en/conduct/). + +## Security + +Please do not open a public issue for a vulnerability. Use GitHub's private +vulnerability reporting (the repository's Security tab → "Report a +vulnerability"), or contact the maintainer listed in `pyproject.toml`. diff --git a/.github/WRITING.md b/.github/WRITING.md new file mode 100644 index 0000000000..3ab4208a39 --- /dev/null +++ b/.github/WRITING.md @@ -0,0 +1,726 @@ +# Writing + +How this project writes prose, for humans and agents alike. It governs +`README.md`, `CHANGES`, `MIGRATION`, commit messages, CLI help and error text, +docstrings, source comments, and the documentation pages under `docs/` — every +surface a reader reaches. + +For environment setup, the gates, and pull request workflow, see +[CONTRIBUTING.md](CONTRIBUTING.md). + +## Voice + +Three surfaces, one voice. A docstring says what a caller may rely on; a +`CHANGES` entry says what changed; prose says what happens. All three are +present tense, lead with the thing being described, and stop. Why it was built +that way belongs in the commit message, which is timestamped and attached to +the diff. + +The most useful editing operation is deleting the introductory sentence. + +Lead with verbs and name concrete things. Put identifiers in backticks. Prefer +short declarative sentences, one operational fact each. Do not explain Python +to Python developers; do explain this project's semantics. + +Type annotations describe shape. Documentation describes meaning. A sentence +that restates a signature has said nothing. + +Use MUST, SHOULD, and MAY only where the normative sense is meant. Say what +actually happens rather than that something is "supported". + +| Instead of | Prefer | +| --------------------------------- | ----------------------------------- | +| "We added…" | "`tmuxp freeze` now accepts…" | +| "New and improved" | "`WorkspaceBuilder` now…" | +| "powerful", "seamless" | state the capability | +| "easily", "simply", "just" | omit | +| "simple", "obvious", "intuitive" | omit | +| "robust" | name the failure that is handled | +| "comprehensive" | name what is covered | +| "production-ready" | state the guarantee | +| "optimized", "blazingly fast" | give the magnitude | +| "various fixes" | name the components | +| "under the hood" | omit unless observable | +| "please note that", "note that" | state the fact | +| "leverage", "utilize" | "use" | +| "delve into" | "read", or omit | +| "best practices" | name the practice | +| "in order to" | "to" | + +## Who you are writing for + +The default reader runs tmuxp and writes workspace files in YAML or JSON. They +are fluent in tmux itself — servers, sessions, windows, panes, layouts, the +shell and its prompt — but you cannot assume they read Python, know tmuxp's +internals, or have heard of its builder architecture, entry points, or +`sys.path`. Serve them first. + +A second, smaller reader writes Python: custom workspace builders, plugins, or +code against `tmuxp`/[libtmux] directly. Serve them too, but mark their +material opt-in — "for the braver cases", "advanced" — so the default reader +knows they can stop. Never make the common case pay a comprehension tax for the +advanced one. + +[libtmux]: https://github.com/tmux-python/libtmux + +Rules that follow: + +- **Second person, present tense, active.** "You name the builder", not "The + builder is selected". Address the reader who is doing the thing. +- **Concept before configuration.** Open by saying what the thing *is* and + what it does for the reader. The YAML surface — the keys, the flags — is the + last detail they need, not the first. A page that opens with "set these + keys" has buried the idea under its mechanics. +- **Say when they can stop.** Lead with the default and the reassurance: most + readers never touch this, it works out of the box, everything here is + optional. Let a skimmer leave after one paragraph. +- **Grant permission, do not demand attention.** "Reach for this when…" tells + readers they are in the right place without implying they must read on. +- **Progressive disclosure.** Order by how many readers need it: default → the + one option a few will tune → swapping the whole thing → writing your own. + Each step is for a smaller audience than the last. +- **Name the trade-off.** If an option costs something — load time, a slower + attach — say so, and say what it buys ("a little slower, but the workspace + is fully prepped before you attach"). State it; do not sell it. +- **Frame by concept, not by mechanism.** Do not call a feature "the keys" or + "the flags" in prose; that names the implementation surface, which is the + reader's last concern. Name the concept. The mechanics vocabulary — a `Key` + / `Type` / `Default` table — is correct in a reference table, and only + there. + +### What stays precise + +Warm the framing, never the facts. Resolution-order lists, value tables, exact +error strings, and class or function cross-references carry meaning in their +exact form — leave them alone. The friendly voice belongs in the sentences +*around* a precise block, introducing it, not inside it paraphrasing it into +vagueness. + +`docs/configuration/workspace-builders.md` is the worked example: a +concept-first intro, an out-of-the-box reassurance, sections ordered by +shrinking audience, an honest trade-off on the prompt wait, and precise +reference tables left precise. + +## README + +A README is the shortest path from "what is this?" to competent use, not the +project's autobiography. + +The first sentence is a contract. It says what abstraction the reader has been +handed, concretely enough to tell this package apart from the neighbouring +one. + +Get to a runnable command before anything the reader can skip. A logo, a +mission statement, and three paragraphs of history in front of the install +line all cost the same thing. + +State the minimum Python and tmux versions in prose, not only in badges. +`requires-python` in `pyproject.toml` is the authority for Python; the README +must agree with it. + +Examples are executable, not illustrative fiction. Never `tmuxp ` +— show the real command. See +[Documented examples that run](#documented-examples-that-run) for which blocks +are executed and how to write one that qualifies. + +Document the semantic model, not the flag list. `--help` already enumerates +flags; what it cannot say is precedence, filesystem effects, what goes to +stdout versus stderr, and what a non-zero exit means. + +State defaults explicitly — defaults are API. State negative guarantees where +they exist: "does not modify your tmux configuration", "never overwrites a +workspace file without `-y`". They establish boundaries faster than any amount +of description. + +Headings stay conventional and stable, because people deep-link them. Badges +are few and load-bearing. + +## The CLI + +`tmuxp` is the one console script this package ships (`tmuxp = 'tmuxp:cli.cli'` +in `pyproject.toml`). Its subcommands — `load`, `freeze`, `convert`, `import`, +`edit`, `ls`, `search`, `shell`, `debug-info` — are argparse subparsers under +`src/tmuxp/cli/`. + +**Exit statuses.** `0` success, `1` general error (config validation, a tmux +command failure), `2` usage error (invalid arguments — argparse's own +convention). This is the documented contract; see +[Exit Codes](https://tmuxp.git-pull.com/cli/exit-codes.html). Do not invent a +new code without updating that page. + +**stdout vs stderr.** Human-facing text — including errors and warnings — goes +through `tmuxp_echo()` (`tmuxp.log`, re-exported via `tmuxp.cli.utils`) or +`OutputFormatter.emit_text()` (`tmuxp.cli._output`), both of which default to +stdout. Errors are distinguished by a bracketed category tag and color, not by +stream: `colors.error("[Builder Error]") + f" {e}"`. The progress/spinner +display (`tmuxp.cli._progress`) writes to stderr by default, keeping stdout +free for a command's real output. + +**Machine-readable output.** Commands that support `--json`/`--ndjson` route +through `OutputFormatter`. In `--ndjson` mode, `emit()` streams one JSON object +per line immediately; in `--json` mode it buffers and `finalize()` writes a +single indented array; `emit_text()` is a no-op in both — a machine mode never +mixes prose into the payload stream. Machine-output behavior for error and +empty-result paths (e.g. `search` with no matches) is not yet defined project +-wide; those paths currently emit styled text through `emit_text()`, which +silently drops in machine modes rather than emitting a structured error. +Document a command's machine-mode behavior explicitly if you add one, rather +than leaving it to this default. + +**Destructive operations never happen silently.** `tmuxp freeze` and +`tmuxp convert` prompt before overwriting a workspace file; `-y`/`--yes` skips +the prompt. `tmuxp load` on a session name that is already running offers to +attach — it does not kill or replace the running session. Preserve this +invariant in any documentation of a command that writes or replaces state: say +what triggers the prompt and what flag skips it. + +## Workspace files + +A workspace file is YAML or JSON describing a tmux session, window, and pane +layout — the domain term is "workspace", never "config" or "session file" in +prose (a "session" is the live tmux object the workspace file builds). `.yaml` +and `.json` are equivalent input formats through `ConfigReader` +(`tmuxp._internal.config_reader`); document a new top-level key for both, and +keep any YAML example convertible with `tmuxp convert`. + +Values trickle down the hierarchy — session → window → pane — so a key set at +the session level is a default any window or pane can override. State that +inheritance explicitly wherever a new key is introduced; it is not visible from +the schema alone. + +## Documented examples that run + +Examples in this repository are tests. This section is the contract for +writing one the test suite can actually see. + +**A fence tag is cosmetic. Only a `>>> ` prompt executes.** A block written as + + ```python + server = Server() + ``` + +is prose that looks like a test. Nothing collects it, nothing runs it, and it +can be wrong for years. The same block written with prompts is a test: + + ```python + >>> server = Server() + ``` + +This is the single most expensive mistake available when editing documentation, +because removing the prompts leaves a green test suite and a silently deleted +test. When editing a file that contains examples, count the prompts before and +after. + +**The fence tag is `python`.** Not `pycon`, not bare. + +**Where examples run.** `[tool.pytest.ini_options]` in `pyproject.toml` sets +`testpaths = ["src/tmuxp", "tests", "docs"]` and `addopts` includes +`--doctest-modules`. That makes every `>>> ` block inside a `src/tmuxp/**` +docstring, and inside any `.py` file under `docs/` (the Sphinx extensions in +`docs/_ext/`), a collected test. **`README.md` is not in `testpaths`.** Its +code blocks, prompted or not, are never executed — write them for a human +reader, and hold them to correctness by review, not by pytest. `docs/*.md` +pages are likewise not doctest-collected; only the `.py` files under `docs/` +are. + +**Fixtures available to a doctest.** The root `conftest.py` defines an +autouse `add_doctest_fixtures` fixture. Every doctest gets `test_utils`, +`tmp_path`, and `monkeypatch` in its namespace. A doctest also gets `server`, +`session`, `window`, and `pane` — real tmux objects — **only** when its module +name is in `conftest.py`'s `DOCTEST_NEEDS_TMUX` set, currently +`{"tmuxp.workspace.builder.classic"}`, and only when `tmux` is on `PATH`. +Writing `>>> session.name` in a docstring outside that one module raises +`NameError` at test time; it is not a fixture available repository-wide. +Doctests in `DOCTEST_NEEDS_TMUX` modules are auto-marked +`pytest.mark.flaky(reruns=2)` (via `pytest_collection_modifyitems`) because +real tmux/shell timing is not deterministic. Adding a new module to that set +is how you opt a doctest into live tmux fixtures — do it deliberately, and +expect the flaky marker to apply. + +**`# doctest: +SKIP` is not permitted.** It is a workaround that tests nothing. +Use the fixtures, or move the example to `tests/examples//`. + +**Do not downgrade a doctest to a non-executed block to make it pass.** A +`.. code-block::` or an unprompted fence does not run. If an example cannot +pass, fix the example or fix the code. + +**Option flags.** `ELLIPSIS` and `NORMALIZE_WHITESPACE` are enabled globally +(`doctest_optionflags` in `pyproject.toml`), so `...` elides variable output — +useful for session/window/pane IDs like `$3` or `@7` — and whitespace +differences do not fail a comparison. Reach for an inline `# doctest: +FLAG` +only for the block that needs something beyond those two. + +**Docstring examples** use the NumPy `Examples` section: + + Examples + -------- + >>> from tmuxp.cli._output import get_output_mode + >>> get_output_mode(json_flag=True, ndjson_flag=False) + + +**Doctests are not required everywhere.** Sphinx `setup(app)` entry points +(`docs/_ext/tmux_layout.py`, `docs/_ext/aafig.py`) are not testable in +isolation the way Sphinx and docutils themselves leave their own `setup()` +functions and `visit_*`/`depart_*` node methods untested by example. Extract a +testable helper predicate from a complex recursive traversal function and +doctest that instead of the traversal itself. + +**Room to grow.** The docutils collector reads `.md` and `.rst` wherever it is +loaded — currently nowhere in this repository's `testpaths`, since only +`docs/*.py` files are collected under `docs/`. Adding a documentation page's +prompted block to the executed set requires first adding that page's directory +to `testpaths`. The MyST `{doctest}` directive and the reStructuredText +`.. doctest::` directive are available if that is ever adopted; document the +change here when it happens. + +## MyST roles + +Any class, method, function, exception, or attribute that has its own rendered +API page must be cited via the appropriate role — never with plain backticks: +`{class}`, `{meth}`, `{func}`, `{exc}`, `{attr}`, `{mod}`. Doc pages without an +explicit ref label use `{doc}`; internal section anchors use `{ref}`. Plain +backticks are correct for code syntax, environment variables, parameter names, +and file paths that are not doc pages — anything without an autodoc +destination. + +Link the first prose mention of any symbol that has a useful destination on +that page — Python objects, tmuxp or libtmux APIs, CLI command pages, topic +pages, external tools. Use the most specific target available. Do not rely on +a later reference section to satisfy the first-mention rule: if the first +occurrence is a heading or a grid-card teaser, link that occurrence or retitle +so the first prose mention can carry the link. Leave command examples, code +blocks, Mermaid node labels, and literal configuration values as code; link +the surrounding prose instead. After the first linked mention on a page, later +mentions can stay plain unless distance or context makes another link useful. + +**Diagrams.** Mermaid diagrams render to inline SVG at build time (via +`sphinx-gp-mermaid`). Tag any node whose label is a command, code identifier, +or config key with `:::cmd` so it renders monospace; leave prose and concept +nodes unstyled. Prefer `flowchart TD` — wide left-to-right charts do not scale +on narrow viewports. Add `:alt:`, `:name:`, and `:responsive: fit` to every +diagram; use `:responsive: preserve` only when the wide artifact is +intentional and should scroll. + +**Internal API pages** document a module with an `{eval-rst}` block wrapping +`.. automodule:: ` and `:members:`, matching `docs/internals/api/**`. A +bare `.. py:module::` registers a cross-reference target but renders an empty +page — reach for it only on an index page that already carries its own +content (grids, prose) where `automodule` would duplicate members documented +on the leaf pages. + +## The changelog + +`CHANGES` is the changelog, rendered as the project's changelog page, modeled +on Django's release-notes shape: deliverables get titles and prose, not +bullets. + +**Release entry boilerplate.** Every release header is +`## tmuxp X.Y.Z (YYYY-MM-DD)`. The file opens with a +`## tmuxp X.Y.Z (Yet to be released)` placeholder block fenced by +`` and `` HTML +comments — new release entries land immediately below the END marker, never +above it. + +**Unreleased entries carry no lead paragraph and no version summary.** +Speaking for a release — what the version "is", "ships", or "focuses on" — is +presumptuous before its scope is final. Only the person cutting the release +writes the lead paragraph, and only once the version and date are set. Never +write or edit a lead paragraph from a feature branch. + +**A released entry opens with a multi-sentence lead paragraph.** Plain prose, +no italic. Open with the version as sentence subject ("tmuxp X.Y.Z ships …") +so the lead is self-contained when excerpted. Two to four sentences on what +shipped and who cares — user-visible takeaways, not internal mechanism. +Cross-reference detail docs with `{ref}` to keep the lead compact. + +**Each deliverable is a section, not a bullet.** Inside `### What's new`, +every distinct deliverable gets a `#### Deliverable title (#NN)` heading +naming it in user vocabulary, followed by one to three prose paragraphs. Do +not wrap a paragraph in `- ` — bullets are for enumerable lists, not +paragraph containers. Cross-link detail docs ("See {ref}`foo` for details.") +so the entry's prose stays focused. + +**The deliverable test.** Before writing an entry, ask: "What's the +deliverable, in user vocabulary?" If you cannot answer in one sentence, the +entry is not ready. Mechanism — helper internals, byte counters, +schema-validation locations — belongs in pull request descriptions and code +comments, not the changelog. + +**Fixed subheadings**, in this order when present: `### Breaking changes`, +`### Dependencies`, `### What's new`, `### Fixes`, `### Documentation`, +`### Development`. Dev tooling (helper scripts, internal automation) lives +under `### Development`. A breaking change shows the migration path with +concrete inline code — a `# Before` / `# After` fenced block — not a pointer +to one. Dependency floor bumps use the form +``Minimum `pkg>=X.Y.Z` (was `>=X.Y.W`)``. + +**PR refs `(#NN)`** sit in each deliverable's `####` heading. + +**When bullets are appropriate.** Catch-all sections (`### Fixes`, +occasionally `### Documentation`) with three or more genuinely small items use +bullets — one line each, never paragraphs. If a bullet swells past two lines, +promote it to a `#### Title (#NN)` heading with a prose body. + +**Anti-patterns.** Fragile metrics that go stale silently — token ceilings, +third-party version pins, percent benchmarks, exact byte counts. Describe the +capability, not the math. Internal jargon: private symbols (leading-underscore +identifiers), algorithm names exposed for the first time, backend scaffolding. +Walls of text dressed up as bullets. Breaking changes buried mid-entry instead +of given their own subheading at the top. + +**Numbers over adjectives**, where a number is available: "cold start 41 ms to +6 ms" is a sentence; "much faster startup" is a smell. + +**Summarization style.** When asked "what changed in the latest version?" or +similar, lead with the entry's lead paragraph (paraphrased if needed), +followed by each `####` deliverable heading under `### What's new` with a +one-sentence summary. Cite `(#NN)` only if asked for source links. Do not +invent versions, dates, or numbers not present in `CHANGES`. Do not quote line +numbers or file offsets — those shift as the file evolves. + +Versions are PEP 440 identifiers. Semantic-versioning meaning applies to the +documented public API — command names, options, exit statuses, configuration +keys, environment variables, and serialized workspace formats, not only +imported Python symbols. + +## Docstrings + +The prime directive: never restate the type. The annotation is the source of +truth; the docstring carries what the annotation cannot. + +This is documentation debt wearing a docstring: + + def get_id(pane: Pane) -> str: + """Get the pane's identifier. + + Parameters + ---------- + pane : Pane + The pane. + + Returns + ------- + str + The identifier. + """ + +Document instead the dimensions the type system cannot encode: + +- **Mutation.** What it changes in place. +- **Ownership.** What the caller must close, release, or keep alive. +- **Ordering.** Whether results come back in a guaranteed order. +- **Timing.** What has finished by the time the call returns. +- **Failure.** Which exceptions are raised and what triggers each. +- **Idempotence.** Whether calling twice does anything the second time. +- **Concurrency.** Whether calls are coalesced, queued, or independent. +- **Units and ranges.** What a number means and what values are accepted. +- **Boundary behaviour.** What zero, empty, and the maximum do. +- **Platform.** Behaviour that differs by operating system, tmux version, or + dependency version. +- **Security boundary.** What is executed, and what is only read — call this + out explicitly for anything that runs a shell command or `exec`s a string + (`tmuxp shell -c`, `$PYTHONSTARTUP` sourcing, workspace `shell_command`). + +The ambiguity worth resolving by example: whether "retry three times" means +three attempts or four. State it. + +The first sentence stands alone; tooling truncates there. PEP 257 applies: +triple double quotes, an imperative one-line summary ending in a period, a +blank line before any extended description. Do not repeat an introspectable +signature. + +NumPy-style docstrings (the `pydocstyle` convention ruff enforces) are the one +dialect this repository uses, enforced by the linter rather than relitigated +in review. + +**Classes with fields** — `NamedTuple`, dataclasses — document every field in +an `Attributes` section: + +```python +class SearchToken(t.NamedTuple): + """Parsed search token with target fields and raw pattern. + + Attributes + ---------- + fields : tuple[str, ...] + Canonical field names to search (e.g., ('name', 'session_name')). + pattern : str + Raw search pattern before regex compilation. + """ +``` + +Autodoc renders every field whether or not you describe it, so an +undocumented `NamedTuple` field ships to the API docs as "Alias for field +number 0" and a dataclass field ships bare. Document all of them — a class +with three fields and two documented still ships a stub for the third. + +## Source comments + +A comment ships only if it passes all three gates. Fail any: delete or +rewrite. Borderline: delete — borderline means the information is +reconstructible, which is what makes deletion cheap. + +**Loss.** Three years from now, would losing this cost a maintainer real time +rediscovering intent, an invariant, a constraint, or a failure mode the code +and tests do not already make obvious? + +**Elite.** Would SQLite, Redis, the Go standard library, or CPython write this +comment, at this length? Those projects state the constraint and stop. They do +not argue with an imagined objector. + +**Upkeep.** Will it stay true without maintenance? A comment that hand-syncs a +value the code owns — a count, an offset, a line reference, a duplicated +constant — is false the first time that value moves. + +### Ceiling + +One or two lines. A comment reaching four is either carrying several facts, in +which case split it, or arguing, in which case cut it to the fact. + +Rationale, alternatives weighed, and the story of how the code got here belong +in the commit message: timestamped, attached to the exact diff, and free to +maintain. + +A comment often holds both a constraint and the deliberation that found it. +Keep the constraint, cut the deliberation. "Runs at most once per second" +survives; "this is the right trade for now" does not. + +### Keep + +- Why over how: upstream tmux quirks, protocol and compatibility constraints, + performance tradeoffs still part of the contract. +- Invariants, preconditions, ordering, lifetime, and concurrency requirements + that types and tests cannot express. +- Code that looks wrong but is not, so a later cleanup does not reintroduce + the bug. +- A high-level sketch of an algorithm whose local operations do not reveal + the whole. + +### Delete + +- Narration of the next lines; code translated into English. +- Restated names, types, defaults, or control flow. +- Values duplicated from the code and hand-synced. +- Justification, hedging, or apology for a choice. +- Speculation about future requirements. +- History version control already holds, including commented-out code. +- Ticket and issue numbers. They say nothing to a reader without tracker + access, and they rot when the tracker moves. Unfinished work goes in the + tracker, not the source. +- Transient observations — "currently", "for now", "the latest release" — + that go stale with no nearby edit. + +### The upkeep gate in practice + +It reaches values that track our own code. It does not reach frozen external +facts. + +Bad (Delete): + +```python +# There are 321 tests to complete for servers. +``` + +Good (Keep): + +```python +# tmux < 3.2 reports the pane ID only after the command completes, +# so this query must stay separate. +``` + +### Documentation exception + +Minimal usage examples, and parameter, return, and raises entries on public +API are exempt from the loss gate — they serve the caller, not the +maintainer. They are exempt from nothing else. Ceiling: a good man page entry. + +## Terminology and capitalization + +Pick the domain noun and keep it. "Workspace file" is the YAML/JSON on disk; +"session" is the live tmux object it builds — do not call a workspace file a +"config" in one paragraph and a "session file" in the next. If the method is +`capture_pane`, write "capture" everywhere rather than alternating with +"read", "grab", and "snapshot". + +Stable vocabulary is what makes search, deep links, and an agent's retrieval +work at all. + +Python and PyPI keep their own capitalisation. Distribution names are written +as they are published. + +Do not write counts into prose — how many symbols exist, how many tests there +are. They go stale silently and no reader needs them. Counts that pin a +fixture or guard an invariant are different, and belong in code. + +## Markdown + +Prose wraps at 80 columns. Table rows, badge lines, and long links are exempt, +because breaking them harms rendering. A pull request or issue body does not +wrap at all: GitHub renders a single newline as a space in a file and as a +line break in a comment, so a wrapped comment body arrives as ragged stubs. + +GitHub alert blocks — `> [!NOTE]`, `> [!WARNING]` — render as literal text +outside GitHub, so reserve them for at most one load-bearing warning per +document. Write the sentence so it carries the fact on its own, and a +renderer that drops the marker loses nothing. + +Do not use a local absolute path or an email address in anything published. + +## Code blocks + +Code blocks are paste-and-run units: pasting one block runs exactly one +intended action. Executed examples are exempt — the test suite runs them, +nobody pastes them. + +- **One command per block.** Multiple steps may share a block only when + explicitly chained with `&&`, `;`, or `\` continuations — the chain is then + one logical command. +- **Explanations go in prose above the block**, never as `#` comments inside + it. +- **Command menus are per-command blocks with prose lead-ins**, not tables. +- **Shell commands use the `console` tag with a `$ ` prefix.** This separates + interactive commands from scripts and enables prompt-aware copy. +- **Split long commands with `\`** — one flag or flag+value pair per indented + continuation line, positional arguments last. + +Good — show the last ten commits as a graph: + +```console +$ git log \ + --max-count=10 \ + --graph \ + --oneline +``` + +Bad: + +```console +# Show the last ten commits as a graph +$ git log --max-count=10 --graph --oneline +``` + +## Commits + +``` +Scope(type[detail]): concise description + +why: Explanation of necessity or impact. + +what: +- Specific technical changes made +- Focused on a single topic +``` + +Keep the subject to 50 characters or fewer, excluding any trailing `(#NN)` +pull request reference, and wrap body lines at 72. Separate the `why:` and +`what:` blocks with a blank line. + +Routine maintenance commits drop the colon and take a capitalised +description, which is what distinguishes them at a glance in +`git log --oneline`: + +``` +py(deps[dev]) Bump dev packages +ai(rules[AGENTS]) Judge comments by three gates +``` + +Everything that changes behaviour keeps the colon. + +Common types: + +- **feat**: New features or enhancements +- **fix**: Bug fixes +- **refactor**: Code restructuring without functional change +- **docs**: Documentation updates +- **chore**: Maintenance (dependencies, tooling, config) +- **test**: Test-related updates +- **style**: Code style and formatting +- **ci**: Workflow and pipeline changes +- **py(deps)**: Dependencies +- **py(deps[dev])**: Dev dependencies +- **ai(rules[AGENTS])**: AI rule updates (`AGENTS.md`, `CLAUDE.md`) +- **ai(claude[command])**: Claude Code command or skill changes (`.claude/`) + +Example: + +``` +Pane(feat[send_keys]): Add support for a literal flag + +why: Send characters without tmux interpreting them. + +what: +- Add a literal parameter to send_keys +- Pass -l when it is set +``` + +For a multi-line message, use a heredoc so the formatting survives: + +```console +$ git commit -m "$(cat <<'EOF' +Scope(feat[detail]): Concise description + +why: Explanation of the change. + +what: +- First change +- Second change +EOF +)" +``` + +### Release commits + +Never create tags. Never push tags. The owner handles tagging and tag pushes, +because a tag triggers the publish workflow. + +A release commit subject is plain and short: `Tag v`. The detailed +why and what go in the body. Do not use the `Scope(type[detail]):` format for +a release — it buries the lede. + +## Slop prevention + +Treat AI slop as review-hostile noise, not as proof that text or code is +wrong. The goal is to maximise information density. + +- **AI signatures.** No "Generated by", no conversational filler, no + unexplained emoji, no tool metadata. +- **Brittle references.** No hard-coded line numbers, fragile file counts, + dated "as of" claims, bare SHAs, or local absolute paths — unless they are + strict evidentiary artefacts such as a benchmark log. +- **Diff narration.** Do not restate what moved, was renamed, or was removed + in anything the reader holds alongside the diff: code, docstrings, README, + `CHANGES`, or a pull request description. The diff and commit message + already carry it. +- **Branch-internal narrative.** Do not mention intermediate states, + abandoned approaches, or "no longer" behaviour unless users of a published + release actually experienced the old state — **the published-release + test**. When cleaning up in hindsight on a long-running branch, diff against + trunk (not an intermediate state on the same branch) to find what the + branch actually introduced, and default to leaving trunk history alone. +- **Low-value scaffolding.** No ownerless TODOs, unused future-proofing, + debug artefacts, or defensive wrappers around failure modes nothing can + reach. +- **Prose inflation.** The diction table under [Voice](#voice) governs; + replace an inflated word with a concrete description of behaviour, + constraints, or trade-offs. +- **Coded labels.** Write rules and findings as plain imperatives. No `[R1]`, + `Option B`, or any index a reader has to decode. + +Preserve the "why". Never delete a comment documenting an invariant, a +protocol constraint, a platform quirk, or an upstream workaround — those are +the facts [Source comments](#source-comments) keeps, and every other comment +is judged by it. + +**Durable source links.** Link to a pinned revision, never to trunk. A pinned +permalink is not a brittle reference; an unlinked SHA dropped into prose is. +`blob/master/…` links rot silently — the file moves, lines shift, and the +anchor lands on unrelated code while still resolving. + +- Prefer a release tag (`blob/v1.74.0/…`). Most durable, and it tells the + reader which released version the claim held for. +- Otherwise use a 7-character commit ref (`blob/9a29b1a/…`) reachable from + `master`. Use when there is no tag or the claim is about unreleased code. + Never a pull-request-head SHA — it can be rebased or garbage-collected. +- Reserve `blob/master/…` for living documents meant to always show the + latest state, such as this contributing guide. +- Line anchors (`#L120-L145`) are only safe on a pinned ref. diff --git a/.github/contributing.md b/.github/contributing.md deleted file mode 100644 index c0eddac2c2..0000000000 --- a/.github/contributing.md +++ /dev/null @@ -1,27 +0,0 @@ -# Contributing - -When contributing to this repository, please first discuss the change you wish to make via issue, -email, or any other method with the maintainers of this repository before making a change. - -See [developing](../docs/developing.md) for environment setup and [AGENTS.md](../AGENTS.md) for -detailed coding standards. - -## Pull Request Process - -1. **Format and lint**: `uv run ruff format .` then `uv run ruff check . --fix --show-fixes` -2. **Type check**: `uv run mypy` -3. **Test**: `uv run pytest` — all tests must pass before submitting -4. **Document**: Update docs if your change affects the public interface -5. You may merge the Pull Request once you have the sign-off of one other developer. If you - do not have permission to do that, you may request a reviewer to merge it for you. - -## Decorum - -- Participants will be tolerant of opposing views. -- Participants must ensure that their language and actions are free of personal - attacks and disparaging personal remarks. -- When interpreting the words and actions of others, participants should always - assume good intentions. -- Behaviour which can be reasonably considered harassment will not be tolerated. - -Based on [Ruby's Community Conduct Guideline](https://www.ruby-lang.org/en/conduct/) diff --git a/AGENTS.md b/AGENTS.md index 59416179f5..107760d659 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,719 +1,66 @@ # AGENTS.md -This file provides guidance to AI agents (e.g., Claude Code, Cursor, and other LLM-powered tools) when working with code in this repository. - -## Project Overview - -tmuxp is a session manager for tmux that allows users to save and load tmux sessions through YAML/JSON configuration files. It's powered by libtmux and provides a declarative way to manage tmux sessions. - -## Development Commands - -### Testing -- `just test` or `uv run py.test` - Run all tests -- `uv run py.test tests/path/to/test.py::TestClass::test_method` - Run a single test -- `uv run ptw .` - Continuous test runner with pytest-watcher -- `uv run ptw . --now --doctest-modules` - Watch tests including doctests -- `just start` or `just watch-test` - Watch and run tests on file changes - -### Code Quality -- `just ruff` or `uv run ruff check .` - Run linter -- `uv run ruff check . --fix --show-fixes` - Fix linting issues automatically -- `just ruff-format` or `uv run ruff format .` - Format code -- `just mypy` or `uv run mypy` - Run type checking (strict mode enabled) -- `just watch-ruff` - Watch and lint on changes -- `just watch-mypy` - Watch and type check on changes - -### Documentation -- `just build-docs` - Build documentation -- `just serve-docs` - Serve docs locally at http://localhost:8013 -- `just dev-docs` - Watch and serve docs with auto-reload -- `just start-docs` - Alternative to dev_docs - -### CLI Commands -- `tmuxp load ` - Load a tmux session from config -- `tmuxp load -d ` - Load session in detached state -- `tmuxp freeze ` - Export running session to config -- `tmuxp convert ` - Convert between YAML and JSON -- `tmuxp shell` - Interactive Python shell with tmux context -- `tmuxp debug-info` - Collect system info for debugging - -## Architecture - -### Core Components - -1. **CLI Module** (`src/tmuxp/cli/`): Entry points for all tmuxp commands - - `load.py`: Load tmux sessions from config files - - `freeze.py`: Export live sessions to config files - - `convert.py`: Convert between YAML/JSON formats - - `shell.py`: Interactive Python shell with tmux context - -2. **Workspace Module** (`src/tmuxp/workspace/`): Core session management - - `builder.py`: Builds tmux sessions from configuration - - `loader.py`: Loads and validates config files - - `finders.py`: Locates workspace config files - - `freezer.py`: Exports running sessions to config - -3. **Plugin System** (`src/tmuxp/plugin.py`): Extensibility framework - - Plugins extend `TmuxpPlugin` base class - - Hooks: `before_workspace_builder`, `on_window_create`, `after_window_finished`, `before_script`, `reattach` - - Version constraint checking for compatibility - -### Configuration Flow - -1. Load YAML/JSON config via `ConfigReader` (handles includes, environment variables) -2. Expand inline shorthand syntax -3. Trickle down default values (session → window → pane) -4. Validate configuration structure -5. Build tmux session via `WorkspaceBuilder` - -### Key Patterns - -- **Type Safety**: All code uses type hints with mypy strict mode -- **Error Handling**: Custom exception hierarchy based on `TmuxpException` -- **Testing**: Pytest with fixtures for tmux server/session/window/pane isolation -- **Future Imports**: All files use `from __future__ import annotations` - -## Configuration Format - -```yaml -session_name: my-session -start_directory: ~/project -windows: - - window_name: editor - layout: main-vertical - panes: - - shell_command: - - vim - - shell_command: - - git status -``` - -## Environment Variables - -- `TMUXP_CONFIGDIR`: Custom directory for workspace configs -- `TMUX_CONF`: Path to tmux configuration file -- `TMUXP_DEFAULT_COLUMNS/ROWS`: Default session dimensions - -## Testing Guidelines - -- **Use functional tests only**: Write tests as standalone functions, not classes. Avoid `class TestFoo:` groupings - use descriptive function names and file organization instead. -- Use pytest fixtures from `tests/fixtures/` for tmux objects -- Test plugins using mock packages in `tests/fixtures/pluginsystem/` -- Use `retry_until` utilities for async tmux operations -- Run single tests with: `uv run py.test tests/file.py::test_function_name` -- **Use libtmux fixtures**: Prefer `server`, `session`, `window`, `pane` fixtures over manual setup -- **Avoid mocks when fixtures exist**: Use real tmux fixtures instead of `MagicMock` -- **Use `tmp_path`** fixture instead of Python's `tempfile` -- **Use `monkeypatch`** fixture instead of `unittest.mock` - -## Code Style - -- Follow NumPy-style docstrings (pydocstyle convention) -- Use ruff for formatting and linting -- Maintain strict mypy type checking -- Keep imports organized with future annotations at top -- **Prefer namespace imports for stdlib**: Use `import enum` and `enum.Enum` instead of `from enum import Enum`; third-party packages may use `from X import Y` -- **Type imports**: Use `import typing as t` and access via namespace (e.g., `t.Optional`) -- **Development workflow**: Format → Test → Commit → Lint/Type Check → Test → Final Commit - -**Classes with fields** — `NamedTuple`, dataclasses — document every field in -an `Attributes` section: - -```python -class SearchToken(t.NamedTuple): - """Parsed search token with target fields and raw pattern. - - Attributes - ---------- - fields : tuple[str, ...] - Canonical field names to search (e.g., ('name', 'session_name')). - pattern : str - Raw search pattern before regex compilation. - """ -``` - -Autodoc renders every field whether or not you describe it, so an -undocumented `NamedTuple` field ships to the API docs as "Alias for field -number 0" and a dataclass field ships bare. Document all of them — a class -with three fields and two documented still ships a stub for the third. - -## Git Commit Standards - -Format commit messages as: -``` -Scope(type[detail]): concise description - -why: Explanation of necessity or impact. - -what: -- Specific technical changes made -- Focused on a single topic -``` - -Keep the subject ≤50 chars (excluding any trailing `(#NN)` PR ref); wrap -body lines at ≤72 chars. Separate the `why:` and `what:` blocks with a -blank line. - -Common commit types: -- **feat**: New features or enhancements -- **fix**: Bug fixes -- **refactor**: Code restructuring without functional change -- **docs**: Documentation updates -- **chore**: Maintenance (dependencies, tooling, config) -- **test**: Test-related updates -- **style**: Code style and formatting -- **py(deps)**: Dependencies -- **py(deps[dev])**: Dev Dependencies -- **ai(rules[AGENTS])**: AI rule updates -- **ai(claude[rules])**: Claude Code rules (CLAUDE.md) -- **ai(claude[command])**: Claude Code command changes - -Example: -``` -Pane(feat[send_keys]): Add support for literal flag - -why: Enable sending literal characters without tmux interpretation - -what: -- Add literal parameter to send_keys method -- Update send_keys to pass -l flag when literal=True -- Add tests for literal key sending -``` -#### Release commits - -Never create tags. Never push tags. The user handles tagging and tag -pushes (tags trigger the CI publish workflow). - -Release commit subjects are plain and short: `Tag v`. Put -the detailed why/what in the commit body. Don't use the -`Scope(type[detail]):` format for releases — don't bury the lede. - -For multi-line commits, use heredoc to preserve formatting: -```bash -git commit -m "$(cat <<'EOF' -feat(Component[method]) add feature description - -why: Explanation of the change. - -what: -- First change -- Second change -EOF -)" -``` - -## Logging Standards - -These rules guide future logging changes; existing code may not yet conform. - -### Logger setup - -- Use `logging.getLogger(__name__)` in every module -- Add `NullHandler` in library `__init__.py` files -- Never configure handlers, levels, or formatters in library code — that's the application's job - -### Structured context via `extra` - -Pass structured data on every log call where useful for filtering, searching, or test assertions. - -**Core keys** (stable, scalar, safe at any log level): - -| Key | Type | Context | -|-----|------|---------| -| `tmux_cmd` | `str` | tmux command line | -| `tmux_subcommand` | `str` | tmux subcommand (e.g. `new-session`) | -| `tmux_target` | `str` | tmux target specifier (e.g. `mysession:1.2`) | -| `tmux_exit_code` | `int` | tmux process exit code | -| `tmux_session` | `str` | session name | -| `tmux_window` | `str` | window name or index | -| `tmux_pane` | `str` | pane identifier | -| `tmux_config_path` | `str` | workspace config file path | -| `tmux_layout` | `str` | window layout string | - -**Heavy/optional keys** (DEBUG only, potentially large): - -| Key | Type | Context | -|-----|------|---------| -| `tmux_stdout` | `list[str]` | tmux stdout lines (truncate or cap; `%(tmux_stdout)s` produces repr) | -| `tmux_stderr` | `list[str]` | tmux stderr lines (same caveats) | - -Treat established keys as compatibility-sensitive — downstream users may build dashboards and alerts on them. Change deliberately. - -### Key naming rules - -- `snake_case`, not dotted; `tmux_` prefix -- Prefer stable scalars; avoid ad-hoc objects -- Heavy keys (`tmux_stdout`, `tmux_stderr`) are DEBUG-only; consider companion `tmux_stdout_len` fields or hard truncation (e.g. `stdout[:100]`) - -### Lazy formatting - -`logger.debug("msg %s", val)` not f-strings. Two rationales: -- Deferred string interpolation: skipped entirely when level is filtered -- Aggregator message template grouping: `"Running %s"` is one signature grouped ×10,000; f-strings make each line unique - -When computing `val` itself is expensive, guard with `if logger.isEnabledFor(logging.DEBUG)`. - -### stacklevel for wrappers - -Increment for each wrapper layer so `%(filename)s:%(lineno)d` and OTel `code.filepath` point to the real caller. Verify whenever call depth changes. - -### LoggerAdapter for persistent context - -For objects with stable identity (Session, Window, Pane), use `LoggerAdapter` to avoid repeating the same `extra` on every call. Lead with the portable pattern (override `process()` to merge); `merge_extra=True` simplifies this on Python 3.13+. - -### Log levels - -| Level | Use for | Examples | -|-------|---------|----------| -| `DEBUG` | Internal mechanics, tmux I/O, config expansion | tmux command + stdout, trickle-down steps | -| `INFO` | Session lifecycle, user-visible operations | Session created, window added, workspace loaded | -| `WARNING` | Recoverable issues, deprecation, user-actionable config | Deprecated key, missing optional program | -| `ERROR` | Failures that stop an operation | tmux command failed, config validation error | - -Config discovery noise belongs in `DEBUG`; only surprising/user-actionable config issues → `WARNING`. - -### Message style - -- Lowercase, past tense for events: `"session created"`, `"tmux command failed"` -- No trailing punctuation -- Keep messages short; put details in `extra`, not the message string - -### Exception logging - -- Use `logger.exception()` only inside `except` blocks when you are **not** re-raising -- Use `logger.error(..., exc_info=True)` when you need the traceback outside an `except` block -- Avoid `logger.exception()` followed by `raise` — this duplicates the traceback. Either add context via `extra` that would otherwise be lost, or let the exception propagate - -### Testing logs - -Assert on `caplog.records` attributes, not string matching on `caplog.text`: -- Scope capture: `caplog.at_level(logging.DEBUG, logger="libtmux.common")` -- Filter records rather than index by position: `[r for r in caplog.records if hasattr(r, "tmux_cmd")]` -- Assert on schema: `record.tmux_exit_code == 0` not `"exit code 0" in caplog.text` -- `caplog.record_tuples` cannot access extra fields — always use `caplog.records` - -### Output channels - -Two output channels serve different audiences: - -1. **Diagnostics** (`logger.*()` with `extra`): System events for log files, `caplog`, and aggregators. Never styled. -2. **User-facing output**: What the human sees. Styled via `Colors` class. - - Commands with output modes (`--json`/`--ndjson`): prefer `OutputFormatter.emit_text()` from `tmuxp.cli._output` — silenced in non-human modes. - - Human-only commands: use `tmuxp_echo()` from `tmuxp.log` (re-exported via `tmuxp.cli.utils`) for user-facing messages. - - **Undefined contracts:** Machine-output behavior for error and empty-result paths (e.g., `search` with no matches) is not yet defined. These paths currently emit styled text through `formatter.emit_text()`, which is a no-op in machine modes. - -Raw `print()` is forbidden in command/business logic. The `print()` call lives only inside the presenter layer (`_output.py`) or `tmuxp_echo`. - -### Avoid - -- f-strings/`.format()` in log calls -- Unguarded logging in hot loops (guard with `isEnabledFor()`) -- Catch-log-reraise without adding new context -- `print()` for debugging or internal diagnostics — use `logger.debug()` with structured `extra` instead -- Logging secret env var values (log key names only) -- Non-scalar ad-hoc objects in `extra` -- Requiring custom `extra` fields in format strings without safe defaults (missing keys raise `KeyError`) - -## Doctests - -**All functions and methods MUST have working doctests.** Doctests serve as both documentation and tests. - -**CRITICAL RULES:** -- Doctests MUST actually execute - never comment out function calls or similar -- Doctests MUST NOT be converted to `.. code-block::` as a workaround (code-blocks don't run) -- If you cannot create a working doctest, **STOP and ask for help** - -**Available tools for doctests:** -- `doctest_namespace` fixtures: `server`, `session`, `window`, `pane`, `tmp_path`, `test_utils` -- Ellipsis for variable output: `# doctest: +ELLIPSIS` -- Update `conftest.py` to add new fixtures to `doctest_namespace` - -**`# doctest: +SKIP` is NOT permitted** - it's just another workaround that doesn't test anything. Use the fixtures properly - tmux is required to run tests anyway. - -**Using fixtures in doctests:** -```python ->>> from tmuxp.workspace.builder import WorkspaceBuilder ->>> config = {'session_name': 'test', 'windows': [{'window_name': 'main'}]} ->>> builder = WorkspaceBuilder(session_config=config, server=server) # doctest: +ELLIPSIS ->>> builder.build() ->>> builder.session.name -'test' -``` - -**When output varies, use ellipsis:** -```python ->>> session.session_id # doctest: +ELLIPSIS -'$...' ->>> window.window_id # doctest: +ELLIPSIS -'@...' -``` - -**Additional guidelines:** -1. **Use narrative descriptions** for test sections rather than inline comments -2. **Move complex examples** to dedicated test files at `tests/examples//test_.py` -3. **Keep doctests simple and focused** on demonstrating usage -4. **Add blank lines between test sections** for improved readability - -**Doctest exceptions** (patterns where doctests are not required): - -1. **Sphinx/docutils `visit_*`/`depart_*` methods** - tested via integration tests; 0 examples across docutils (851 methods), Sphinx (800+), and CPython's `ast.NodeVisitor` -2. **Sphinx `setup()` functions** - entry points not testable in isolation -3. **Complex recursive traversal functions** - extract helper predicates instead - -**Best practice for node processing**: Extract testable helper functions (like `_is_usage_block()`) and doctest those. Keep complex visitor logic in integration tests. - -## Documentation Standards - -### Code Blocks - -Code blocks are paste-and-run units: pasting one block runs exactly one -intended action. Doctests and other executed examples are exempt — the test -suite runs them, nobody pastes them. - -- **One command per block.** Multiple steps may share a block only when - explicitly chained with `&&`, `;`, or `\` continuations — the chain is - then one logical command. -- **Explanations go in prose above the block**, never as `#` comments inside it. -- **Command menus are per-command blocks with prose lead-ins**, not tables. -- **Shell commands use the `console` tag with a `$ ` prefix.** This separates - interactive commands from scripts and enables prompt-aware copy. -- **Split long commands with `\`** — one flag or flag+value pair per indented - continuation line, positional arguments last. - -Good: - -Show the last ten commits as a graph: - -```console -$ git log \ - --max-count=10 \ - --graph \ - --oneline -``` - -Bad: - -```console -# Show the last ten commits as a graph -$ git log --max-count=10 --graph --oneline -``` - -### Changelog Conventions - -These rules apply when authoring entries in `CHANGES`, which is rendered as the Sphinx changelog page. Modeled on Django's release-notes shape — deliverables get titles and prose, not bullets. - -**Release entry boilerplate.** Every release header is `## tmuxp X.Y.Z (YYYY-MM-DD)`. The file opens with a `## tmuxp X.Y.Z (Yet to be released)` placeholder block fenced by `` and `` HTML comments — new release entries land immediately below the END marker, never above it. - -**Open with a multi-sentence lead paragraph.** Plain prose, no italic. Open with the version as sentence subject (*"tmuxp X.Y.Z ships …"*) so the lead is self-contained when excerpted. Two to four sentences telling the reader what shipped and who cares — user-visible takeaways, not internal mechanism. Cross-reference detail docs with `{ref}` to keep the lead compact. - -**Each deliverable is a section, not a bullet.** Inside `### What's new`, every distinct deliverable gets a `#### Deliverable title (#NN)` heading naming it in user vocabulary, followed by 1-3 prose paragraphs explaining what shipped. Don't wrap a paragraph in `- ` — bullets are for enumerable lists, not paragraph containers. Cross-link detail docs (`See {ref}\`foo\` for details.`) so prose stays focused. - -**The deliverable test.** Before writing an entry, ask: "What's the deliverable, in user vocabulary?" If you can't answer in one sentence, the entry isn't ready. Mechanism (helper internals, byte counters, schema-validation locations) belongs in PR descriptions and code comments, not the changelog. - -**Fixed subheadings**, in this order when present: `### Breaking changes`, `### Dependencies`, `### What's new`, `### Fixes`, `### Documentation`, `### Development`. Dev tooling (helper scripts, internal automation) lives under `### Development`. For breaking changes, show the migration path with concrete inline code (e.g. a `# Before` / `# After` fenced code block). Dependency floor bumps use the form ``Minimum `pkg>=X.Y.Z` (was `>=X.Y.W`)``. - -**PR refs `(#NN)`** sit in each deliverable's `####` heading. - -**When bullets are appropriate.** Catch-all sections (`### Fixes`, occasionally `### Documentation`) with 3+ genuinely small items use bullets — one line each, never paragraphs. If a bullet swells past two lines, promote it to a `#### Title (#NN)` heading with prose body. - -**Anti-patterns.** - -- Fragile metrics: token ceilings, third-party version pins, percent benchmarks, exact byte counts. Describe the *capability*, not the math. -- Internal jargon: private symbols (leading-underscore identifiers), algorithm names exposed for the first time, backend scaffolding. -- Walls of text dressed up as bullets. -- Buried breaking changes — they get their own subheading at the top of the entry. - -**Always link autodoc'd APIs.** Any class, method, function, exception, or attribute that has its own rendered page must be cited via the appropriate role (`{class}`, `{meth}`, `{func}`, `{exc}`, `{attr}`) — never with plain backticks. Doc pages without explicit ref labels use `{doc}`. Plain backticks are correct for code syntax, env vars, parameter names, and file paths that aren't doc pages — anything without an autodoc destination. - -**MyST roles.** Class references use `{class}` (e.g. `{class}\`~tmuxp.workspace.builder.WorkspaceBuilder\``), methods use `{meth}`, functions use `{func}`, exceptions use `{exc}`, attributes use `{attr}`, internal anchors use `{ref}`, doc-path links use `{doc}`. - -**Summarization style.** When a user asks "what changed in the latest version?" or similar, lead with the entry's lead paragraph (paraphrased if needed), followed by each `####` deliverable heading under `### What's new` with a one-sentence summary. Cite `(#NN)` only if the user asks for source links. Don't invent versions, dates, or numbers not present in `CHANGES`. Don't quote line numbers or file offsets — those shift as the file evolves. - -## Important Notes - -- **QA every edit**: Run formatting and tests before committing -- **Minimum Python**: 3.10+ (per pyproject.toml) -- **Minimum tmux**: 3.2+ (as per README) - -## CLI Color Semantics (Revision 1, 2026-01-04) - -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. - -### Design Principles - -1. **Structural hierarchy**: Headers > Items > Details -2. **Semantic meaning**: What IS this element? -3. **Visual weight**: What should draw the eye first? -4. **Depth separation**: Parent elements should visually contain children - -Inspired by patterns from **jq** (object keys vs values), **ripgrep** (path/line/match distinction), and **mise/just** (semantic method names). - -### Hierarchy-Based Colors - -| Level | Element Type | Method | Color | Examples | -|-------|--------------|--------|-------|----------| -| **L0** | Section headers | `heading()` | Bright cyan + bold | "Local workspaces:", "Global workspaces:" | -| **L1** | Primary content | `highlight()` | Magenta + bold | Workspace names (braintree, .tmuxp) | -| **L2** | Supplementary info | `info()` | Cyan | Paths (~/.tmuxp, ~/project/.tmuxp.yaml) | -| **L3** | Metadata/labels | `muted()` | Blue | Source labels (Legacy:, XDG default:) | - -### Status-Based Colors (Override hierarchy when applicable) - -| Status | Method | Color | Examples | -|--------|--------|-------|----------| -| Success/Active | `success()` | Green | "active", "18 workspaces" | -| Warning | `warning()` | Yellow | Deprecation notices | -| Error | `error()` | Red | Error messages | - -### Example Output - -``` -Local workspaces: ← heading() bright_cyan+bold - .tmuxp ~/work/python/tmuxp/.tmuxp.yaml ← highlight() + info() - -Global workspaces (~/.tmuxp): ← heading() + info() - braintree ← highlight() - cihai ← highlight() - -Global workspace directories: ← heading() - Legacy: ~/.tmuxp (18 workspaces, active) ← muted() + info() + success() - XDG default: ~/.config/tmuxp (not found) ← muted() + info() + muted() -``` - -### Available Methods - -```python -colors = Colors() -colors.heading("Section:") # Cyan + bold (section headers) -colors.highlight("item") # Magenta + bold (primary content) -colors.info("/path/to/file") # Cyan (paths, supplementary info) -colors.muted("label:") # Blue (metadata, labels) -colors.success("ok") # Green (success states) -colors.warning("caution") # Yellow (warnings) -colors.error("failed") # Red (errors) -``` - -### Key Rules - -**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. - -**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. - -**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. - -## Comments earn their maintenance cost - -A comment ships only if it passes all three gates. Fail any: delete or rewrite. -Borderline: delete — borderline means the information is reconstructible, which -is what makes deletion cheap. - -**Loss.** Three years from now, would losing this cost a maintainer real time -rediscovering intent, an invariant, a constraint, or a failure mode the code and -tests do not already make obvious? - -**Elite.** Would SQLite, Redis, the Go standard library, or CPython write this -comment, at this length? Those projects state the constraint and stop. They do -not argue with an imagined objector. - -**Upkeep.** Will it stay true without maintenance? A comment that hand-syncs a -value the code owns — a count, an offset, a line reference, a duplicated -constant — is false the first time that value moves. - -### Ceiling - -One or two lines. A comment reaching four is either carrying several facts, in -which case split it, or arguing, in which case cut it to the fact. - -Rationale, alternatives weighed, and the story of how the code got here belong -in the commit message: timestamped, attached to the exact diff, and free to -maintain. - -A comment often holds both a constraint and the deliberation that found it. Keep -the constraint, cut the deliberation. "Runs at most once per second" survives; -"this is the right trade for now" does not. - -### Keep - -- Why over how: upstream quirks, protocol and compatibility constraints, - performance tradeoffs still part of the contract. -- Invariants, preconditions, ordering, lifetime, and concurrency requirements - that types and tests cannot express. -- Code that looks wrong but is not, so a later cleanup does not reintroduce the - bug. -- A high-level sketch of an algorithm whose local operations do not reveal the - whole. - -### Delete - -- Narration of the next lines; code translated into English. -- Restated names, types, defaults, or control flow. -- Values duplicated from the code and hand-synced. -- Justification, hedging, or apology for a choice. -- Speculation about future requirements. -- History version control already holds, including commented-out code. -- Ticket and issue numbers. They say nothing to a reader without tracker access, - and they rot when the tracker moves. Unfinished work goes in the tracker, not - the source. -- Transient observations — "currently", "for now", "the latest release" — - that go stale with no nearby edit. - -### The upkeep gate in practice - -It reaches values that track our own code. It does not reach frozen external -facts. - -Bad (Delete): - -```python -# There are 321 tests to complete for servers. -``` - -Good (Keep): - -```python -# tmux < 3.2 reports the pane ID only after the command completes, -# so this query must stay separate. -``` - -### Documentation exception - -Doctests, minimal usage examples, and param, return, and raises lines on public -API are exempt from the loss gate — they serve the caller, not the maintainer. -They are exempt from nothing else. Ceiling: a good man page entry. - -NumPy-style `Parameters`, `Returns`, and `Attributes` sections and executable -doctests fall under this exception — autodoc ships every field whether or not -you describe it, and a doctest that runs is also a test. TSDoc summaries, -`@param` and `@returns` tags, and the compiled examples fall under this -exception. - -## AI Slop Prevention - -Treat AI slop as **review-hostile noise**, not as proof that text or -code is wrong. The goal is to maximize information density by removing -artifacts that make the repository harder to trust or navigate. - -### The Anti-Slop Rubric - -Before committing, audit all AI-assisted changes for these noise -patterns: - -- **AI Signatures:** Remove "Generated by", footers, conversational - filler ("Certainly!", "Here is..."), unexplained emojis (🤖, ✨), and - AI-tool metadata. -- **Brittle References:** Avoid hard-coded line numbers, fragile - file/test counts, dated "as of" claims, bare SHAs, and local - absolute paths unless they are strict evidentiary artifacts (e.g., - benchmark logs). -- **Diff Narration:** Do not restate what moved, was renamed, or was - removed in artifacts the downstream reader holds: code, docstrings, - README, CHANGES, PR descriptions, or release notes. The diff and - commit message already carry this history. -- **Branch-Internal Narrative:** Do not mention intermediate branch - states, abandoned approaches, or "no longer" behavior unless users - of a published release actually experienced the old state (**The - Published-Release Test**). -- **Low-Value Scaffolding:** Remove ownerless TODOs (`TODO: revisit`), - unused future-proofing, debug artifacts, and defensive wrappers that - do not protect a currently reachable failure mode. -- **Prose Inflation:** Replace generic AI "tells" like *comprehensive, - robust, seamless, production-ready, leverage, delve, tapestry,* and - *best practices* with concrete descriptions of behavior, - constraints, or trade-offs. -- **Coded Labels:** Write rules, options, and findings as plain - imperatives. Don't tag them with codes like `[R1]`, `A1`, or - `Option B` in artifacts a human reads — the reader shouldn't have to - decode an index. Internal agent bookkeeping may use ids; shipped text - may not. - -### Durable Source Links - -Link to a pinned revision, never to trunk. A pinned permalink is not a -brittle reference; an unlinked SHA dropped into prose is. `blob/master/…` -links rot silently — the file moves, lines shift, and the anchor lands -on unrelated code while still resolving. - -- Prefer a release tag (`blob/v1.4.0/…`). Most durable, and it tells - the reader which released version the claim held for. -- Otherwise use a 7-char commit ref (`blob/9a29b1a/…`) reachable from - trunk. Use when there is no tag or the claim is about unreleased - code. Never a PR-head SHA — it can be rebased or garbage-collected. -- Reserve `blob/master/…` for living documents meant to always show the - latest state, such as a contributing guide. -- Line anchors (`#L120-L145`) are only safe on a pinned ref. - -### Preservation & Context - -Subjective cleanup must never remove load-bearing rationale. Adjudicate -comments with the comment policy above; borderline cases are deleted, not -kept. - -- **Preserve the "Why":** You MUST NOT delete comments that document - invariants, protocol constraints, platform quirks, security - boundaries, and upstream workarounds. -- **Evidence is Immune:** Preserve exact counts, dates, and SHAs when - they serve as evidence in benchmark results, release notes, stack - traces, or lockfiles. -- **Behavior Over Inventory:** A useful description explains what - changed for the *system or user*; it does not provide an inventory - of files or functions the diff already shows. - -### The Published-Release Test - -Long-running branches accumulate tactical decisions — renames, -refactors, attempts-then-reverts. When deciding what counts as -branch-internal, use trunk or the parent branch as the baseline — not -intermediate states inside the current branch. Ask: - -> Did users of the most recently published release ever experience -> this old name, old behavior, or bug? - -If the answer is **no**, it is branch-internal narrative. Move it to -the commit message and describe only the final state in the artifact. - -**Keep in shipped artifacts:** -- Deprecations and migration guides for symbols that actually shipped. -- `### Fixes` entries for bugs that affected users of a published - release. -- Comments explaining *why the current code looks this way* - (invariants, platform quirks) that make sense to a reader who never - saw the previous version. - -### Cleanup in Hindsight - -When applying these rules retroactively from inside a feature branch, -first establish scope by diffing against the parent branch (or trunk) -to identify which commits this branch actually introduced. Then: - -- **In-branch commits:** Prompt the user with two options: `fixup!` - commits with `git rebase --autosquash` to address each causal commit - at its source, or a single cleanup commit at branch tip. -- **Trunk/Parent commits:** Default to leaving them alone. Act only on - explicit user instruction. If the user opts in, fold the cleanup - into a single commit at branch tip; do not rewrite shared history. -- **Scope guard:** If cleaning prior slop would touch a colleague's - work or expand the branch beyond its stated goal, stay in lane: - protect the current goal and leave prior slop alone. - -### Change Discipline - -- Make the smallest coherent change that solves the verified problem; - keep unrelated cleanup out of it. -- Reuse an existing file, component, helper, API, or test before adding - a new one. Modify in place when the change fits the file's - responsibility. -- Keep new APIs private until a caller outside the module needs them. +tmuxp is a session manager for tmux: it saves and loads tmux sessions, +windows, and panes from declarative YAML or JSON workspace files, built on +[libtmux](https://github.com/tmux-python/libtmux). + +Follow the conventions already in the tree, and keep a change scoped to what +was asked for. + +## What is here + +| Path | What it is | +| ------------------------- | ------------------------------------------------- | +| `src/tmuxp/cli/` | Subcommands: load, freeze, convert, import, edit, ls, search, shell, debug-info | +| `src/tmuxp/workspace/` | `ConfigReader`, `WorkspaceBuilder`, workspace finders and freezer | +| `src/tmuxp/plugin.py` | `TmuxpPlugin` base class and hook dispatch | +| `src/tmuxp/exc.py` | Exception hierarchy rooted at `TmuxpException` | +| `src/tmuxp/_internal/` | `ConfigReader` backend, `Colors`, non-public helpers | +| `tests/` | pytest suite; runs against a real tmux server | +| `docs/` | Sphinx documentation source | +| `CHANGES` | Changelog, rendered as the docs changelog page | +| `MIGRATION` | Deprecation and migration notes | +| `.tmuxp.yaml` | This project's own dev workspace file (`tmuxp load .`) | + +## Which policy applies + +- Documentation, user-facing text, `CHANGES`, release notes, commit messages, + docstrings, and source comments: + [.github/WRITING.md](.github/WRITING.md) +- Environment, the gates, tests, documentation builds, releases, and pull + requests: [.github/CONTRIBUTING.md](.github/CONTRIBUTING.md) + +Each of those is the single home for its subject. Where a rule seems to be +stated twice, the file listed above is the one that governs. + +Logging conventions and CLI color semantics are scoped to `src/tmuxp/` and +live in [src/tmuxp/AGENTS.md](src/tmuxp/AGENTS.md). + +## Change discipline + +- Make the smallest coherent change that solves the verified problem; keep + unrelated cleanup out of it. +- Reuse an existing file, helper, API, or test before adding a new one. - Add a file only for a durable boundary — a distinct responsibility, - independent reuse, or splitting an oversized high-touch module — not - for a single-use helper or a one-line re-export. - -### Keep Instructions Lean - -Treat this file like code and prune it. - -- Delete a line whose removal would not cause a mistake. -- Move multi-step procedures into skills, path-specific rules into - nested AGENTS.md files, and hard limits into hooks or CI. -- Keep only non-obvious, broadly applicable defaults here. Anything a - reader can infer from the code, a manifest, or a linter does not - belong. + independent reuse, or splitting an oversized module — not for a + single-use helper or a one-line re-export. +- Add a test for every user-visible behaviour change, and a `CHANGES` entry + for every change to the public API, CLI, configuration, or output. +- A passing gate is evidence only once it has been shown capable of failing. + Pair a new test with a deliberate break that proves it bites. + +## Domain facts + +- tmux 3.2+, Python 3.10+ (`requires-python` in `pyproject.toml`); mypy runs + in strict mode. +- One console script (`tmuxp`) and one plugin entry-point group + (`tmuxp.workspace_builders`). +- A workspace file is YAML or JSON; values trickle down session → window → + pane, so a key set at the session level is a default a window or pane can + override. + +## References + +- Changelog: `CHANGES` (rendered at ) +- Docs: +- Upstream: diff --git a/README.md b/README.md index 74befa3c7a..db85c3e413 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ # tmuxp -Session manager for tmux. Save and load your tmux sessions through simple configuration files. Powered by [libtmux](https://github.com/tmux-python/libtmux). +Session manager for tmux. Save and load your tmux sessions through +declarative workspace files. Powered by +[libtmux](https://github.com/tmux-python/libtmux). [![Python Package](https://img.shields.io/pypi/v/tmuxp.svg)](https://pypi.org/project/tmuxp/) [![Docs](https://github.com/tmux-python/tmuxp/workflows/docs/badge.svg)](https://tmuxp.git-pull.com/) @@ -13,75 +15,76 @@ is available on Leanpub and [Amazon Kindle](http://amzn.to/2gPfRhC). Read and browse the book for free [on the web](https://leanpub.com/the-tao-of-tmux/read). -**Have some spare time?** Help us triage and code-review on the tracker. See [issue -#290](https://github.com/tmux-python/tmuxp/discussions/290)! +**Have some spare time?** Help us triage and code-review on the tracker. See +[issue #290](https://github.com/tmux-python/tmuxp/discussions/290)! # Installation pip: ```console -pip install --user tmuxp +$ pip install --user tmuxp ``` -If you're managing the project with [uv](https://docs.astral.sh/uv/), add tmuxp as a dependency instead: +If you're managing the project with [uv](https://docs.astral.sh/uv/), add +tmuxp as a dependency instead: ```console -uv add tmuxp +$ uv add tmuxp ``` -To run tmuxp without installing it globally, similar to `pipx`, invoke it with -[uvx](https://docs.astral.sh/uv/guides/tools/): +To run tmuxp without installing it globally, similar to `pipx`, invoke it +with [uvx](https://docs.astral.sh/uv/guides/tools/): ```console -uvx tmuxp +$ uvx tmuxp ``` Homebrew: ```console -brew install tmuxp +$ brew install tmuxp ``` Debian / ubuntu: ```console -sudo apt install tmuxp +$ sudo apt install tmuxp ``` Nix: ```console -[[ -z $(which tmux) ]] && (nix-env -i tmux && nix-env -i tmuxp) || nix-env -i tmuxp +$ [[ -z $(which tmux) ]] && (nix-env -i tmux && nix-env -i tmuxp) || nix-env -i tmuxp ``` -Find the package for your distro on repology: +Find the package for your distro on repology: + Developmental releases: - [pip](https://pip.pypa.io/en/stable/): ```console - pip install --user --upgrade --pre tmuxp + $ pip install --user --upgrade --pre tmuxp ``` - Or request the pre-release from a uv project environment: + Or allow a pre-release from a uv project environment: ```console - uv add 'tmuxp>=1.10.0b1' + $ uv add tmuxp --prerelease allow ``` - - [uvx](https://docs.astral.sh/uv/guides/tools/): ```console - uvx tmuxp + $ uvx --from 'tmuxp' --prerelease allow tmuxp ``` - [pipx](https://pypa.github.io/pipx/docs/): ```console - pipx install --suffix=@next 'tmuxp' --pip-args '\--pre' --force + $ pipx install --suffix=@next 'tmuxp' --pip-args '\--pre' --force ``` Then use `tmuxp@next load [session]`. @@ -111,30 +114,30 @@ windows: Save as _mysession.yaml_, and load: ```console -tmuxp load ./mysession.yaml +$ tmuxp load ./mysession.yaml ``` Projects with _.tmuxp.yaml_ or _.tmuxp.json_ load via directory: ```console -tmuxp load path/to/my/project/ +$ tmuxp load path/to/my/project/ ``` Load multiple at once (in bg, offer to attach last): ```console -tmuxp load mysession ./another/project/ +$ tmuxp load mysession ./another/project/ ``` Name a session: ```console -tmuxp load -s session_name ./mysession.yaml +$ tmuxp load -s session_name ./mysession.yaml ``` -[simple](https://tmuxp.git-pull.com/configuration/examples/#short-hand-inline-style) and -[very -elaborate](https://tmuxp.git-pull.com/configuration/examples/#super-advanced-dev-environment) +[simple](https://tmuxp.git-pull.com/configuration/examples/#short-hand-inline-style) +and +[very elaborate](https://tmuxp.git-pull.com/configuration/examples/#super-advanced-dev-environment) config examples # User-level configurations @@ -149,11 +152,11 @@ Load your tmuxp config from anywhere by using the filename, assuming _\~/.config/tmuxp/mysession.yaml_ (or _.json_): ```console -tmuxp load mysession +$ tmuxp load mysession ``` -See [author's tmuxp configs](https://github.com/tony/tmuxp-config) and -the projects' +See [author's tmuxp configs](https://github.com/tony/tmuxp-config) and the +projects' [tmuxp.yaml](https://github.com/tmux-python/tmuxp/blob/master/.tmuxp.yaml). # Shell @@ -162,11 +165,9 @@ _New in 1.6.0_: `tmuxp shell` launches into a python console preloaded with the attached server, session, and window in -[libtmux](https://github.com/tmux-python/libtmux) objects. +[libtmux](https://github.com/tmux-python/libtmux) objects: ```console -tmuxp shell - (Pdb) server (Pdb) server.sessions @@ -185,25 +186,26 @@ Window(@3 1:your_window, Session($1 your_project)) Pane(%6 Window(@3 1:your_window, Session($1 your_project)) ``` -Supports [PEP -553](https://www.python.org/dev/peps/pep-0553/) `breakpoint()` +Supports [PEP 553](https://www.python.org/dev/peps/pep-0553/) `breakpoint()` (including `PYTHONBREAKPOINT`). Also supports direct commands via `-c`: ```console -tmuxp shell -c 'print(window.name)' +$ tmuxp shell -c 'print(window.name)' my_window +``` -tmuxp shell -c 'print(window.name.upper())' +```console +$ tmuxp shell -c 'print(window.name.upper())' MY_WINDOW ``` -Read more on [tmuxp shell](https://tmuxp.git-pull.com/cli/shell/) in -the CLI docs. +Read more on [tmuxp shell](https://tmuxp.git-pull.com/cli/shell/) in the CLI +docs. # Pre-load hook -Run custom startup scripts (such as installing project dependencies) -before loading tmux. See the +Run custom startup scripts (such as installing project dependencies) before +loading tmux. See the [before_script](https://tmuxp.git-pull.com/configuration/examples/#bootstrap-project-before-launch) example @@ -220,18 +222,18 @@ You can also load sessions in the background by passing `-d` flag Snapshot your tmux layout, pane paths, and window/session names. ```console -tmuxp freeze session-name +$ tmuxp freeze session-name ``` -See more about [freezing -tmux](https://tmuxp.git-pull.com/cli/freeze/) sessions. +See more about [freezing tmux](https://tmuxp.git-pull.com/cli/freeze/) +sessions. # Convert a session file Convert a session file from yaml to json and vice versa. ```console -tmuxp convert filename +$ tmuxp convert filename ``` This will prompt you for confirmation and shows you the new file that is @@ -240,34 +242,38 @@ going to be written. You can auto confirm the prompt. In this case no preview will be shown. ```console -tmuxp convert -y filename -tmuxp convert --yes filename +$ tmuxp convert -y filename +``` + +Or the long form: + +```console +$ tmuxp convert --yes filename ``` # Plugin System -tmuxp has a plugin system to allow for custom behavior. See more about -the [Plugin System](https://tmuxp.git-pull.com/topics/plugins/). +tmuxp has a plugin system to allow for custom behavior. See more about the +[Plugin System](https://tmuxp.git-pull.com/topics/plugins/). # Debugging Helpers -The `load` command provides a way to log output to a log file for -debugging purposes. +The `load` command provides a way to log output to a log file for debugging +purposes. ```console -tmuxp load --log-file . +$ tmuxp load --log-file . ``` Collect system info to submit with a Github issue: ```console -tmuxp debug-info +$ tmuxp debug-info ------------------ environment: system: Linux arch: x86_64 - -# ... so on +... ``` # Docs / Reading material @@ -282,10 +288,10 @@ online](https://tmuxp.git-pull.com/about_tmux/). # Donations -Your donations fund development of new features, testing and support. -Your money will go directly to maintenance and development of the -project. If you are an individual, feel free to give whatever feels -right for the value you get out of the project. +Your donations fund development of new features, testing and support. Your +money will go directly to maintenance and development of the project. If you +are an individual, feel free to give whatever feels right for the value you +get out of the project. See donation options at . diff --git a/docs/AGENTS.md b/docs/AGENTS.md deleted file mode 100644 index da88da5f44..0000000000 --- a/docs/AGENTS.md +++ /dev/null @@ -1,119 +0,0 @@ -# Documentation voice - -This file covers the *voice* of prose under `docs/` — how to frame a -feature page so a reader meets the idea before its configuration. It -complements the repository-root `AGENTS.md`, which already governs code -blocks, shell-command formatting, changelog conventions, and MyST -roles. When the two overlap, the root file wins; this one only answers -the question it leaves open: how should the prose sound? - -## Who you are writing for - -The default reader runs tmuxp and writes workspace files in YAML or -JSON. They are fluent in tmux itself — servers, sessions, windows, -panes, layouts, the shell and its prompt — but you cannot assume they -read Python, know tmuxp's internals, or have heard of its builder -architecture, entry points, or `sys.path`. - -A second, smaller reader writes Python: custom builders, plugins, code -against libtmux. Serve them too, but mark their material as opt-in -("for the braver cases", "advanced") so the default reader knows they -can stop. Never make the common case pay a comprehension tax for the -advanced one. - -## Voice - -- **Second person, present tense, active.** "You name the builder", not - "The builder is selected". Address the reader who is doing the thing. -- **Concept before configuration.** Open by saying what the thing *is* - and what it does for the reader. The YAML surface — the keys, the - flags — is the last detail they need, not the first. A page that - opens with "set these keys" has buried the idea under its mechanics. -- **Say when they can stop.** Lead with the default and the - reassurance: most readers never touch this, it works out of the box, - everything here is optional. Let a skimmer leave after one sentence. -- **Progressive disclosure.** Order by how many readers need it: - default → the one option a few will tune → swapping the whole thing - → writing your own. Each step is for a smaller audience than the last. -- **Name the trade-off.** If an option costs something — load time, a - slower attach — say so, and say what it buys ("a little slower, but - the workspace is fully prepped before you attach"). State it; don't - sell it. -- **Frame by concept, not by mechanism.** Don't call a feature "the - keys" or "the flags" in prose; that names the implementation surface, - which is the reader's last concern. Name the concept. The mechanics - vocabulary — a `Key` / `Type` / `Default` table — is correct in a - reference table, and only there. - -## What stays precise - -Warm the framing, never the facts. Resolution-order lists, value -tables, exact error strings, and class or function cross-references -carry meaning in their exact form — leave them alone. The friendly -voice belongs in the sentences *around* a precise block, introducing -it, not inside it paraphrasing it into vagueness. - -## Cross-references - -Point the advanced reader at the deep-dive rather than inlining it, and -put the link where their interest peaks — on the phrase that made them -curious ("write your own") — not as a standalone footnote the eye -skips. Use the MyST roles listed in the root `AGENTS.md`. - -Link the first prose mention of any symbol that has a useful destination on -that page. This includes Python objects, tmuxp APIs, libtmux APIs, CLI command -pages, topic/configuration pages, and external tools or projects. Use the most -specific target available: `{class}`, `{meth}`, `{func}`, `{mod}`, `{exc}`, or -`{attr}` for API objects; `{ref}` or `{doc}` for documentation pages and -section anchors; and a Markdown link or reference link for external projects. -After the first linked mention on a page, later mentions can stay plain unless -the distance or context makes another link useful. - -Do not rely on a later reference section to satisfy the first-mention rule. If -the first occurrence would be a heading, grid-card teaser, or introductory -sentence, link that occurrence or retitle the heading so the first prose mention -can carry the link. Leave command examples, code blocks, Mermaid node labels, -and literal configuration values as code; link the surrounding prose instead. - -## A page that does this - -`docs/configuration/workspace-builders.md` is the worked example: -a concept-first intro, an out-of-the-box reassurance, sections ordered -by shrinking audience, an honest trade-off on the prompt wait, and -precise reference tables left precise. Read it before reshaping another -page. - -## Diagrams and reference pages - -Two mechanical conventions, separate from voice: - -- **Mermaid diagrams** render to inline SVG at build time (via the - `sphinx-gp-mermaid` package). Tag any node whose label is a command, - code identifier, config key, or other symbol with `:::cmd` so it - renders monospace — - the way that text reads as code inline; leave prose and concept nodes - unstyled. Prefer top-to-bottom (`flowchart TD`); wide left-to-right - charts don't scale on narrow viewports. Add `:alt:`, `:name:`, and - `:responsive: fit` to every diagram; use `:responsive: preserve` only - when the wide artifact is intentional and should scroll instead of - shrinking. `docs/configuration/workspace-builders.md` is the reference. -- **Internal API pages** document a module with an `{eval-rst}` block - wrapping `.. automodule:: ` (with `:members:`), the way the - existing `docs/internals/api/**` pages do. A bare `.. py:module::` - registers a cross-reference target but renders an empty page — reach - for it only to add a *package* target to an index page that already - carries its own content (grids, prose), where `automodule` would - duplicate members documented on the leaf pages. - -## Before you commit - -- Does the page open with what the feature *is*, or with how to - configure it? -- Can a reader who needs only the default stop after the first - paragraph? -- Is anything framed as "the keys/flags" that should be named by - concept instead? -- Are the advanced and Python-only parts clearly marked opt-in? -- Did you leave every table, error string, and cross-reference exact? -- Are diagram command/symbol nodes tagged `:::cmd`, and is the chart - vertical unless it has a reason to be wide? diff --git a/docs/project/code-style.md b/docs/project/code-style.md index 8ce7748dbe..48335aad2b 100644 --- a/docs/project/code-style.md +++ b/docs/project/code-style.md @@ -1,35 +1,13 @@ # Code Style -## Formatting +This page's content split across two files in the repository root, which are +also what an AI coding agent reads: -tmuxp uses [ruff](https://github.com/astral-sh/ruff) for both linting and formatting. +- Formatting, linting, type checking, and import conventions — the commands + and the gates they are part of — moved to [CONTRIBUTING.md], under "The + gates" and "Imports and typing". +- The docstring convention — the prose rule, not the command that enforces + it — moved to [WRITING.md], under "Docstrings". -```console -$ uv run ruff format . -``` - -```console -$ uv run ruff check . --fix --show-fixes -``` - -## Type Checking - -Strict [mypy](https://mypy-lang.org/) is enforced. - -```console -$ uv run mypy -``` - -## Docstrings - -All public functions and methods use -[NumPy-style docstrings](https://numpydoc.readthedocs.io/en/latest/format.html). - -## Imports - -- Standard library: namespace imports (`import pathlib`, not `from pathlib import Path`) - - Exception: `from dataclasses import dataclass, field` for - {func}`~dataclasses.dataclass` and {func}`~dataclasses.field` -- Typing: `import typing as t`, access via {data}`t.Optional `, - {class}`t.NamedTuple `, etc. -- All files: `from __future__ import annotations` +[CONTRIBUTING.md]: https://github.com/tmux-python/tmuxp/blob/master/.github/CONTRIBUTING.md +[WRITING.md]: https://github.com/tmux-python/tmuxp/blob/master/.github/WRITING.md diff --git a/docs/project/contributing.md b/docs/project/contributing.md index 49214355bf..13c7fa7f34 100644 --- a/docs/project/contributing.md +++ b/docs/project/contributing.md @@ -1,297 +1,15 @@ (developing)= - -# Developing and Testing - -The tests live in `tests/`, written with [pytest]. They run against a real tmux -server on a separate socket (`$ tmux -L test_case`), so they never disturb your -own sessions. - -[pytest]: http://pytest.org/ - (install-dev-env)= - -## Install the latest code from git - -### Get the source - -Check out the code from GitHub: - -```console -$ git clone git@github.com:tmux-python/tmuxp.git -``` - -```console -$ cd tmuxp -``` - -### Bootstrap - -The easiest way to set up a dev environment is with [uv], which manages the -virtualenv and Python dependencies for you. (See [uv's documentation] to install -uv itself.) - -Create the virtualenv and install everything locked in `uv.lock`: - -```console -$ uv sync --all-extras --dev -``` - -To refresh those packages later: - -```console -$ uv sync --all-extras --dev --upgrade -``` - -Then prefix any Python command with `uv run`: - -```console -$ uv run [command] -``` - -That's it — you're ready to code. - -[uv]: https://github.com/astral-sh/uv -[uv's documentation]: https://docs.astral.sh/uv - -### Advanced: manual virtualenv - -Prefer to manage the virtualenv yourself? Create one: - -```console -$ virtualenv .venv -``` - -Activate it in your current shell: - -```console -$ source .venv/bin/activate -``` - -Install tmuxp in editable mode, so your edits take effect immediately: - -```console -$ pip install -e . -``` - -With a uv-managed project, add the checkout as an editable dev dependency -instead: - -```console -$ uv add --dev --editable . -``` - -Prefer a one-off, pipx-style run while you hack? Call tmuxp through [uvx]: - -```console -$ uvx tmuxp -``` - -[uvx]: https://docs.astral.sh/uv/guides/tools/ - -## Test runner - -[pytest] runs the tests. Inside the virtualenv, the `tmuxp` command and a -project-local `python` are already on your `PATH`. - -### Rerun on file change - -Watch files and re-run tests on every save, via [pytest-watcher]: - -```console -$ just start -``` - -[pytest-watcher]: https://github.com/olzhasar/pytest-watcher - -### Manual - -```console -$ uv run py.test -``` - -Or: - -```console -$ just test -``` - -### pytest options - -Pass extra arguments through `PYTEST_ADDOPTS`. See the [pytest usage docs] for -everything it accepts. - -[pytest usage docs]: https://docs.pytest.org/ - -Verbose: - -```console -$ env PYTEST_ADDOPTS="--verbose" just start -``` - -Pick a file: - -```console -$ env PYTEST_ADDOPTS="tests/workspace/test_builder.py" just start -``` - -Drop into a single test and stop on the first error: - -```console -$ env PYTEST_ADDOPTS="-s -x -vv tests/workspace/test_builder.py::test_automatic_rename_option" \ - just start -``` - -Drop into `pdb` on the first error: - -```console -$ env PYTEST_ADDOPTS="-x -s --pdb" just start -``` - -With [ipython] installed: - -```console -$ env PYTEST_ADDOPTS="--pdbcls=IPython.terminal.debugger:TerminalPdb" just start -``` - -[ipython]: https://ipython.org/ - (test-specific-tests)= - -### Manual invocation - -Test a single file: - -```console -$ py.test tests/test_config.py -``` - -A single test inside it: - -```console -$ py.test tests/test_config.py::test_export_json -``` - -Several at once, space-separated: - -```console -$ py.test tests/test_{window,pane}.py tests/test_config.py::test_export_json -``` - (test-builder-visually)= - -### Visual testing - -You can watch the suite build sessions in real time by keeping a client open in -a second terminal. - -Terminal 1 — start a server on the test socket: - -```console -$ tmux -L test_case -``` - -Terminal 2 — from the tmuxp checkout (and your virtualenv, if you use one), run -the builder tests: - -```console -$ py.test tests/workspace/test_builder.py -``` - -Terminal 1 flickers as sessions build before your eyes — the building tmuxp -normally hides from users. - -### Testing options - -Set `RETRY_TIMEOUT_SECONDS` if certain workspace-builder tests are stubborn on -your machine, e.g. `RETRY_TIMEOUT_SECONDS=10 py.test`. CI runs the same suite: - -```{literalinclude} ../../.github/workflows/tests.yml -:language: yaml -``` - -## Documentation - -Rebuild the docs whenever a source file changes: - -```console -$ just watch-docs -``` - (tmuxp-developer-config)= - -## tmuxp developer config - -```{image} /_static/tmuxp-dev-screenshot.png -:width: 1030 -:height: 605 -:align: center -:loading: lazy -``` - -After you {ref}`install-dev-env`, load the project's own workspace from the -checkout root: - -```console -$ tmuxp load . -``` - -This loads the `.tmuxp.yaml` at the project root: - -```{literalinclude} ../../.tmuxp.yaml -:language: yaml -``` - -## Formatting - -### Linting - -The project uses [ruff] for linting, import sorting, and formatting. - -Lint: - -```console -$ just ruff -``` - -Autofix what ruff can: - -```console -$ uv run ruff check . --fix --show-fixes -``` - -#### Formatting - -[ruff format] handles formatting: - -```console -$ just ruff-format -``` - -### Type checking - -[mypy] does static type checking: - -```console -$ just mypy -``` - -Re-check on change: - -```console -$ just watch-mypy -``` - (gh-actions)= -## Continuous integration +# Developing and Testing -tmuxp uses [GitHub Actions] for continuous integration. To see the tmux and -Python versions under test, read [.github/workflows/tests.yml]. Builds run on -`master` and on pull requests, and are visible on the [build site]. +Environment setup, the test suite, and the CI gates moved to +[CONTRIBUTING.md] in the repository root, which is also the file an +AI coding agent reads. This page stays published so existing links and +search results keep resolving. -[ruff]: https://ruff.rs -[ruff format]: https://docs.astral.sh/ruff/formatter/ -[mypy]: http://mypy-lang.org/ -[GitHub Actions]: https://github.com/features/actions -[build site]: https://github.com/tmux-python/tmuxp/actions?query=workflow%3Atests -[.github/workflows/tests.yml]: https://github.com/tmux-python/tmuxp/blob/master/.github/workflows/tests.yml +[CONTRIBUTING.md]: https://github.com/tmux-python/tmuxp/blob/master/.github/CONTRIBUTING.md diff --git a/pyproject.toml b/pyproject.toml index 7290e63a0d..b70b097c46 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -120,6 +120,15 @@ lint = [ requires = ["hatchling"] build-backend = "hatchling.build" +[tool.hatch.build.targets.wheel] +# AGENTS.md (+ its CLAUDE.md symlink) is contributor guidance, not a package +# resource; hatchling's default src-layout packaging would otherwise ship it +# inside every install. +exclude = [ + "src/tmuxp/AGENTS.md", + "src/tmuxp/CLAUDE.md", +] + [tool.uv.exclude-newer-package] # git-pull packages release in lockstep with their workspaces, so a # fresh release blocking on the 3-day cooldown blocks every diff --git a/src/tmuxp/AGENTS.md b/src/tmuxp/AGENTS.md new file mode 100644 index 0000000000..f2e2837e3e --- /dev/null +++ b/src/tmuxp/AGENTS.md @@ -0,0 +1,119 @@ +# src/tmuxp/AGENTS.md + +Conventions scoped to this package: logging, and the CLI's color semantics. +Everything else — prose policy, the gates, tests — is in the root +[AGENTS.md](../../AGENTS.md) and the files it points to. + +## Logging + +These rules guide logging changes; existing code may not yet conform to all +of them. + +**Setup.** `logging.getLogger(__name__)` in every module. Add a +`NullHandler` in library `__init__.py` files. Never configure handlers, +levels, or formatters in library code — that is the application's job. + +**Structured context via `extra`.** Pass structured data on every log call +where useful for filtering, searching, or test assertions. + +Core keys (stable, scalar, safe at any log level): + +| Key | Type | Context | +| --------------------- | ------------- | ------------------------------------ | +| `tmux_cmd` | `str` | tmux command line | +| `tmux_subcommand` | `str` | tmux subcommand (e.g. `new-session`) | +| `tmux_target` | `str` | tmux target specifier (`mysession:1.2`) | +| `tmux_exit_code` | `int` | tmux process exit code | +| `tmux_session` | `str` | session name | +| `tmux_window` | `str` | window name or index | +| `tmux_pane` | `str` | pane identifier | +| `tmux_config_path` | `str` | workspace file path | +| `tmux_layout` | `str` | window layout string | + +Heavy/optional keys (DEBUG only, potentially large): `tmux_stdout`, +`tmux_stderr` (`list[str]`; truncate or cap — `%(tmux_stdout)s` produces a +`repr`). + +Treat established keys as compatibility-sensitive — downstream users may +build dashboards and alerts on them. Change deliberately. Keys are +`snake_case`, not dotted, with a `tmux_` prefix; prefer stable scalars over +ad-hoc objects. + +**Lazy formatting.** `logger.debug("msg %s", val)`, not an f-string: the +interpolation is skipped entirely when the level is filtered, and a log +aggregator groups `"Running %s"` as one signature instead of one per distinct +value. Guard an expensive `val` with `if logger.isEnabledFor(logging.DEBUG)`. + +**`stacklevel` for wrappers.** Increment it for each wrapper layer so +`%(filename)s:%(lineno)d` and the OpenTelemetry `code.filepath` attribute +point at the real caller. Verify whenever call depth changes. + +**`LoggerAdapter` for persistent context.** For objects with stable identity +(`Session`, `Window`, `Pane`), use `LoggerAdapter` instead of repeating the +same `extra` on every call. Override `process()` to merge extras; on Python +3.13+, `merge_extra=True` does this for you. + +**Log levels.** + +| Level | Use for | +| ---------- | ---------------------------------------------------- | +| `DEBUG` | Internal mechanics: tmux I/O, config expansion | +| `INFO` | Session lifecycle: session created, window added | +| `WARNING` | Recoverable, user-actionable: deprecated key, missing optional program | +| `ERROR` | Failures that stop an operation: tmux command failed, validation error | + +Config-discovery noise belongs in `DEBUG`; only a surprising or +user-actionable config issue is `WARNING`. + +**Message style.** Lowercase, past tense for events (`"session created"`, +`"tmux command failed"`), no trailing punctuation, details in `extra` rather +than the message string. + +**Exception logging.** `logger.exception()` only inside an `except` block you +are not re-raising from. `logger.error(..., exc_info=True)` for a traceback +outside an `except` block. Avoid `logger.exception()` followed by `raise` — +it duplicates the traceback; either add `extra` context or let the exception +propagate. + +**Output channels.** Two channels serve different audiences and are never +mixed: + +1. Diagnostics — `logger.*()` with `extra` — for log files, `caplog`, and + aggregators. Never styled. +2. User-facing output — what the human sees, styled via `Colors`. Commands + with `--json`/`--ndjson` output modes use `OutputFormatter.emit_text()` + from `tmuxp.cli._output` (silenced in machine modes); human-only commands + use `tmuxp_echo()` from `tmuxp.log` (re-exported via `tmuxp.cli.utils`). + +Raw `print()` is forbidden in command and business logic. The one place it is +allowed is the presenter layer — `_output.py` and `tmuxp_echo` themselves. + +**Avoid:** f-strings/`.format()` in log calls; unguarded logging in hot +loops; catch-log-reraise without adding context; `print()` for debugging; +logging a secret env var's value (log the key name only); non-scalar ad-hoc +objects in `extra`; custom `extra` fields referenced in a format string +without a safe default (a missing key raises `KeyError`). + +## CLI color semantics + +The `Colors` class (`src/tmuxp/_internal/colors.py`) chooses color by +**hierarchy level** and **semantic meaning**, not by data type — inspired by +`jq` (object keys vs. values), `ripgrep` (path/line/match), and `mise`/`just` +(semantic method names). + +| Level | Element type | Method | Color | +| ------ | ---------------------- | -------------- | -------------------- | +| L0 | Section headers | `heading()` | Bright cyan + bold | +| L1 | Primary content | `highlight()` | Magenta + bold | +| L2 | Supplementary info | `info()` | Cyan | +| L3 | Metadata/labels | `muted()` | Blue | + +Status colors override hierarchy when they apply: `success()` green, +`warning()` yellow, `error()` red. + +**Never use the same color for adjacent hierarchy levels** — if headers and +items are both blue, they blend together. **Avoid the ANSI dim attribute** +(`\x1b[2m`, standard or bright) — too dark to read on a black terminal +background. **Do not rely on bold alone** for a distinction some +terminal/font combinations render identically to normal weight — pair it +with a color difference. diff --git a/docs/CLAUDE.md b/src/tmuxp/CLAUDE.md similarity index 100% rename from docs/CLAUDE.md rename to src/tmuxp/CLAUDE.md