refactor: replace tabwriter with a Table for consistent table rendering across commands

This commit is contained in:
Pasha Sviderski
2026-03-30 14:32:37 +10:00
parent e22cdf0b18
commit 636307d3ea
7 changed files with 62 additions and 79 deletions
+6 -6
View File
@@ -3,11 +3,10 @@ package context
import ( import (
"fmt" "fmt"
"maps" "maps"
"os"
"slices" "slices"
"text/tabwriter"
"github.com/psviderski/uncloud/internal/cli" "github.com/psviderski/uncloud/internal/cli"
"github.com/psviderski/uncloud/internal/cli/tui"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@@ -38,8 +37,8 @@ func list(uncli *cli.CLI) error {
contextNames := slices.Sorted(maps.Keys(uncli.Config.Contexts)) contextNames := slices.Sorted(maps.Keys(uncli.Config.Contexts))
currentContext := uncli.Config.CurrentContext currentContext := uncli.Config.CurrentContext
tw := tabwriter.NewWriter(os.Stdout, 0, 0, 3, ' ', 0) t := tui.NewTable()
fmt.Fprintln(tw, "NAME\tCURRENT\tCONNECTIONS") t.Headers("NAME", "CURRENT", "CONNECTIONS")
for _, name := range contextNames { for _, name := range contextNames {
current := "" current := ""
@@ -47,8 +46,9 @@ func list(uncli *cli.CLI) error {
current = "✓" current = "✓"
} }
connCount := len(uncli.Config.Contexts[name].Connections) connCount := len(uncli.Config.Contexts[name].Connections)
fmt.Fprintf(tw, "%s\t%s\t%d\n", name, current, connCount) t.Row(name, current, fmt.Sprintf("%d", connCount))
} }
return tw.Flush() fmt.Println(t)
return nil
} }
+15 -15
View File
@@ -4,11 +4,10 @@ import (
"context" "context"
"fmt" "fmt"
"net/netip" "net/netip"
"os"
"strings" "strings"
"text/tabwriter"
"github.com/psviderski/uncloud/internal/cli" "github.com/psviderski/uncloud/internal/cli"
"github.com/psviderski/uncloud/internal/cli/tui"
"github.com/psviderski/uncloud/internal/machine/network" "github.com/psviderski/uncloud/internal/machine/network"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@@ -39,12 +38,9 @@ func list(ctx context.Context, uncli *cli.CLI) error {
} }
// Print the list of machines in a table format. // Print the list of machines in a table format.
tw := tabwriter.NewWriter(os.Stdout, 0, 0, 3, ' ', 0) t := tui.NewTable()
// Print header. t.Headers("NAME", "STATE", "ADDRESS", "PUBLIC IP", "WIREGUARD ENDPOINTS", "MACHINE ID")
if _, err = fmt.Fprintln(tw, "NAME\tSTATE\tADDRESS\tPUBLIC IP\tWIREGUARD ENDPOINTS\tMACHINE ID"); err != nil {
return fmt.Errorf("write header: %w", err)
}
// Print rows.
for _, member := range machines { for _, member := range machines {
m := member.Machine m := member.Machine
subnet, _ := m.Network.Subnet.ToPrefix() subnet, _ := m.Network.Subnet.ToPrefix()
@@ -62,14 +58,18 @@ func list(ctx context.Context, uncli *cli.CLI) error {
endpoints[i] = addrPort.String() endpoints[i] = addrPort.String()
} }
if _, err = fmt.Fprintf( t.Row(
tw, "%s\t%s\t%s\t%s\t%s\t%s\n", m.Name, capitalise(member.State.String()), subnet, publicIP, m.Name,
strings.Join(endpoints, ", "), member.Machine.Id, capitalise(member.State.String()),
); err != nil { subnet.String(),
return fmt.Errorf("write row: %w", err) publicIP,
strings.Join(endpoints, tui.Faint.Render(", ")),
member.Machine.Id,
)
} }
}
return tw.Flush() fmt.Println(t)
return nil
} }
// capitalise returns a string where the first character is upper case, and the rest is lower case. // capitalise returns a string where the first character is upper case, and the rest is lower case.
+1 -1
View File
@@ -206,7 +206,7 @@ func formatContainerTree(containers []api.ServiceContainer) string {
// Add containers as children. // Add containers as children.
for _, ctr := range ctrs { for _, ctr := range ctrs {
state, _ := ctr.HumanState() state, _ := ctr.HumanState()
info := fmt.Sprintf("%s • %s • %s", ctr.Name, ctr.Config.Image, state) info := fmt.Sprintf("%s • %s • %s", ctr.Name, tui.FormatImage(ctr.Config.Image, tui.NoStyle), state)
t.Child(info) t.Child(info)
} }
+8 -14
View File
@@ -3,14 +3,13 @@ package service
import ( import (
"context" "context"
"fmt" "fmt"
"os"
"slices" "slices"
"text/tabwriter"
"time" "time"
"github.com/docker/docker/pkg/stringid" "github.com/docker/docker/pkg/stringid"
"github.com/docker/go-units" "github.com/docker/go-units"
"github.com/psviderski/uncloud/internal/cli" "github.com/psviderski/uncloud/internal/cli"
"github.com/psviderski/uncloud/internal/cli/tui"
"github.com/psviderski/uncloud/pkg/api" "github.com/psviderski/uncloud/pkg/api"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@@ -73,10 +72,8 @@ func inspect(ctx context.Context, uncli *cli.CLI, opts inspectOptions) error {
}) })
// Print the list of containers in a table format. // Print the list of containers in a table format.
tw := tabwriter.NewWriter(os.Stdout, 0, 0, 3, ' ', 0) t := tui.NewTable()
if _, err = fmt.Fprintln(tw, "CONTAINER ID\tIMAGE\tCREATED\tSTATUS\tIP ADDRESS\tMACHINE"); err != nil { t.Headers("CONTAINER ID", "IMAGE", "CREATED", "STATUS", "IP ADDRESS", "MACHINE")
return fmt.Errorf("write header: %w", err)
}
now := time.Now().UTC() now := time.Now().UTC()
for _, ctr := range svc.Containers { for _, ctr := range svc.Containers {
@@ -98,19 +95,16 @@ func inspect(ctx context.Context, uncli *cli.CLI, opts inspectOptions) error {
ipStr = ip.String() ipStr = ip.String()
} }
_, err = fmt.Fprintf( t.Row(
tw,
"%s\t%s\t%s\t%s\t%s\t%s\n",
stringid.TruncateID(ctr.Container.ID), stringid.TruncateID(ctr.Container.ID),
ctr.Container.Config.Image, tui.FormatImage(ctr.Container.Config.Image, tui.NoStyle),
created, created,
state, state,
ipStr, ipStr,
machine, machine,
) )
if err != nil {
return fmt.Errorf("write row: %w", err)
} }
}
return tw.Flush() fmt.Println(t)
return nil
} }
+18 -19
View File
@@ -3,12 +3,11 @@ package service
import ( import (
"context" "context"
"fmt" "fmt"
"os"
"slices" "slices"
"strings" "strings"
"text/tabwriter"
"github.com/psviderski/uncloud/internal/cli" "github.com/psviderski/uncloud/internal/cli"
"github.com/psviderski/uncloud/internal/cli/tui"
"github.com/psviderski/uncloud/pkg/api" "github.com/psviderski/uncloud/pkg/api"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@@ -50,20 +49,22 @@ func list(ctx context.Context, uncli *cli.CLI) error {
}) })
// Print the list of services in a table format. // Print the list of services in a table format.
tw := tabwriter.NewWriter(os.Stdout, 0, 0, 3, ' ', 0) t := tui.NewTable()
// Include the ID column if there are duplicate service names to differentiate them. // Include the ID column if there are duplicate service names to differentiate them.
headers := []string{"NAME", "MODE", "REPLICAS", "IMAGE", "ENDPOINTS"}
if haveDuplicateNames { if haveDuplicateNames {
if _, err = fmt.Fprintf(tw, "ID\t"); err != nil { headers = append([]string{"ID"}, headers...)
return fmt.Errorf("write header: %w", err)
}
}
if _, err = fmt.Fprintln(tw, "NAME\tMODE\tREPLICAS\tIMAGE\tENDPOINTS"); err != nil {
return fmt.Errorf("write header: %w", err)
} }
t.Headers(headers...)
for _, s := range services { for _, s := range services {
images := strings.Join(s.Images(), ", ") images := s.Images()
endpoints := strings.Join(s.Endpoints(), ", ") for i, img := range images {
images[i] = tui.FormatImage(img, tui.NoStyle)
}
formattedImages := strings.Join(images, tui.Faint.Render(", "))
endpoints := strings.Join(s.Endpoints(), tui.Faint.Render(", "))
// If no endpoints from ports, check if the service uses custom Caddy config. // If no endpoints from ports, check if the service uses custom Caddy config.
if endpoints == "" { if endpoints == "" {
@@ -74,15 +75,13 @@ func list(ctx context.Context, uncli *cli.CLI) error {
} }
} }
row := []string{s.Name, s.Mode, fmt.Sprintf("%d", len(s.Containers)), formattedImages, endpoints}
if haveDuplicateNames { if haveDuplicateNames {
if _, err = fmt.Fprintf(tw, "%s\t", s.ID); err != nil { row = append([]string{s.ID}, row...)
return fmt.Errorf("write row: %w", err)
} }
t.Row(row...)
} }
if _, err = fmt.Fprintf(tw, "%s\t%s\t%d\t%s\t%s\n",
s.Name, s.Mode, len(s.Containers), images, endpoints); err != nil { fmt.Println(t)
return fmt.Errorf("write row: %w", err) return nil
}
}
return tw.Flush()
} }
+6 -10
View File
@@ -3,12 +3,11 @@ package volume
import ( import (
"context" "context"
"fmt" "fmt"
"os"
"slices" "slices"
"strings" "strings"
"text/tabwriter"
"github.com/psviderski/uncloud/internal/cli" "github.com/psviderski/uncloud/internal/cli"
"github.com/psviderski/uncloud/internal/cli/tui"
"github.com/psviderski/uncloud/pkg/api" "github.com/psviderski/uncloud/pkg/api"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@@ -86,16 +85,13 @@ func list(ctx context.Context, uncli *cli.CLI, opts listOptions) error {
} }
// Print the volumes in a table format. // Print the volumes in a table format.
tw := tabwriter.NewWriter(os.Stdout, 0, 0, 3, ' ', 0) t := tui.NewTable()
fmt.Fprintln(tw, "NAME\tDRIVER\tMACHINE") t.Headers("NAME", "DRIVER", "MACHINE")
for _, v := range volumes { for _, v := range volumes {
fmt.Fprintf(tw, "%s\t%s\t%s\n", t.Row(v.Volume.Name, v.Volume.Driver, v.MachineName)
v.Volume.Name,
v.Volume.Driver,
v.MachineName,
)
} }
return tw.Flush() fmt.Println(t)
return nil
} }
+8 -14
View File
@@ -3,13 +3,12 @@ package wg
import ( import (
"context" "context"
"fmt" "fmt"
"os"
"strings" "strings"
"text/tabwriter"
"time" "time"
"github.com/docker/go-units" "github.com/docker/go-units"
"github.com/psviderski/uncloud/internal/cli" "github.com/psviderski/uncloud/internal/cli"
"github.com/psviderski/uncloud/internal/cli/tui"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes" "golang.zx2c4.com/wireguard/wgctrl/wgtypes"
"google.golang.org/grpc/codes" "google.golang.org/grpc/codes"
@@ -97,10 +96,8 @@ func runShow(ctx context.Context, uncli *cli.CLI, opts showOptions) error {
return nil return nil
} }
tw := tabwriter.NewWriter(os.Stdout, 0, 0, 3, ' ', 0) t := tui.NewTable()
if _, err = fmt.Fprintln(tw, "PEER\tPUBLIC KEY\tENDPOINT\tHANDSHAKE\tRECEIVED\tSENT\tALLOWED IPS"); err != nil { t.Headers("PEER", "PUBLIC KEY", "ENDPOINT", "HANDSHAKE", "RECEIVED", "SENT", "ALLOWED IPS")
return fmt.Errorf("write header: %w", err)
}
for _, peer := range resp.Peers { for _, peer := range resp.Peers {
machineName, ok := machinesNamesByPublicKey[wgtypes.Key(peer.PublicKey).String()] machineName, ok := machinesNamesByPublicKey[wgtypes.Key(peer.PublicKey).String()]
@@ -113,20 +110,17 @@ func runShow(ctx context.Context, uncli *cli.CLI, opts showOptions) error {
lastHandshake = time.Since(peer.LastHandshakeTime.AsTime()).Round(time.Second).String() + " ago" lastHandshake = time.Since(peer.LastHandshakeTime.AsTime()).Round(time.Second).String() + " ago"
} }
_, err = fmt.Fprintf( t.Row(
tw,
"%s\t%s\t%s\t%s\t%s\t%s\t%s\n",
machineName, machineName,
wgtypes.Key(peer.PublicKey).String(), wgtypes.Key(peer.PublicKey).String(),
peer.Endpoint, peer.Endpoint,
lastHandshake, lastHandshake,
units.HumanSize(float64(peer.ReceiveBytes)), units.HumanSize(float64(peer.ReceiveBytes)),
units.HumanSize(float64(peer.TransmitBytes)), units.HumanSize(float64(peer.TransmitBytes)),
strings.Join(peer.AllowedIps, ", "), strings.Join(peer.AllowedIps, tui.Faint.Render(", ")),
) )
if err != nil {
return fmt.Errorf("write row: %w", err)
} }
}
return tw.Flush() fmt.Println(t)
return nil
} }