Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,18 @@ jobs:

- name: Build documentation
if: env.PUBLISH == 'true'
env:
# This tree is published to the bucket root, not nested under
# libtmux.org's shell, so the shell's chrome and site-wide search
# are not reachable from here. Keep Furo's own search page and load
# no chrome. See the note in docs/conf.py.
#
# Delete this when the sync below moves under libtmux.org, to a
# per-port prefix. Left behind, the build publishes nested and
# still unskinned — no chrome, and Furo's search where the site's
# own belongs — which reads as the shell having failed rather than
# as a stale flag. tests/test_docs_conf.py covers both shapes.
LIBTMUX_DOCS_STANDALONE: '1'
run: |
cd docs && just html

Expand Down
14 changes: 14 additions & 0 deletions CHANGES
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,13 @@ _Notes on the upcoming release will go here._

### Documentation

#### libtmux.org chrome and site-wide search (#755)

Documentation pages carry libtmux.org's header, footer, and version switcher,
and take their colors from the site's shared design tokens. Sphinx's own
search page redirects to the site-wide search, which covers every libtmux
port. Pages render as stock Furo when the shared stylesheet is unreachable.

#### Cleaner `from_env` examples (#719)

The rendered examples for {meth}`Pane.from_env() <libtmux.Pane.from_env>` and
Expand All @@ -59,6 +66,13 @@ it.

### Development

#### Docs toolchain on gp-sphinx 0.1.0a38 (#755)

`gp-sphinx` and its sibling extensions move to 0.1.0a38. `sphinx-gp-llms`
resolves from a pinned upstream commit until a release carries its fix:
`genindex`, `py-modindex`, and `search` no longer link a `.md` twin that was
never written.

#### CI actions updated to current majors

Workflow actions moved to their current major releases: `actions/checkout` v7,
Expand Down
131 changes: 131 additions & 0 deletions docs/_static/libtmux-org.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
/*
* libtmux.org design-token adapter for Furo.
*
* tokens.css defines ~25 semantic --lt-* names and restyles nothing by
* itself. This is the other half: the table mapping each onto Furo's own
* --color-* contract. Without it a page carries those variables unused in
* the cascade and still paints Furo's stock blue.
*
* Import, never copy. The values stay live at the CDN, so a chrome-color
* fix reaches an already-published build without a rebuild here. Only the
* mapping is fixed at build time, and it moves only when Furo's own
* variable contract does.
*
* Every var(--lt-*) carries Furo's own stock value as its fallback, and
* that is load-bearing rather than decoration. tokens.css is fetched
* cross-origin, so it can 404 — a local preview, a PR preview, any deploy
* before DNS resolves. Per the custom-properties spec, a var() naming an
* undefined property with no fallback resolves to the guaranteed-invalid
* value, so --color-background-primary would compute to `unset` rather
* than white: transparent backgrounds and UA-default text, worse than the
* unskinned page this file exists to fix. The fallbacks are Furo's own
* colors, not libtmux's, so a failed fetch degrades to plain Furo instead
* of vendoring the palette. They track gp-furo-tokens, the package Furo's
* light and dark defaults are generated from.
*
* Scope and order: Furo reads its --color-* tokens at `body` and
* `body[data-theme="dark"]`. This file matches that scope and specificity
* exactly and is listed last in html_css_files (docs/conf.py), so it loads
* after furo-tw.css and wins at equal specificity.
*
* Background on the theme chain and the rejected vendoring alternative is
* in the libtmux.org docs-site repository, under notes/research/.
*/
@import url('https://libtmux.org/_shell/tokens.css');

