From 7022e498ff4d55d6c8c41526fc1307acf0000209 Mon Sep 17 00:00:00 2001 From: Adrian Hesketh Date: Wed, 6 May 2026 16:53:47 +0100 Subject: [PATCH 1/3] fix: enabling paging for long lists, fixes #168 --- cmd/xc/interactive.go | 6 +- examples/long/README.md | 245 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 249 insertions(+), 2 deletions(-) create mode 100644 examples/long/README.md diff --git a/cmd/xc/interactive.go b/cmd/xc/interactive.go index 2993f6e..e5b1451 100644 --- a/cmd/xc/interactive.go +++ b/cmd/xc/interactive.go @@ -28,7 +28,8 @@ const ( paginationPadding = 4 helpPadding = 4 listItemWidth = 20 - listItemHeight = 6 + listItemHeight = 14 + listHeightMargin = 2 ) type taskItem struct { @@ -76,6 +77,7 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case tea.WindowSizeMsg: m.list.SetWidth(msg.Width) + m.list.SetHeight(msg.Height - listHeightMargin) return m, nil case tea.KeyMsg: @@ -111,7 +113,7 @@ func interactivePicker(ctx context.Context, tasks []models.Task, dir string) err for _, t := range tasks { items = append(items, taskItem{t}) } - l := list.New(items, itemDelegate{}, listItemWidth, listItemHeight+len(tasks)) + l := list.New(items, itemDelegate{}, listItemWidth, listItemHeight) l.Title = "xc: Choose a task" l.SetShowStatusBar(false) l.DisableQuitKeybindings() diff --git a/examples/long/README.md b/examples/long/README.md new file mode 100644 index 0000000..49e6ab9 --- /dev/null +++ b/examples/long/README.md @@ -0,0 +1,245 @@ +# Long list + +This is a really long list. + +## Tasks + +### Task1 + +```bash +echo Task1 +``` + +### Task2 + +```bash +echo Task2 +``` + +### Task3 + +```bash +echo Task3 +``` + +### Task4 + +```bash +echo Task4 +``` + +### Task5 + +```bash +echo Task5 +``` + +### Task6 + +```bash +echo Task6 +``` + +### Task7 + +```bash +echo Task7 +``` + +### Task8 + +```bash +echo Task8 +``` + +### Task9 + +```bash +echo Task9 +``` + +### Task10 + +```bash +echo Task10 +``` + +### Task11 + +```bash +echo Task11 +``` + +### Task12 + +```bash +echo Task12 +``` + +### Task13 + +```bash +echo Task13 +``` + +### Task14 + +```bash +echo Task14 +``` + +### Task15 + +```bash +echo Task15 +``` + +### Task16 + +```bash +echo Task16 +``` + +### Task17 + +```bash +echo Task17 +``` + +### Task18 + +```bash +echo Task18 +``` + +### Task19 + +```bash +echo Task19 +``` + +### Task20 + +```bash +echo Task20 +``` + +### Task21 + +```bash +echo Task21 +``` + +### Task22 + +```bash +echo Task22 +``` + +### Task23 + +```bash +echo Task23 +``` + +### Task24 + +```bash +echo Task24 +``` + +### Task25 + +```bash +echo Task25 +``` + +### Task26 + +```bash +echo Task26 +``` + +### Task27 + +```bash +echo Task27 +``` + +### Task28 + +```bash +echo Task28 +``` + +### Task29 + +```bash +echo Task29 +``` + +### Task30 + +```bash +echo Task30 +``` + +### Task31 + +```bash +echo Task31 +``` + +### Task32 + +```bash +echo Task32 +``` + +### Task33 + +```bash +echo Task33 +``` + +### Task34 + +```bash +echo Task34 +``` + +### Task35 + +```bash +echo Task35 +``` + +### Task36 + +```bash +echo Task36 +``` + +### Task37 + +```bash +echo Task37 +``` + +### Task38 + +```bash +echo Task38 +``` + +### Task39 + +```bash +echo Task39 +``` + +### Task40 + +```bash +echo Task40 +``` From c13f635ee799e62034aefa0f359469a01c6fe7a6 Mon Sep 17 00:00:00 2001 From: Adrian Hesketh Date: Thu, 7 May 2026 17:23:42 +0100 Subject: [PATCH 2/3] chore: bump Go version and remove linter warnings where we can use new features --- .github/workflows/test.yaml | 4 ++-- flake.lock | 32 ++++++++++++++++++++++++------- flake.nix | 11 ++++++----- go.mod | 2 +- parser/parsemd/parsemd_test.go | 5 ++--- parser/parseorg/parseorg_test.go | 5 ++--- run/run.go | 33 +++++++++++++++----------------- run/run_test.go | 2 -- 8 files changed, 53 insertions(+), 41 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index b55d4e4..b1ecba4 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -9,7 +9,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: - go-version: "1.22" + go-version: "1.25" - name: Run Unit tests run: | go test -race -covermode atomic -coverprofile=covprofile ./... @@ -26,6 +26,6 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: - go-version: 1.22 + go-version: "1.25" - name: golangci-lint uses: golangci/golangci-lint-action@v8 diff --git a/flake.lock b/flake.lock index 5b00219..c9e2964 100644 --- a/flake.lock +++ b/flake.lock @@ -1,12 +1,15 @@ { "nodes": { "flake-utils": { + "inputs": { + "systems": "systems" + }, "locked": { - "lastModified": 1667395993, - "narHash": "sha256-nuEHfE/LcWyuSWnS8t12N1wc105Qtau+/OdUAjtQ0rA=", + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", "owner": "numtide", "repo": "flake-utils", - "rev": "5aed5285a952e0b949eb3ba02c12fa4fcfef535f", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", "type": "github" }, "original": { @@ -17,16 +20,16 @@ }, "nixpkgs": { "locked": { - "lastModified": 1764521362, - "narHash": "sha256-M101xMtWdF1eSD0xhiR8nG8CXRlHmv6V+VoY65Smwf4=", + "lastModified": 1778003029, + "narHash": "sha256-q/nkKLDtHIyLjZpKhWk3cSK5IYsFqtMd6UtXF3ddjgA=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "871b9fd269ff6246794583ce4ee1031e1da71895", + "rev": "0c88e1f2bdb93d5999019e99cb0e61e1fe2af4c5", "type": "github" }, "original": { "owner": "NixOS", - "ref": "25.11", + "ref": "nixos-25.11", "repo": "nixpkgs", "type": "github" } @@ -36,6 +39,21 @@ "flake-utils": "flake-utils", "nixpkgs": "nixpkgs" } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } } }, "root": "root", diff --git a/flake.nix b/flake.nix index d6739f4..5a9ce0f 100644 --- a/flake.nix +++ b/flake.nix @@ -1,6 +1,6 @@ { inputs = { - nixpkgs.url = "github:NixOS/nixpkgs/25.11"; + nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.11"; flake-utils.url = "github:numtide/flake-utils"; }; @@ -15,14 +15,15 @@ { defaultPackage = xc; packages = { + default = xc; xc = xc; }; devShells = { default = pkgs.mkShell { - packages = [ xc ]; - }; - xc = pkgs.mkShell { - packages = [ xc ]; + packages = [ + pkgs.go_1_25 + pkgs.golangci-lint + ]; }; }; } diff --git a/go.mod b/go.mod index 3236905..c0b5a25 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/joerdav/xc -go 1.20 +go 1.25 require ( github.com/charmbracelet/bubbles v0.16.1 diff --git a/parser/parsemd/parsemd_test.go b/parser/parsemd/parsemd_test.go index 5906273..7992d20 100644 --- a/parser/parsemd/parsemd_test.go +++ b/parser/parsemd/parsemd_test.go @@ -433,7 +433,6 @@ func TestParseAttribute(t *testing.T) { }, } for _, tt := range tests { - tt := tt t.Run(tt.name, func(t *testing.T) { var p parser p.scanner = bufio.NewScanner(strings.NewReader(tt.in)) @@ -473,7 +472,7 @@ func BenchmarkParse10_000Tasks(b *testing.B) { buf.WriteString(` ## Tasks `) - for i := 0; i < 100; i++ { + for i := range 100 { buf.WriteString(` ### task-` + fmt.Sprint(i) + ` @@ -490,7 +489,7 @@ echo "Hello, world2!" ` + codeBlockStarter) } file := buf.String() - for i := 0; i < b.N; i++ { + for range b.N { p, err := NewParser(strings.NewReader(file), nil) if err != nil { b.Fatal(err) diff --git a/parser/parseorg/parseorg_test.go b/parser/parseorg/parseorg_test.go index bb943d7..0bd716c 100644 --- a/parser/parseorg/parseorg_test.go +++ b/parser/parseorg/parseorg_test.go @@ -380,7 +380,6 @@ func TestParseAttribute(t *testing.T) { }, } for _, tt := range tests { - tt := tt t.Run(tt.name, func(t *testing.T) { var p parser p.scanner = bufio.NewScanner(strings.NewReader(tt.in)) @@ -420,7 +419,7 @@ func BenchmarkParse10_000Tasks(b *testing.B) { buf.WriteString(` ** Tasks `) - for i := 0; i < 100; i++ { + for i := range 100 { buf.WriteString(` *** task-` + fmt.Sprint(i) + ` @@ -437,7 +436,7 @@ echo "Hello, world2!" ` + codeBlockEnd) } file := buf.String() - for i := 0; i < b.N; i++ { + for range b.N { p, err := NewParser(strings.NewReader(file), nil) if err != nil { b.Fatal(err) diff --git a/run/run.go b/run/run.go index 81ea34b..c987fa9 100644 --- a/run/run.go +++ b/run/run.go @@ -64,16 +64,18 @@ const scriptHeader = ` #!/bin/bash const traceHeader = " set -o xtrace\n" func taskUsage(task models.Task) string { - argUsage := fmt.Sprintf("xc %s", task.Name) + var sb strings.Builder + sb.WriteString("Task has required inputs:\n\t") + fmt.Fprint(&sb, "xc ", task.Name) for _, n := range task.Inputs { - argUsage += fmt.Sprintf(" <%s>", strings.ToLower(n)) + fmt.Fprintf(&sb, " <%s>", strings.ToLower(n)) } - envUsage := "" + sb.WriteString("\n\t") for _, n := range task.Inputs { - envUsage += fmt.Sprintf("%s=<%s> ", n, strings.ToLower(n)) + fmt.Fprintf(&sb, "%s=<%s> ", n, strings.ToLower(n)) } - envUsage += fmt.Sprintf("xc %s", task.Name) - return fmt.Sprintf("Task has required inputs:\n\t%s\n\t%s", argUsage, envUsage) + fmt.Fprintf(&sb, "xc %s", task.Name) + return sb.String() } func environmentContainsInput(env []string, input string) bool { @@ -178,17 +180,14 @@ func (r *Runner) runDepsAsync(ctx context.Context, padding int, _ bool, dependen var wg sync.WaitGroup errs := make([]error, len(dependencies)) for i, t := range dependencies { - wg.Add(1) - go func(index int, task string) { - defer wg.Done() - ta, err := shlex.Split(task) + wg.Go(func() { + ta, err := shlex.Split(t) if err != nil { - errs[index] = err + errs[i] = err return } - - errs[index] = r.runWithPadding(ctx, ta[0], ta[1:], padding, true) - }(i, t) + errs[i] = r.runWithPadding(ctx, ta[0], ta[1:], padding, true) + }) } wg.Wait() @@ -246,10 +245,8 @@ func (r *Runner) ValidateDependencies(task string, prevTasks []string) error { if !ok { return fmt.Errorf("task %s not found", t) } - for _, pt := range prevTasks { - if pt == st.Name { - return fmt.Errorf("task %s contains a circular dependency", t) - } + if slices.Contains(prevTasks, st.Name) { + return fmt.Errorf("task %s contains a circular dependency", t) } err := r.ValidateDependencies(st.Name, append([]string{st.Name}, prevTasks...)) if err != nil { diff --git a/run/run_test.go b/run/run_test.go index 702e698..5788d6b 100644 --- a/run/run_test.go +++ b/run/run_test.go @@ -177,7 +177,6 @@ func testCases() []testCase { func TestRunAsync(t *testing.T) { for _, tt := range testCases() { - tt := tt t.Run(tt.name, func(t *testing.T) { for i := range tt.tasks { tt.tasks[i].DepsBehaviour = models.DependencyBehaviourAsync @@ -204,7 +203,6 @@ func TestRunAsync(t *testing.T) { func TestRun(t *testing.T) { for _, tt := range testCases() { - tt := tt t.Run(tt.name, func(t *testing.T) { runner, err := NewRunner(tt.tasks, "") if (err != nil) != tt.expectedParseError { From e6644867a734674ed18b215e42c36eabcfc2cacf Mon Sep 17 00:00:00 2001 From: Adrian Hesketh Date: Thu, 7 May 2026 17:31:04 +0100 Subject: [PATCH 3/3] chore: remove goconst from test files, and fix gosec linter warning --- .golangci.yaml | 4 ++++ parser/parsemd/parsemd_test.go | 16 ++++++++-------- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/.golangci.yaml b/.golangci.yaml index f6d88ff..7fb323a 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -33,6 +33,10 @@ linters: - common-false-positives - legacy - std-error-handling + rules: + - linters: + - goconst + path: _test\.go$ paths: - third_party$ - builtin$ diff --git a/parser/parsemd/parsemd_test.go b/parser/parsemd/parsemd_test.go index 7992d20..035bcb0 100644 --- a/parser/parsemd/parsemd_test.go +++ b/parser/parsemd/parsemd_test.go @@ -88,8 +88,8 @@ echo "Hello, world2!" if len(result) != len(expected) { t.Fatalf("want %d tasks got %d", len(expected), len(result)) } - for i := range result { - assertTask(t, expected[i], result[i]) + for i, exp := range expected { + assertTask(t, exp, result[i]) } } @@ -123,8 +123,8 @@ func TestParseFileToEOF(t *testing.T) { if len(result) != len(expected) { t.Fatalf("want %d tasks got %d", len(expected), len(result)) } - for i := range result { - assertTask(t, expected[i], result[i]) + for i, exp := range expected { + assertTask(t, exp, result[i]) } } @@ -240,8 +240,8 @@ func TestCustomHeadingByFlag(t *testing.T) { if len(result) != len(expected) { t.Fatalf("want %d tasks got %d", len(expected), len(result)) } - for i := range result { - assertTask(t, expected[i], result[i]) + for i, exp := range expected { + assertTask(t, exp, result[i]) } } @@ -264,8 +264,8 @@ func TestCustomHeadingByNextLineMarker(t *testing.T) { if len(result) != len(expected) { t.Fatalf("want %d tasks got %d", len(expected), len(result)) } - for i := range result { - assertTask(t, expected[i], result[i]) + for i, exp := range expected { + assertTask(t, exp, result[i]) } }