Skip to content
Merged
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
52 changes: 51 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ jobs:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: '1.22'
go-version-file: go.mod
cache: true

- name: gofmt
Expand All @@ -36,3 +36,53 @@ jobs:
# The plugin entry point is shell; catch syntax errors before users do.
- name: shellcheck kaku-tab.tmux
run: bash -n kaku-tab.tmux

lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version-file: go.mod
cache: true

- uses: golangci/golangci-lint-action@v9
with:
# Pinned: an unpinned linter turns an unrelated upstream release into
# a red build on a PR that changed nothing.
version: v2.12.2

coverage:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version-file: go.mod
cache: true

- name: test with coverage
run: go test -covermode=atomic -coverprofile=cover.out ./...

# Thresholds live in .testcoverage.yml, set just under where the tree
# stands so this is a ratchet rather than a wall.
- uses: vladopajic/go-test-coverage@v2
with:
config: ./.testcoverage.yml

# The release config is only exercised on a tag push, which is the worst
# possible time to discover it no longer parses.
goreleaser-check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-go@v5
with:
go-version-file: go.mod
cache: true
- uses: goreleaser/goreleaser-action@v6
with:
version: '~> v2'
args: check
33 changes: 17 additions & 16 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,26 +8,27 @@ permissions:
contents: write

jobs:
build:
goreleaser:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: '1.22'
# goreleaser builds the changelog from git history, which a shallow
# clone does not have.
fetch-depth: 0

- name: Build binaries
run: |
mkdir -p dist
for target in darwin/amd64 darwin/arm64 linux/amd64 linux/arm64; do
os=${target%/*}; arch=${target#*/}
GOOS=$os GOARCH=$arch go build \
-ldflags "-s -w -X main.version=${GITHUB_REF_NAME}" \
-o "dist/kaku-tab-$os-$arch" ./cmd/kaku-tab
done
cd dist && sha256sum * > SHA256SUMS
- uses: actions/setup-go@v5
with:
go-version-file: go.mod
cache: true