body {
--color-background-primary: var(--lt-color-bg, white);
--color-background-secondary: var(--lt-color-bg-secondary, #f8f9fb);
--color-background-hover: var(--lt-color-bg-hover, #efeff4);

--color-foreground-primary: var(--lt-color-fg, black);
--color-foreground-secondary: var(--lt-color-fg-secondary, #5a5c63);
--color-foreground-muted: var(--lt-color-fg-muted, #6b6f76);

--color-background-border: var(--lt-color-border, #eeebee);

--color-brand-primary: var(--lt-color-accent, #0a4bff);
--color-brand-content: var(--lt-color-link, #2757dd);
--color-brand-visited: var(--lt-color-link-visited, #872ee0);

--color-inline-code-background: var(--lt-color-code-bg, #f8f9fb);
--color-highlighted-background: var(--lt-color-highlighted-bg, #ddeeff);

--color-api-added: var(--lt-color-added, #21632c);
--color-api-removed: var(--lt-color-removed, #b30000);
--color-api-changed: var(--lt-color-changed, #046172);
--color-api-deprecated: var(--lt-color-deprecated, #605706);

/* Furo's own stock admonition colors diverge per kind even though this
adapter collapses them onto four shared --lt-* accents; the fallback
restores each kind's own stock value, not the collapsed one, since a
degrade should look like unmodified Furo, not a half-applied palette. */
--color-admonition-title--danger: var(--lt-color-danger, #ff5252);
--color-admonition-title--error: var(--lt-color-danger, #ff5252);
--color-admonition-title--attention: var(--lt-color-danger, #ff5252);
--color-admonition-title--warning: var(--lt-color-warning, #ff9100);
--color-admonition-title--caution: var(--lt-color-warning, #ff9100);
--color-admonition-title--note: var(--lt-color-info, #00b0ff);
--color-admonition-title--seealso: var(--lt-color-info, #448aff);
--color-admonition-title--hint: var(--lt-color-success, #00c852);
--color-admonition-title--tip: var(--lt-color-success, #00c852);

--font-stack:
var(--lt-font-sans, -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial,
sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji');
--font-stack--monospace:
var(--lt-font-mono, 'SFMono-Regular', Menlo, Consolas, Monaco, 'Liberation Mono',
'Lucida Console', monospace);
}

@media not print {
body[data-theme='dark'] {
--color-background-primary: var(--lt-color-bg, #131416);
--color-background-secondary: var(--lt-color-bg-secondary, #1a1c1e);
--color-background-hover: var(--lt-color-bg-hover, #1e2124);

--color-foreground-primary: var(--lt-color-fg, #cfd0d0);
--color-foreground-secondary: var(--lt-color-fg-secondary, #9ca0a5);
--color-foreground-muted: var(--lt-color-fg-muted, #81868d);

--color-background-border: var(--lt-color-border, #303335);

--color-brand-primary: var(--lt-color-accent, #3d94ff);
--color-brand-content: var(--lt-color-link, #5ca5ff);
--color-brand-visited: var(--lt-color-link-visited, #b27aeb);

--color-inline-code-background: var(--lt-color-code-bg, #1a1c1e);
--color-highlighted-background: var(--lt-color-highlighted-bg, #083563);

--color-api-added: var(--lt-color-added, #3db854);
--color-api-removed: var(--lt-color-removed, #ff7575);
--color-api-changed: var(--lt-color-changed, #09b0ce);
--color-api-deprecated: var(--lt-color-deprecated, #b1a10b);
}

@media (prefers-color-scheme: dark) {
body:not([data-theme='light']) {
--color-background-primary: var(--lt-color-bg, #131416);
--color-background-secondary: var(--lt-color-bg-secondary, #1a1c1e);
--color-background-hover: var(--lt-color-bg-hover, #1e2124);

--color-foreground-primary: var(--lt-color-fg, #cfd0d0);
--color-foreground-secondary: var(--lt-color-fg-secondary, #9ca0a5);
--color-foreground-muted: var(--lt-color-fg-muted, #81868d);

--color-background-border: var(--lt-color-border, #303335);

--color-brand-primary: var(--lt-color-accent, #3d94ff);
--color-brand-content: var(--lt-color-link, #5ca5ff);
--color-brand-visited: var(--lt-color-link-visited, #b27aeb);

--color-inline-code-background: var(--lt-color-code-bg, #1a1c1e);
--color-highlighted-background: var(--lt-color-highlighted-bg, #083563);

--color-api-added: var(--lt-color-added, #3db854);
--color-api-removed: var(--lt-color-removed, #ff7575);
--color-api-changed: var(--lt-color-changed, #09b0ce);
--color-api-deprecated: var(--lt-color-deprecated, #b1a10b);
}
}
}
41 changes: 41 additions & 0 deletions docs/_templates_shell/search.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
{#
Replaces Sphinx's built-in search page (always rendered from a template
named "search.html") with a redirect to the shell's Pagefind search, which
covers every libtmux port. Furo's own search UI needs a client-side index
this build does not emit at that path.

Loaded only when docs/conf.py turns the shell integration on; the
standalone deploy keeps Sphinx's own search page. See conf.py.

No {% extends %} — this replaces the page outright rather than filling one
of layout.html's blocks. The wording matches the rediraffe stubs in this
tree (redirects.txt) so the site audit's redirect check, which matches "You
should have been redirected", skips it instead of flagging an empty page.

The redirect is guarded rather than a bare <meta refresh>: this build is
served nested under the shell (/py/<version>/api/search/), but the same
tree can be served at a root, where the page's own path *is* /search/ and
an unguarded refresh would reload itself forever.
#}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>{{ _('Search') }}</title>
<meta name="robots" content="noindex" />
<script>
(function () {
var here = window.location.pathname.replace(/\/+$/, '/');
if (here !== '/search/') {
window.location.replace('/search/');
}
})();
</script>
</head>
<body>
<p>You should have been redirected to the site-wide search.</p>
<p>
<a href="https://libtmux.org/search/">If not, click here to continue.</a>
</p>
</body>
</html>
35 changes: 34 additions & 1 deletion docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from __future__ import annotations

import os
import pathlib
import sys

Expand All @@ -21,6 +22,21 @@
with (project_src / "libtmux" / "__about__.py").open() as fp:
exec(fp.read(), about)

# The shell integration — libtmux.org's chrome and its site-wide search — is
# correct only where this build is served *under* the assembled site, at
# /py/<version>/api/. Two consumers do that: libtmux.org's assembler and a
# local preview of it. A third does not: .github/workflows/docs.yml publishes
# this same tree to the bucket behind libtmux.git-pull.com, at the root, where
# /_shell/shell.js is nothing and /search/ is this build's own search page.
#
# So the integration is on by default, for the two that nest it, and that
# workflow sets LIBTMUX_DOCS_STANDALONE=1 to opt out. Standalone keeps Furo's
# own search page and loads no chrome, which is what that host serves today.
#
# The design-token adapter stays on in both: it degrades to stock Furo by
# itself when the shared stylesheet is unreachable (see its own header).
shell_integration = os.environ.get("LIBTMUX_DOCS_STANDALONE", "") != "1"

conf = merge_sphinx_config(
project=about["__title__"],
version=about["__version__"],
Expand Down Expand Up @@ -48,8 +64,25 @@
" throughout 2026."
),
},
# _templates_shell holds only the search override, kept out of _templates
# so dropping it from this list is all it takes to fall back to Furo's own
# search page. Sphinx searches the list in order, and the value gp_sphinx
# would otherwise supply on its own is spelled out here so a change to its
# default cannot silently drop either directory.
templates_path=(
["_templates_shell", "_templates"] if shell_integration else ["_templates"]
),
html_favicon="_static/favicon.ico",
html_css_files=["css/custom.css"],
# libtmux-org.css maps the site's shared --lt-* design tokens onto Furo's
# own --color-* contract (see the file's own header) and must load after
# css/custom.css so its overrides win.
html_css_files=["css/custom.css", "libtmux-org.css"],
# shell.js injects the header, footer and version switcher. Referenced,
# never copied, so a chrome fix reaches an already-published build without
# a rebuild here.
html_js_files=(
[("/_shell/shell.js", {"defer": "defer"})] if shell_integration else []
),
html_extra_path=["manifest.json"],
rediraffe_redirects="redirects.txt",
# AGENTS.md (+ its CLAUDE.md symlink) is agent guidance, not a site
Expand Down
37 changes: 31 additions & 6 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,12 @@ Changes = "https://github.com/tmux-python/libtmux/blob/master/CHANGES"
[dependency-groups]
dev = [
# Docs (via gp-sphinx)
"gp-sphinx==0.1.0a37",
"sphinx-autodoc-api-style==0.1.0a37",
"sphinx-autodoc-pytest-fixtures==0.1.0a37",
"gp-sphinx==0.1.0a38",
# Declared directly so the source override below can redirect it; an
# override does not reach a dependency arriving transitively via gp-sphinx.
"sphinx-gp-llms",
"sphinx-autodoc-api-style==0.1.0a38",
"sphinx-autodoc-pytest-fixtures==0.1.0a38",
"sphinx-autobuild",
"types-docutils",
# Testing
Expand All @@ -75,9 +78,10 @@ dev = [
]

docs = [
"gp-sphinx==0.1.0a37",
"sphinx-autodoc-api-style==0.1.0a37",
"sphinx-autodoc-pytest-fixtures==0.1.0a37",
"gp-sphinx==0.1.0a38",
"sphinx-gp-llms",
"sphinx-autodoc-api-style==0.1.0a38",
"sphinx-autodoc-pytest-fixtures==0.1.0a38",
"sphinx-autobuild",
]
testing = [
Expand Down Expand Up @@ -288,3 +292,24 @@ testpaths = [
markers = [
"integration: sphinx integration tests (require full sphinx build)",
]

[tool.uv.sources]
# sphinx-gp-llms from upstream `main`, for one fix libtmux.org needs.
#
# The extension links a `.md` twin beside every page, including `genindex`,
# `py-modindex` and `search`, which have no twin — dead links in the assembled
# site. The link is rendered from a theme template rather than resolved as a
# reference, so a `-W` build stays green and Sphinx never reports it.
#
# `git-pull/gp-sphinx@3b0bdc15` carries the fix and reports version 0.1.0a38,
# the same version pinned above, so no dependency override is needed. The
# commit is public and cloned over HTTPS: no credentials, and CI resolves it
# anonymously.
#
# Delete this block once a release carries the fix; the `==0.1.0a38` pins
# above then become a plain version bump and nothing else changes.
#
# Only this sub-package is redirected, not the `gp-sphinx` meta-package:
# pinning the whole thing makes uv resolve its siblings from git too, and
# those are already on PyPI at the version pinned above.
sphinx-gp-llms = { git = "https://github.com/git-pull/gp-sphinx.git", rev = "3b0bdc15e30121bf346ccd046e66f5a049fd60a2", subdirectory = "packages/sphinx-gp-llms" }
Loading
Loading