From a4a80bc3259afd969730db5f2378047d625453cf Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 22 Aug 2026 10:00:03 -0500 Subject: [PATCH] Docs(refactor[agents]): Route instead of restate why: AGENTS.md is read on every task, so a policy that matters to one class of change costs context on all the others. Splitting how we work from how we write lets each be loaded when it applies, and keeps both discoverable to humans rather than only to agents. what: - Replace the AGENTS.md body with a router: the project map, universal change discipline, then one pointer per class of change - Add .github/WRITING.md for prose policy: voice, README, the CLI (exit statuses, stdout/stderr, machine output, destructive-op invariants), workspace-file conventions, doctests, MyST roles, the changelog, docstrings, source comments, commits, and slop prevention - Add .github/CONTRIBUTING.md for workflow policy: setup, the gates, imports/typing, tests and fixtures, docs build, releasing, PRs, decorum, and security, using the repo's real commands throughout - Add src/tmuxp/AGENTS.md (with a CLAUDE.md symlink) for logging standards and CLI color semantics, scoped to the package - Turn docs/project/contributing.md and code-style.md into pointer pages at the canonical .github files (a MyST {include} across the docs/ boundary emits dead anchors on this Sphinx setup), keeping every existing (label)= anchor - Rewrite README.md: console/$ code blocks, one command per block, and correct the developmental-release install examples to the --prerelease allow form CHANGES already documents - Delete docs/AGENTS.md, docs/CLAUDE.md, and the lowercase .github/contributing.md, now superseded - Exclude the nested AGENTS.md and its CLAUDE.md symlink from the wheel; hatchling packages every file under the package directory, so contributor guidance would otherwise ship inside every install --- .github/CONTRIBUTING.md | 335 +++++++++++++++ .github/WRITING.md | 726 +++++++++++++++++++++++++++++++ .github/contributing.md | 27 -- AGENTS.md | 779 +++------------------------------- README.md | 122 +++--- docs/AGENTS.md | 119 ------ docs/project/code-style.md | 40 +- docs/project/contributing.md | 294 +------------ pyproject.toml | 9 + src/tmuxp/AGENTS.md | 119 ++++++ {docs => src/tmuxp}/CLAUDE.md | 0 11 files changed, 1331 insertions(+), 1239 deletions(-) create mode 100644 .github/CONTRIBUTING.md create mode 100644 .github/WRITING.md delete mode 100644 .github/contributing.md delete mode 100644 docs/AGENTS.md create mode 100644 src/tmuxp/AGENTS.md rename {docs => src/tmuxp}/CLAUDE.md (100%) 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