Contributing
Most conventions here are enforced by a test or a CI job; each section names the check that fails when a rule is broken. How to run the tests, and what the test-only environment variables do, is in docs/testing.md.
Before you push
Section titled “Before you push”These are the commands the test job in
.github/workflows/ci.yml runs (the last three on
Linux only):
go build ./...go vet ./...go test -race ./...go test -coverprofile=cover.out -coverpkg=./... ./...go run ./internal/tools/covercheck cover.outgo run ./internal/tools/doccheckCI also checks that the generated files below are current, and runs
golangci-lint run ./... (v2.1.6) and gosec ./... (v2.29.0). Both are dev
tools that CI installs with go install; they are not module dependencies.
No dependencies
Section titled “No dependencies”Every package, examples and tools included, imports only the standard library
and this module, and go.mod has no require directive.
TestLibraryImportsStdlibOnly in internal/archtest fails otherwise. CI
actions (such as vmactions/freebsd-vm) and dev tools installed in CI are
not module dependencies.
Package rules
Section titled “Package rules”- Import direction. Packages are layered in tiers, and an import may only
point down.
TestImportDirectionininternal/archtestis the authoritative check;.golangci.ymlmirrors it for golangci-lint. Every package underinternal/needs a tier (TestEveryInternalPackageHasATier). The tiers are described in docs/architecture/overview.md. - Test support stays in tests. A non-test file must not import a
test-support package (
TestTestHelpersStayOutOfProductionCode). - No exported mutable package variables, except
Err*sentinels. Export a function that returns a copy instead (TestNoExportedMutableVars). - File size. A hand-written non-test source file over 800 lines fails
TestFileSizeunless it is listed inoversizeininternal/archtest/layers_test.gowith a reason. Generated files are exempt. - No process-wide width toggle in library code. Only an application may call
ansi.SetClusterWidth; pass anansi.Measurerinstead (TestNoGlobalWidthToggle).
Widget packages
Section titled “Widget packages”A component is one package with a value-typed Model configured through
exported fields.
- No functional-option types in component packages: options belong to
tui.ProgramOption(TestComponentsHaveNoFunctionalOptions). - Every stateful widget’s
ModelhasLinearizeandLayoutNodemethods (TestEveryStatefulWidgetHasLinearizeAndLayoutNode). A package that legitimately lacks one goes ina11yAllowlistininternal/archtest/a11y_test.gowith a real reason; the list is empty today. - A widget that handles keys for navigation also handles the mouse
(
TestKeyNavigationPackagesHandleMouse), unless it is inmouseAllowlistwith a reason. Updatemust not mutate a value copy of the Model. Add the new Model toTestCopyIsolationSweepininternal/isolation/sweep_test.go; the list is maintained by hand.
Documentation
Section titled “Documentation”go run ./internal/tools/doccheck (CI, Linux) fails when:
- an exported identifier in a public package has no doc comment;
- a package, internal ones included, has no package comment;
- the
Experimental:list in doc.go and the packages whose comment saysStability: experimental.disagree. Mark a new experimental package in both places; - a Markdown file or workflow names a dot-slash relative path or a
github.com/ows4444/tui/...import path that does not exist.CHANGELOG.mdis skipped.
go test ./internal/tools/doccheck also checks specific claims in
README.md against the code (colour detection variables, grapheme
clusters, unsupported platforms, the LayoutNode and Linearize claims, and that
unverified terminals and screen readers are marked so).
Every public package needs at least one runnable Example in a _test.go
file (TestEveryPackageHasExample in the root package, and
TestEveryPublicPackageHasExample in internal/archtest). Code shown in
Markdown should be copied from an Example so go test compiles it.
Website
Section titled “Website”The site at https://tui.nizaami.com is an Astro Starlight
project in site/. Its npm packages are site tooling, not module dependencies.
It has no pages of its own except the landing page
(site/src/content/docs/index.mdx, with its components in site/src/components/;
Hero.astro and Footer.astro replace Starlight’s own, and the theme is
site/src/styles/custom.css): site/scripts/sync-docs.mjs generates the
rest from README.md, CONTRIBUTING.md, CHANGELOG.md and docs/, and the
Examples page from each example’s 80×24 size-matrix screen. For colour the
sync runs go test -run 'TestSizeMatrix/^80x24$' ./examples/... with
TUI_SCREENS_DIR set, which makes testutil.SizeMatrix write each frame
unstripped; a frame is used only if, stripped, it equals
testdata/size-80x24.golden, and without Go the screens show without colour.
Their categories are EXAMPLE_CATEGORIES in the sync script. Edit those
sources, not the generated files, which are gitignored. A page added to docs/
needs an entry in pages in the sync script and in the sidebar in
site/astro.config.mjs. The sync fails on a relative link that points nowhere.
For search engines and AI assistants the sync also gives each page a
descriptive <title> (seoTitle in pages), a meta description (the
description in pages, or else the first prose sentences), JSON-LD, and a
plain Markdown copy at /<page>.md, indexed by /llms.txt and joined in
/llms-full.txt. Site-wide JSON-LD and the social image tags are in
site/astro.config.mjs; the landing page’s FAQ JSON-LD in
site/src/content/docs/index.mdx must match the FAQ text on that page.
site/public/og.png is rendered from site/scripts/og-image.html, whose header
comment has the command.
$ cd site$ npm ci$ npm run dev # http://localhost:4321, regenerates pages on start$ npm run build # static site in site/dist/Coverage
Section titled “Coverage”Each library package needs at least 90% statement coverage, counted per
package with coverage credited across packages (-coverpkg=./...).
internal/tools/covercheck skips examples/ and internal/tools/, and any
internal/ package whose non-test source has a
//covercheck:helper <reason> line; the reason is required.
Generated files
Section titled “Generated files”Don’t edit these by hand. Regenerate, then commit the result.
| File | Regenerate | CI check |
|---|---|---|
api.txt |
go run ./internal/tools/apilist -w |
apilist -check in the api job |
.golangci.yml |
ARCHTEST_UPDATE=1 go test ./internal/archtest -run TestGolangciMirror |
TestGolangciMirror in go test ./... |
ansi/runewidth_tables.go |
go run ./internal/tools/genwidth -o ansi/runewidth_tables.go |
widthtables job |
ansi/graphemebreak_tables.go |
go run ./internal/tools/gengrapheme -o ansi/graphemebreak_tables.go |
widthtables job |
internal/bidi/tables.go |
go run ./internal/tools/genbidi -o internal/bidi/tables.go |
widthtables job |
testdata/*.golden |
TUITEST_UPDATE=1 or -update, depending on the helper; see docs/testing.md |
the package’s tests |
The three Unicode generators fetch pinned Unicode 17.0.0 files from
unicode.org and verify each against a SHA-256 pin, so they need network access;
-dir reads local copies instead. To move to a newer Unicode version, change
unicodeVersion in the generator and run it with -print-hashes to get the
new pins.
bench/baseline.txt is the allocation baseline for the bench workflow.
No script in the repository regenerates it.
api.txt and golden files are checked out with LF line endings on every
platform (.gitattributes).
Changelog
Section titled “Changelog”Add user-visible changes to CHANGELOG.md under
## [Unreleased]. Removing or changing an exported identifier needs a
- BREAKING: bullet in that section naming the identifier as a whole word
(Style.Render or Render), and saying what to use instead. Once a tag
exists, the api CI job runs
go run ./internal/tools/apilist -since-tag -changelog CHANGELOG.md, which
fails on a removed or changed line of api.txt that no such bullet names.
With no tag yet it compares nothing.
Benchmarks
Section titled “Benchmarks”The bench workflow (.github/workflows/bench.yml)
runs on pull requests that touch a .go file, bench/ or the workflow
itself, every Monday at 06:00 UTC, and on demand. It fails
when allocs/op or B/op grow more than 10% over bench/baseline.txt
(internal/tools/benchgate); ns/op is not gated. To compare locally:
go test -run '^$' -bench . -benchmem -count=6 . ./ansi ./layout ./virtuallist ./logview ./viewport ./textarea ./markdown ./datatable ./streamtext > bench/new.txtgo run ./internal/tools/benchgate -base bench/baseline.txt -new bench/new.txtbench/new.txt is git-ignored.