Skip to content

Commit 7a9ea1b

Browse files
committed
feat: ls sessions as table
1 parent 779940e commit 7a9ea1b

2 files changed

Lines changed: 85 additions & 7 deletions

File tree

internal/cli/list_cmd.go

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@ package cli
22

33
import (
44
"fmt"
5+
"regexp"
56
"sort"
67
"strings"
78

89
"github.com/chenasraf/tx/internal/config"
10+
"github.com/chenasraf/tx/internal/table"
911
"github.com/chenasraf/tx/internal/tmux"
1012
"github.com/spf13/cobra"
1113
)
@@ -61,13 +63,7 @@ func runList(cmd *cobra.Command, args []string) error {
6163
sessionsOutput, err := tmux.ListSessions(opts)
6264
sessionsStr := ""
6365
if err == nil && sessionsOutput != "" {
64-
// Format sessions output
65-
lines := strings.Split(strings.TrimSpace(sessionsOutput), "\n")
66-
for _, line := range lines {
67-
if line != "" {
68-
sessionsStr += " " + line + "\n"
69-
}
70-
}
66+
sessionsStr = formatSessionsTable(sessionsOutput, " ")
7167
} else {
7268
sessionsStr = " No tmux sessions\n"
7369
}
@@ -107,3 +103,28 @@ func runList(cmd *cobra.Command, args []string) error {
107103

108104
return nil
109105
}
106+
107+
var sessionLineRe = regexp.MustCompile(`^([^:]+):\s*(\d+)\s+windows?\s*\(created\s+([^)]+)\)\s*(.*)$`)
108+
109+
// formatSessionsTable parses `tmux ls` output and renders it as a bordered
110+
// table with headers. Each output line is prefixed with indent.
111+
func formatSessionsTable(raw, indent string) string {
112+
lines := strings.Split(strings.TrimSpace(raw), "\n")
113+
rows := make([][]string, 0, len(lines))
114+
for _, line := range lines {
115+
if line == "" {
116+
continue
117+
}
118+
m := sessionLineRe.FindStringSubmatch(line)
119+
if m == nil {
120+
// Fallback: dump the whole line into the Name column
121+
rows = append(rows, []string{line, "", "", ""})
122+
continue
123+
}
124+
status := strings.TrimSpace(m[4])
125+
status = strings.TrimPrefix(status, "(")
126+
status = strings.TrimSuffix(status, ")")
127+
rows = append(rows, []string{m[1], m[2], m[3], status})
128+
}
129+
return table.Render([]string{"Name", "Windows", "Created", "Status"}, rows, indent)
130+
}

internal/table/table.go

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
// Package table renders simple bordered text tables with headers.
2+
package table
3+
4+
import (
5+
"strings"
6+
"unicode/utf8"
7+
)
8+
9+
// Render returns a bordered table string with the given headers and rows.
10+
// Every line in the output is prefixed with indent. Column widths auto-size
11+
// to the widest cell (header included).
12+
func Render(headers []string, rows [][]string, indent string) string {
13+
widths := make([]int, len(headers))
14+
for i, h := range headers {
15+
widths[i] = utf8.RuneCountInString(h)
16+
}
17+
for _, row := range rows {
18+
for i, cell := range row {
19+
if i >= len(widths) {
20+
break
21+
}
22+
if w := utf8.RuneCountInString(cell); w > widths[i] {
23+
widths[i] = w
24+
}
25+
}
26+
}
27+
28+
border := func(left, mid, right string) string {
29+
parts := make([]string, len(widths))
30+
for i, w := range widths {
31+
parts[i] = strings.Repeat("─", w+2)
32+
}
33+
return left + strings.Join(parts, mid) + right
34+
}
35+
rowLine := func(cells []string) string {
36+
parts := make([]string, len(widths))
37+
for i := range widths {
38+
cell := ""
39+
if i < len(cells) {
40+
cell = cells[i]
41+
}
42+
pad := max(widths[i]-utf8.RuneCountInString(cell), 0)
43+
parts[i] = " " + cell + strings.Repeat(" ", pad) + " "
44+
}
45+
return "│" + strings.Join(parts, "│") + "│"
46+
}
47+
48+
var b strings.Builder
49+
b.WriteString(indent + border("┌", "┬", "┐") + "\n")
50+
b.WriteString(indent + rowLine(headers) + "\n")
51+
b.WriteString(indent + border("├", "┼", "┤") + "\n")
52+
for _, row := range rows {
53+
b.WriteString(indent + rowLine(row) + "\n")
54+
}
55+
b.WriteString(indent + border("└", "┴", "┘") + "\n")
56+
return b.String()
57+
}

0 commit comments

Comments
 (0)