Skip to content

Commit 945db30

Browse files
committed
Release v2.5.1: Fix table truncation, spinner grace period
Table: revert from Bubbles table (designed for interactive TUI) back to manual lipgloss-styled table that respects full content width. Bubbles table was truncating emails and dates too aggressively. Spinner: add 200ms grace period before showing. Fast commands (<200ms) complete without any spinner flash. Slow commands get the animated spinner after the grace period.
1 parent d5c898e commit 945db30

4 files changed

Lines changed: 48 additions & 50 deletions

File tree

VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
2.5.0
1+
2.5.1

internal/ui/spinner.go

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,19 +6,31 @@ import (
66
"time"
77
)
88

9-
// Spinner frames from charmbracelet/bubbles dot spinner
109
var spinnerFrames = []string{"⣾", "⣽", "⣻", "⢿", "⡿", "⣟", "⣯", "⣷"}
1110

11+
// Spinner shows an animated indicator while work is in progress.
12+
// Has a 200ms grace period before showing to avoid flashing on fast operations.
1213
type Spinner struct {
13-
stop chan struct{}
14-
done sync.WaitGroup
14+
stop chan struct{}
15+
done sync.WaitGroup
16+
showed bool
1517
}
1618

19+
// StartSpinner begins a spinner that appears after 200ms.
1720
func StartSpinner(msg string) *Spinner {
1821
s := &Spinner{stop: make(chan struct{})}
1922
s.done.Add(1)
2023
go func() {
2124
defer s.done.Done()
25+
26+
// Grace period: don't show spinner for fast operations
27+
select {
28+
case <-s.stop:
29+
return
30+
case <-time.After(200 * time.Millisecond):
31+
}
32+
33+
s.showed = true
2234
i := 0
2335
for {
2436
select {
@@ -27,8 +39,7 @@ func StartSpinner(msg string) *Spinner {
2739
return
2840
default:
2941
frame := spinnerFrames[i%len(spinnerFrames)]
30-
styled := BranchStyle.Render(frame)
31-
fmt.Printf("\r%s %s", styled, DimStyle.Render(msg))
42+
fmt.Printf("\r%s %s", BranchStyle.Render(frame), DimStyle.Render(msg))
3243
i++
3344
time.Sleep(80 * time.Millisecond)
3445
}
@@ -37,6 +48,7 @@ func StartSpinner(msg string) *Spinner {
3748
return s
3849
}
3950

51+
// Stop halts the spinner and clears the line.
4052
func (s *Spinner) Stop() {
4153
close(s.stop)
4254
s.done.Wait()

internal/ui/table.go

Lines changed: 29 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -2,80 +2,66 @@ package ui
22

33
import (
44
"fmt"
5+
"strings"
56

6-
"github.com/charmbracelet/bubbles/table"
77
"github.com/charmbracelet/lipgloss"
88
)
99

1010
var (
11-
titleStyle = lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("5")).MarginBottom(1)
11+
headerStyle = lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("4"))
12+
titleStyle = lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("5")).MarginBottom(1)
13+
sepStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("240"))
1214
)
1315

14-
const maxColWidth = 50
15-
1616
func PrintTable(headers []string, rows [][]string, title string) {
1717
if title != "" {
1818
fmt.Println(titleStyle.Render(title))
1919
}
2020

21-
// Calculate column widths based on content
21+
// Calculate column widths from VISIBLE content width (ANSI-aware)
2222
widths := make([]int, len(headers))
2323
for i, h := range headers {
24-
widths[i] = lipgloss.Width(h) + 2
24+
widths[i] = len(h)
2525
}
2626
for _, row := range rows {
2727
for i, cell := range row {
28-
w := lipgloss.Width(cell) + 2
28+
w := lipgloss.Width(cell)
2929
if i < len(widths) && w > widths[i] {
3030
widths[i] = w
3131
}
3232
}
3333
}
3434

35-
// Cap non-last columns
36-
lastCol := len(headers) - 1
35+
// Add padding
3736
for i := range widths {
38-
if i != lastCol && widths[i] > maxColWidth {
39-
widths[i] = maxColWidth
40-
}
37+
widths[i] += 2
4138
}
4239

43-
// Build columns
44-
cols := make([]table.Column, len(headers))
40+
// Header
41+
hParts := make([]string, len(headers))
4542
for i, h := range headers {
46-
cols[i] = table.Column{Title: h, Width: widths[i]}
43+
hParts[i] = headerStyle.Width(widths[i]).Render(h)
44+
}
45+
fmt.Println(strings.Join(hParts, ""))
46+
47+
// Separator
48+
sParts := make([]string, len(headers))
49+
for i := range headers {
50+
sParts[i] = sepStyle.Render(strings.Repeat("─", widths[i]))
4751
}
52+
fmt.Println(strings.Join(sParts, ""))
4853

49-
// Build rows
50-
tableRows := make([]table.Row, len(rows))
51-
for i, row := range rows {
52-
r := make(table.Row, len(headers))
53-
for j := range headers {
54-
if j < len(row) {
55-
r[j] = row[j]
54+
// Rows - use lipgloss Width for ANSI-aware padding, MaxWidth for truncation
55+
for _, row := range rows {
56+
parts := make([]string, len(headers))
57+
for i := range headers {
58+
cell := ""
59+
if i < len(row) {
60+
cell = row[i]
5661
}
62+
parts[i] = lipgloss.NewStyle().Width(widths[i]).MaxWidth(widths[i]).Render(cell)
5763
}
58-
tableRows[i] = r
64+
fmt.Println(strings.Join(parts, ""))
5965
}
60-
61-
// Style the table
62-
s := table.DefaultStyles()
63-
s.Header = s.Header.
64-
Bold(true).
65-
Foreground(lipgloss.Color("4")).
66-
BorderStyle(lipgloss.NormalBorder()).
67-
BorderBottom(true).
68-
BorderForeground(lipgloss.Color("240"))
69-
s.Selected = lipgloss.NewStyle() // Disable selection highlighting for static display
70-
s.Cell = s.Cell.Padding(0, 1)
71-
72-
t := table.New(
73-
table.WithColumns(cols),
74-
table.WithRows(tableRows),
75-
table.WithHeight(len(tableRows)+1),
76-
table.WithStyles(s),
77-
)
78-
79-
fmt.Println(t.View())
8066
fmt.Println()
8167
}

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
44

55
[project]
66
name = "gx-git"
7-
version = "2.5.0"
7+
version = "2.5.1"
88
description = "Git Productivity Toolkit"
99
readme = "README.md"
1010
license = {text = "MIT"}

0 commit comments

Comments
 (0)