- uses: softprops/action-gh-release@v2
- uses: goreleaser/goreleaser-action@v6
with:
files: dist/*
generate_release_notes: true
version: '~> v2'
args: release --clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# Optional. Without it the release still publishes and the Homebrew
# cask is skipped — see the skip_upload guard in .goreleaser.yaml.
# Needs a PAT with write access to dsaad68/homebrew-tap.
HOMEBREW_TAP_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }}
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
bin/
dist/
cover.out

# Local agent/editor state
.claude/settings.local.json
54 changes: 54 additions & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
version: "2"

linters:
default: none
enable:
- errcheck
- govet
- ineffassign
- staticcheck
- unused
- bodyclose
- copyloopvar
- misspell
- nilerr
- revive
- unconvert

settings:
errcheck:
# A deliberate `_ =` is how this codebase marks "best effort, and the
# caller cannot do anything useful with the failure" — reaping a
# satellite, focusing a pane. Flagging those would just add noise.
check-blank: false
revive:
# Deliberately without the `exported` rule. This codebase comments why
# something is the way it is, not what it is; that rule would demand a
# line of restated signature above every thin tmux wrapper and every
# bubbletea interface method, and a gate full of noise is a gate people
# learn to skip.
rules:
- name: indent-error-flow
- name: var-declaration
- name: errorf
- name: context-as-argument
- name: increment-decrement
- name: unreachable-code
staticcheck:
checks: ['all']

exclusions:
generated: lax
rules:
# Test helpers are allowed to be undocumented and to ignore errors.
- path: _test\.go
linters: [errcheck, revive]

formatters:
enable:
- gofmt

issues:
# A lint gate that only reports the first few problems is not a gate.
max-issues-per-linter: 0
max-same-issues: 0
85 changes: 85 additions & 0 deletions .goreleaser.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# yaml-language-server: $schema=https://goreleaser.com/static/schema.json
version: 2

project_name: kaku-tab

before:
hooks:
- go mod tidy

builds:
- id: kaku-tab
main: ./cmd/kaku-tab
binary: kaku-tab
env:
- CGO_ENABLED=0
goos: [linux, darwin]
goarch: [amd64, arm64]
# Matches the Makefile: main.version is what `kaku-tab version` prints.
ldflags:
- -s -w -X main.version={{ .Version }}
mod_timestamp: '{{ .CommitTimestamp }}'

archives:
# The binary keeps its plain name inside the archive. kaku-tab.tmux looks for
# `kaku-tab` on PATH when there is no local build, so an archive that unpacks
# to kaku-tab_darwin_arm64 would install something the plugin cannot find.
- id: default
formats: [tar.gz]
name_template: >-
{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}
files:
- README.md
- LICENSE
- kaku-tab.tmux
- docs/*

checksum:
name_template: checksums.txt

changelog:
use: github
sort: asc
filters:
exclude:
- '^docs:'
- '^test:'
- '^chore:'
- Merge pull request

# A cask, not a formula: goreleaser deprecated `brews`, and a cask also strips
# the macOS quarantine attribute from the downloaded binary. `brew install
# --cask` is macOS-only, which is where Kaku runs; Linux users have `go install`
# and the tarballs above.
homebrew_casks:
- name: kaku-tab
binaries: [kaku-tab]
repository:
owner: dsaad68
name: homebrew-tap
token: '{{ .Env.HOMEBREW_TAP_TOKEN }}'
# Publishing needs the tap repo and a token with write access to it. Until
# both exist the release still succeeds and simply skips the cask, rather
# than failing after the archives are already uploaded.
skip_upload: '{{ if index .Env "HOMEBREW_TAP_TOKEN" }}false{{ else }}true{{ end }}'
homepage: https://github.com/dsaad68/kaku-tab
description: A tmux plugin that maps one tmux window to one terminal tab
caveats: |
This installs the binary only. tmux still needs the plugin entry point,
from TPM:

set -g @plugin 'dsaad68/kaku-tab'

or from a checkout:

run-shell '~/.tmux/plugins/kaku-tab/kaku-tab.tmux'

Either way it finds kaku-tab on PATH, so no build step runs.

release:
draft: false
prerelease: auto
footer: |
---
Installing: `brew install dsaad68/tap/kaku-tab`, or unpack the archive for
your platform and put `kaku-tab` on your PATH.
34 changes: 34 additions & 0 deletions .testcoverage.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Thresholds for github.com/vladopajic/go-test-coverage.
#
# These are a ratchet set just under where the tree stands today, not an
# aspiration. The point is that a change cannot quietly delete coverage from
# internal/resolve — the package the invariants in CLAUDE.md live in — on its
# way to fixing something else.

profile: cover.out

exclude:
paths:
# Thin wrappers around an external process: the tmux CLI, the Kaku mux CLI,
# and argument parsing in main. A unit test of these can only assert the
# arguments back at itself, so they are excluded rather than pinned at 0
# and dragging the total into meaninglessness. They are covered instead by
# running the real thing — see "Verifying changes" in CLAUDE.md.
- ^cmd/kaku-tab/
- ^internal/tmux/
- ^internal/kaku/

threshold:
file: 0
package: 0
total: 40

override:
# The join. Everything else is presentation or plumbing around the table it
# produces, so this is the number that matters most.
- path: ^internal/resolve$
threshold: 85
- path: ^internal/ui$
threshold: 40
- path: ^internal/mru$
threshold: 80
7 changes: 6 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,16 @@ Go binary + a thin `kaku-tab.tmux` plugin entry point.
```sh
make build # bin/kaku-tab
make test # go vet + go test
make lint # gofmt + vet
make lint # golangci-lint, same .golangci.yml as CI
make cover # tests + coverage thresholds from .testcoverage.yml
```

Tests use recorded fixtures — no tmux server or terminal needed.

CI runs test / lint / coverage / goreleaser-check. The coverage thresholds are
a ratchet set just under the current numbers: if a change drops
`internal/resolve`, that is the signal, not a nuisance.

## Where to start

`internal/resolve` is the core: it joins the terminal's pane list to tmux's
Expand Down
30 changes: 25 additions & 5 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,13 @@ PKG := ./cmd/kaku-tab
VERSION ?= $(shell git describe --tags --always --dirty 2>/dev/null || echo dev)
LDFLAGS := -s -w -X main.version=$(VERSION)

.PHONY: all build install test lint fmt clean
# Pinned to match .github/workflows/ci.yml. A local lint that disagrees with CI
# is worse than no local lint.
GOLANGCI := github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.12.2
COVCHECK := github.com/vladopajic/go-test-coverage/v2@v2.19.0
GORELEASER := github.com/goreleaser/goreleaser/v2@latest

.PHONY: all build install test lint fmt cover release-snapshot clean

all: build

Expand All @@ -21,9 +27,23 @@ test:
fmt:
gofmt -w cmd internal

lint: fmt
go vet ./...
@test -z "$$(gofmt -l cmd internal)" || (echo "unformatted files"; exit 1)
## lint: the full linter set from .golangci.yml, same as CI. Uses an installed
## golangci-lint when there is one, since `go run` rebuilds it from scratch.
lint:
@if command -v golangci-lint >/dev/null 2>&1; then \
golangci-lint run; \
else \
go run $(GOLANGCI) run; \
fi

## cover: run tests and check the thresholds in .testcoverage.yml
cover:
go test -covermode=atomic -coverprofile=cover.out ./...
@go run $(COVCHECK) --config=.testcoverage.yml

## release-snapshot: build the release artifacts locally, publishing nothing
release-snapshot:
go run $(GORELEASER) release --snapshot --clean --skip=publish

clean:
rm -rf bin
rm -rf bin dist cover.out
Loading
Loading