From 8b30e80688b1ba1348395587907e2831550f60ea Mon Sep 17 00:00:00 2001 From: Virgil Date: Thu, 2 Apr 2026 05:23:01 +0000 Subject: [PATCH] feat(cli): add ASCII glyph fallbacks for tree and tracker Co-authored-by: Virgil --- go.sum | 12 ------------ pkg/cli/ansi_test.go | 4 +--- pkg/cli/check_test.go | 1 + pkg/cli/glyph_test.go | 2 ++ pkg/cli/output_test.go | 2 ++ pkg/cli/tracker.go | 23 ++++++++++++++++------- pkg/cli/tracker_test.go | 30 ++++++++++++++++++++++++++++++ pkg/cli/tree.go | 10 +++++++--- pkg/cli/tree_test.go | 22 ++++++++++++++++++++++ 9 files changed, 81 insertions(+), 25 deletions(-) diff --git a/go.sum b/go.sum index c4c9fe2..7085683 100644 --- a/go.sum +++ b/go.sum @@ -6,13 +6,10 @@ forge.lthn.ai/core/go-i18n v0.1.7 h1:aHkAoc3W8fw3RPNvw/UszQbjyFWXHszzbZgty3SwyAA forge.lthn.ai/core/go-i18n v0.1.7/go.mod h1:0VDjwtY99NSj2iqwrI09h5GUsJeM9s48MLkr+/Dn4G8= forge.lthn.ai/core/go-inference v0.1.7 h1:9Dy6v03jX5ZRH3n5iTzlYyGtucuBIgSe+S7GWvBzx9Q= forge.lthn.ai/core/go-inference v0.1.7/go.mod h1:jfWz+IJX55wAH98+ic6FEqqGB6/P31CHlg7VY7pxREw= -forge.lthn.ai/core/go-io v0.1.6/go.mod h1:3MSuQZuzhCi6aefECQ/LxhM8ooVLam1KgEvgeEjYZVc= forge.lthn.ai/core/go-log v0.0.4 h1:KTuCEPgFmuM8KJfnyQ8vPOU1Jg654W74h8IJvfQMfv0= forge.lthn.ai/core/go-log v0.0.4/go.mod h1:r14MXKOD3LF/sI8XUJQhRk/SZHBE7jAFVuCfgkXoZPw= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= -github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA= -github.com/bits-and-blooms/bitset v1.24.4/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw= github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4= github.com/charmbracelet/colorprofile v0.4.3 h1:QPa1IWkYI+AOB+fE+mg/5/4HRMZcaXex9t5KX76i20Q= @@ -23,12 +20,10 @@ github.com/charmbracelet/x/ansi v0.11.6 h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF github.com/charmbracelet/x/ansi v0.11.6/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ= github.com/charmbracelet/x/cellbuf v0.0.15 h1:ur3pZy0o6z/R7EylET877CBxaiE1Sp1GMxoFPAIztPI= github.com/charmbracelet/x/cellbuf v0.0.15/go.mod h1:J1YVbR7MUuEGIFPCaaZ96KDl5NoS0DAWkskup+mOY+Q= -github.com/charmbracelet/x/exp/golden v0.0.0-20240806155701-69247e0abc2a/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U= github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk= github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI= github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8= github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0= -github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA= github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk= github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= @@ -36,7 +31,6 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= @@ -69,7 +63,6 @@ github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiT github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= @@ -77,8 +70,6 @@ github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJu go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90 h1:jiDhWWeC7jfWqR9c/uplMOqJ0sbNlNWv0UkzE0vX1MA= golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90/go.mod h1:xE1HEv6b+1SCZ5/uscMRjUBKtIxworgEcEi+/n9NQDQ= -golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY= -golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= @@ -87,9 +78,6 @@ golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU= golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A= golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= -golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0= -golang.org/x/tools/go/expect v0.1.1-deprecated/go.mod h1:eihoPOH+FgIqa3FpoTwguz/bVUSGBlGQU67vpBeOrBY= -golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated/go.mod h1:RVAQXBGNv1ib0J382/DPCRS/BPnsGebyM1Gj5VSDpG8= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= diff --git a/pkg/cli/ansi_test.go b/pkg/cli/ansi_test.go index 1ec7a3e..d73efc2 100644 --- a/pkg/cli/ansi_test.go +++ b/pkg/cli/ansi_test.go @@ -76,9 +76,7 @@ func TestRender_ColorEnabled_Good(t *testing.T) { } func TestUseASCII_Good(t *testing.T) { - // Save original state - original := ColorEnabled() - defer SetColorEnabled(original) + restoreThemeAndColors(t) // Enable first, then UseASCII should disable colors SetColorEnabled(true) diff --git a/pkg/cli/check_test.go b/pkg/cli/check_test.go index 760853c..ed1a532 100644 --- a/pkg/cli/check_test.go +++ b/pkg/cli/check_test.go @@ -3,6 +3,7 @@ package cli import "testing" func TestCheckBuilder(t *testing.T) { + restoreThemeAndColors(t) UseASCII() // Deterministic output // Pass diff --git a/pkg/cli/glyph_test.go b/pkg/cli/glyph_test.go index d43c0be..532e555 100644 --- a/pkg/cli/glyph_test.go +++ b/pkg/cli/glyph_test.go @@ -3,6 +3,7 @@ package cli import "testing" func TestGlyph(t *testing.T) { + restoreThemeAndColors(t) UseUnicode() if Glyph(":check:") != "✓" { t.Errorf("Expected ✓, got %s", Glyph(":check:")) @@ -15,6 +16,7 @@ func TestGlyph(t *testing.T) { } func TestCompileGlyphs(t *testing.T) { + restoreThemeAndColors(t) UseUnicode() got := compileGlyphs("Status: :check:") if got != "Status: ✓" { diff --git a/pkg/cli/output_test.go b/pkg/cli/output_test.go index fbe5c53..ae236d0 100644 --- a/pkg/cli/output_test.go +++ b/pkg/cli/output_test.go @@ -27,6 +27,7 @@ func captureOutput(f func()) string { } func TestSemanticOutput(t *testing.T) { + restoreThemeAndColors(t) UseASCII() // Test Success @@ -102,6 +103,7 @@ func TestSemanticOutput(t *testing.T) { } func TestSemanticOutput_GlyphShortcodes(t *testing.T) { + restoreThemeAndColors(t) UseASCII() out := captureOutput(func() { diff --git a/pkg/cli/tracker.go b/pkg/cli/tracker.go index c89a17f..83da830 100644 --- a/pkg/cli/tracker.go +++ b/pkg/cli/tracker.go @@ -12,8 +12,9 @@ import ( "golang.org/x/term" ) -// Spinner frames (braille pattern). -var spinnerFrames = []string{"⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"} +// Spinner frames for the live tracker. +var spinnerFramesUnicode = []string{"⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"} +var spinnerFramesASCII = []string{"-", "\\", "|", "/"} // taskState tracks the lifecycle of a tracked task. type taskState int @@ -227,7 +228,7 @@ func (tr *TaskTracker) renderLine(idx, frame int) { case taskPending: icon = DimStyle.Render(Glyph(":pending:")) case taskRunning: - icon = InfoStyle.Render(spinnerFrames[frame%len(spinnerFrames)]) + icon = InfoStyle.Render(trackerSpinnerFrame(frame)) case taskDone: icon = SuccessStyle.Render(Glyph(":check:")) case taskFailed: @@ -304,16 +305,24 @@ func (tr *TaskTracker) String() string { var sb strings.Builder for _, t := range tasks { name, status, state := t.snapshot() - icon := "…" + icon := Glyph(":pending:") switch state { case taskDone: - icon = "✓" + icon = Glyph(":check:") case taskFailed: - icon = "✗" + icon = Glyph(":cross:") case taskRunning: - icon = "⠋" + icon = Glyph(":spinner:") } fmt.Fprintf(&sb, "%s %s %s\n", icon, Pad(name, nameW), status) } return sb.String() } + +func trackerSpinnerFrame(frame int) string { + frames := spinnerFramesUnicode + if currentTheme == ThemeASCII { + frames = spinnerFramesASCII + } + return frames[frame%len(frames)] +} diff --git a/pkg/cli/tracker_test.go b/pkg/cli/tracker_test.go index 6f4a813..8bbe798 100644 --- a/pkg/cli/tracker_test.go +++ b/pkg/cli/tracker_test.go @@ -10,6 +10,17 @@ import ( "github.com/stretchr/testify/require" ) +func restoreThemeAndColors(t *testing.T) { + t.Helper() + + prevTheme := currentTheme + prevColor := ColorEnabled() + t.Cleanup(func() { + currentTheme = prevTheme + SetColorEnabled(prevColor) + }) +} + func TestTaskTracker_Good(t *testing.T) { t.Run("add and complete tasks", func(t *testing.T) { tr := NewTaskTracker() @@ -159,6 +170,25 @@ func TestTaskTracker_Good(t *testing.T) { assert.Contains(t, out, "✗") assert.Contains(t, out, "⠋") }) + + t.Run("ASCII theme uses ASCII symbols", func(t *testing.T) { + restoreThemeAndColors(t) + UseASCII() + + tr := NewTaskTracker() + tr.out = &bytes.Buffer{} + + tr.Add("repo-a").Done("clean") + tr.Add("repo-b").Fail("dirty") + tr.Add("repo-c").Update("pulling") + + out := tr.String() + assert.Contains(t, out, "[OK]") + assert.Contains(t, out, "[FAIL]") + assert.Contains(t, out, "-") + assert.NotContains(t, out, "✓") + assert.NotContains(t, out, "✗") + }) } func TestTaskTracker_Bad(t *testing.T) { diff --git a/pkg/cli/tree.go b/pkg/cli/tree.go index ead9195..017c0df 100644 --- a/pkg/cli/tree.go +++ b/pkg/cli/tree.go @@ -90,13 +90,17 @@ func (n *TreeNode) renderLabel() string { } func (n *TreeNode) writeChildren(sb *strings.Builder, prefix string) { + tee := Glyph(":tee:") + Glyph(":dash:") + Glyph(":dash:") + " " + corner := Glyph(":corner:") + Glyph(":dash:") + Glyph(":dash:") + " " + pipe := Glyph(":pipe:") + " " + for i, child := range n.children { last := i == len(n.children)-1 - connector := "├── " - next := "│ " + connector := tee + next := pipe if last { - connector = "└── " + connector = corner next = " " } diff --git a/pkg/cli/tree_test.go b/pkg/cli/tree_test.go index 0efdc5d..af21509 100644 --- a/pkg/cli/tree_test.go +++ b/pkg/cli/tree_test.go @@ -103,6 +103,28 @@ func TestTree_Good(t *testing.T) { "└── child\n" assert.Equal(t, expected, tree.String()) }) + + t.Run("ASCII theme uses ASCII connectors", func(t *testing.T) { + prevTheme := currentTheme + prevColor := ColorEnabled() + UseASCII() + t.Cleanup(func() { + currentTheme = prevTheme + SetColorEnabled(prevColor) + }) + + tree := NewTree("core-php") + tree.Add("core-tenant").Add("core-bio") + tree.Add("core-admin") + tree.Add("core-api") + + expected := "core-php\n" + + "+-- core-tenant\n" + + "| `-- core-bio\n" + + "+-- core-admin\n" + + "`-- core-api\n" + assert.Equal(t, expected, tree.String()) + }) } func TestTree_Bad(t *testing.T) { -- 2.45.3