Skip to content

Commit a2cdd87

Browse files
committed
feat(cliutil): Added CLI table rendering function SimpleTable, ShowTable, FormatTable
- 实现SimpleTable函数使用text/tabwriter进行简单表格渲染 - 实现TableBuilder用于构建带边框的格式化表格
1 parent c45ff43 commit a2cdd87

File tree

2 files changed

+291
-0
lines changed

2 files changed

+291
-0
lines changed

cliutil/table.go

Lines changed: 266 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,266 @@
1+
package cliutil
2+
3+
import (
4+
"bytes"
5+
"fmt"
6+
"strings"
7+
"text/tabwriter"
8+
9+
"github.com/gookit/goutil/x/ccolor"
10+
"github.com/gookit/goutil/x/stdio"
11+
)
12+
13+
// SimpleTable 使用标准库 text/tabwriter 实现简单的表格
14+
func SimpleTable(cols []string, rows [][]any) {
15+
var sb strings.Builder
16+
// 参数说明:输出流, 最小单元宽度, 制表符宽度, 填充空格数, 填充字节, 标志位(Debug加"|")
17+
w := tabwriter.NewWriter(&sb, 0, 2, 3, ' ', tabwriter.Debug)
18+
19+
// 渲染表头
20+
header := strings.Join(cols, "\t ")
21+
stdio.Fprintln(w, header)
22+
// stdio.Fprintln(w, strings.Repeat("-", len(header) + len(cols)*2))
23+
24+
// 渲染行
25+
for _, row := range rows {
26+
var rowStr []string
27+
for _, cell := range row {
28+
rowStr = append(rowStr, fmt.Sprintf("%v", cell))
29+
}
30+
stdio.Fprintln(w, strings.Join(rowStr, "\t "))
31+
}
32+
33+
_ = w.Flush()
34+
fmt.Println(sb.String())
35+
}
36+
37+
// TableStyle 定义表格的样式
38+
type TableStyle struct {
39+
// 边框字符
40+
BorderTop string // 顶部边框
41+
BorderBottom string // 底部边框
42+
BorderLeft string // 左边框
43+
BorderRight string // 右边框
44+
BorderJoin string // 内部交叉点
45+
BorderRowSep string // 行分隔符
46+
HeaderSep string // 表头分隔符
47+
ColumnSep string // 列分隔符
48+
ColumnPadding int // 列内边距
49+
50+
// 默认的对齐方式 0: 左对齐, 1: 居中, 2: 右对齐
51+
// - 默认左对齐
52+
DefaultAlign int
53+
// 定义每列的对齐方式 (0: 左对齐, 1: 居中, 2: 右对齐)
54+
Align []int
55+
56+
// 颜色设置 (需要终端支持 ANSI 颜色)
57+
HeaderColor string
58+
RowColor string
59+
BorderColor string
60+
}
61+
62+
// 默认的表格样式实例
63+
var defaultStyle = DefaultStyle()
64+
65+
// MinimalStyle 极简风格 (无边框)
66+
var MinimalStyle = &TableStyle{
67+
BorderTop: "",
68+
BorderBottom: "",
69+
BorderLeft: "",
70+
BorderRight: "",
71+
BorderJoin: " ",
72+
HeaderSep: "-",
73+
ColumnSep: " ",
74+
BorderRowSep: "",
75+
ColumnPadding: 1,
76+
HeaderColor: "ylw", // 黄色
77+
}
78+
79+
// DefaultStyle 返回默认的表格样式
80+
func DefaultStyle() *TableStyle {
81+
return &TableStyle{
82+
BorderTop: "-", // 使用横线作为顶部边框
83+
BorderBottom: "-", // 使用横线作为底部边框
84+
BorderLeft: "|", // 使用竖线作为左边框
85+
BorderRight: "|", // 使用竖线作为右边框
86+
BorderJoin: "+", // 使用加号作为交叉点
87+
BorderRowSep: "-", // 使用横线作为行分隔
88+
HeaderSep: "=", // 使用等号作为表头分隔
89+
ColumnSep: "|",
90+
ColumnPadding: 1, // 默认1个空格的边距
91+
HeaderColor: "bold", // 粗体
92+
RowColor: "", // 默认无颜色
93+
BorderColor: "", // 默认无颜色
94+
}
95+
}
96+
97+
// ShowTable CLI渲染显示显示表格
98+
func ShowTable(cols []string, rows [][]any, options ...*TableStyle) {
99+
ccolor.Println(FormatTable(cols, rows, options...))
100+
}
101+
102+
// FormatTable CLI渲染显示显示表格
103+
func FormatTable(cols []string, rows [][]any, options ...*TableStyle) string {
104+
return NewTableBuilder(options...).Format(cols, rows)
105+
}
106+
107+
type TableBuilder struct {
108+
*TableStyle
109+
// sb strings.Builder
110+
// context data
111+
colWidths []int
112+
totalWidth int
113+
}
114+
115+
func NewTableBuilder(options ...*TableStyle) *TableBuilder {
116+
t := &TableBuilder{
117+
TableStyle: defaultStyle,
118+
}
119+
120+
// 处理选项
121+
if len(options) > 0 && options[0] != nil {
122+
t.TableStyle = options[0]
123+
}
124+
return t
125+
}
126+
127+
func (t *TableBuilder) prepare(cols []string, rows [][]any) {
128+
// 计算每列的最大宽度 (包含padding)
129+
colWidths := make([]int, len(cols))
130+
for i, col := range cols {
131+
colWidths[i] = len(col)
132+
}
133+
for _, row := range rows {
134+
for i, cell := range row {
135+
str := fmt.Sprintf("%v", cell)
136+
if len(str) > colWidths[i] {
137+
colWidths[i] = len(str)
138+
}
139+
}
140+
}
141+
t.colWidths = colWidths
142+
143+
// 计算总宽度
144+
padding := t.ColumnPadding
145+
for _, w := range colWidths {
146+
t.totalWidth += w + padding*2 + 1 // 内容 + 左右padding + 分隔符
147+
}
148+
t.totalWidth += 1 // 最后的右边框
149+
150+
}
151+
152+
// Format 格式化构建CLI表格
153+
func (t *TableBuilder) Format(cols []string, rows [][]any) string {
154+
if len(cols) == 0 {
155+
return ""
156+
}
157+
158+
t.prepare(cols, rows)
159+
160+
var buf bytes.Buffer
161+
162+
// 1. 顶部边框
163+
if t.BorderTop != "" {
164+
buf.WriteString(t.buildSep(t.BorderTop))
165+
buf.WriteRune('\n')
166+
}
167+
168+
// 2. 表头
169+
buf.WriteString(t.buildRow(cols, true))
170+
171+
// 3. 表头分隔符
172+
if t.HeaderSep != "" {
173+
buf.WriteString(t.buildSep(t.HeaderSep))
174+
buf.WriteRune('\n')
175+
}
176+
177+
// 4. 数据行
178+
for _, row := range rows {
179+
rowStrs := make([]string, len(cols))
180+
for i, cell := range row {
181+
rowStrs[i] = fmt.Sprintf("%v", cell)
182+
}
183+
buf.WriteString(t.buildRow(rowStrs, false))
184+
}
185+
186+
// 底部边框
187+
if t.BorderBottom != "" {
188+
buf.WriteString(t.buildSep(t.BorderBottom))
189+
}
190+
191+
return buf.String()
192+
}
193+
194+
// 构建辅助函数:构建分隔线
195+
func (t *TableBuilder) buildSep(char string) string {
196+
if char == "" {
197+
return ""
198+
}
199+
200+
var sb strings.Builder
201+
sb.WriteString(t.BorderLeft)
202+
for i, w := range t.colWidths {
203+
if i > 0 {
204+
sb.WriteString(t.BorderJoin)
205+
}
206+
sb.WriteString(strings.Repeat(char, w+t.ColumnPadding*2))
207+
}
208+
209+
sb.WriteString(t.BorderRight)
210+
return sb.String()
211+
}
212+
213+
// 构建辅助函数:构建一行
214+
func (t *TableBuilder) buildRow(cells []string, isHeader bool) string {
215+
var sb strings.Builder
216+
sb.WriteString(t.BorderLeft)
217+
padding := t.ColumnPadding
218+
219+
for i, cell := range cells {
220+
if i > 0 {
221+
// sb.WriteString(t.BorderColor)
222+
sb.WriteString(t.ColumnSep)
223+
}
224+
225+
// 应用对齐
226+
width := t.colWidths[i]
227+
alignment := 0 // 默认左对齐
228+
if t.Align != nil && i < len(t.Align) {
229+
alignment = t.Align[i]
230+
}
231+
232+
padLeft := strings.Repeat(" ", padding)
233+
padRight := strings.Repeat(" ", padding)
234+
235+
switch alignment {
236+
case 1: // 居中
237+
totalPad := width - len(cell)
238+
if totalPad > 0 {
239+
leftPad := totalPad / 2
240+
rightPad := totalPad - leftPad
241+
padLeft = strings.Repeat(" ", padding+leftPad)
242+
padRight = strings.Repeat(" ", padding+rightPad)
243+
}
244+
case 2: // 右对齐
245+
padLeft = strings.Repeat(" ", width+padding-len(cell))
246+
padRight = strings.Repeat(" ", padding)
247+
default: // 左对齐
248+
padRight = strings.Repeat(" ", width+padding-len(cell))
249+
}
250+
251+
sb.WriteString(padLeft)
252+
// sb.WriteString(cell)
253+
if isHeader {
254+
sb.WriteString(ccolor.WrapTag(cell, t.HeaderColor))
255+
} else {
256+
sb.WriteString(ccolor.WrapTag(cell, t.RowColor))
257+
}
258+
259+
sb.WriteString(padRight)
260+
}
261+
262+
sb.WriteString(t.BorderRight)
263+
sb.WriteString("\n")
264+
265+
return sb.String()
266+
}

cliutil/table_test.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package cliutil_test
2+
3+
import (
4+
"fmt"
5+
"testing"
6+
7+
"github.com/gookit/goutil/cliutil"
8+
)
9+
10+
func TestShowTable(t *testing.T) {
11+
cols := []string{"ID", "Name", "Age", "Active"}
12+
rows := [][]any{
13+
{1, "Alice", 28, true},
14+
{2, "Bob", 32, false},
15+
{3, "Charlie", 24, true},
16+
}
17+
18+
fmt.Println("Base on text/tabwriter")
19+
cliutil.SimpleTable(cols, rows)
20+
21+
fmt.Println("Use custom render:")
22+
cliutil.ShowTable(cols, rows)
23+
fmt.Println("Use MinimalStyle:")
24+
cliutil.ShowTable(cols, rows, cliutil.MinimalStyle)
25+
}

0 commit comments

Comments
 (0)