diff --git a/cmd/uncloud/context/ls.go b/cmd/uncloud/context/ls.go index 80d0e92f..e9eb4b98 100644 --- a/cmd/uncloud/context/ls.go +++ b/cmd/uncloud/context/ls.go @@ -3,11 +3,10 @@ package context import ( "fmt" "maps" - "os" "slices" - "text/tabwriter" "github.com/psviderski/uncloud/internal/cli" + "github.com/psviderski/uncloud/internal/cli/tui" "github.com/spf13/cobra" ) @@ -38,8 +37,8 @@ func list(uncli *cli.CLI) error { contextNames := slices.Sorted(maps.Keys(uncli.Config.Contexts)) currentContext := uncli.Config.CurrentContext - tw := tabwriter.NewWriter(os.Stdout, 0, 0, 3, ' ', 0) - fmt.Fprintln(tw, "NAME\tCURRENT\tCONNECTIONS") + t := tui.NewTable() + t.Headers("NAME", "CURRENT", "CONNECTIONS") for _, name := range contextNames { current := "" @@ -47,8 +46,9 @@ func list(uncli *cli.CLI) error { current = "✓" } 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 } diff --git a/cmd/uncloud/machine/ls.go b/cmd/uncloud/machine/ls.go index de6eeabc..e04c15d8 100644 --- a/cmd/uncloud/machine/ls.go +++ b/cmd/uncloud/machine/ls.go @@ -4,11 +4,10 @@ import ( "context" "fmt" "net/netip" - "os" "strings" - "text/tabwriter" "github.com/psviderski/uncloud/internal/cli" + "github.com/psviderski/uncloud/internal/cli/tui" "github.com/psviderski/uncloud/internal/machine/network" "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. - tw := tabwriter.NewWriter(os.Stdout, 0, 0, 3, ' ', 0) - // Print header. - 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. + t := tui.NewTable() + t.Headers("NAME", "STATE", "ADDRESS", "PUBLIC IP", "WIREGUARD ENDPOINTS", "MACHINE ID") + for _, member := range machines { m := member.Machine subnet, _ := m.Network.Subnet.ToPrefix() @@ -62,14 +58,18 @@ func list(ctx context.Context, uncli *cli.CLI) error { endpoints[i] = addrPort.String() } - if _, err = fmt.Fprintf( - tw, "%s\t%s\t%s\t%s\t%s\t%s\n", m.Name, capitalise(member.State.String()), subnet, publicIP, - strings.Join(endpoints, ", "), member.Machine.Id, - ); err != nil { - return fmt.Errorf("write row: %w", err) - } + t.Row( + m.Name, + capitalise(member.State.String()), + subnet.String(), + 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. diff --git a/cmd/uncloud/machine/rm.go b/cmd/uncloud/machine/rm.go index 82c98c5c..dfd2797c 100644 --- a/cmd/uncloud/machine/rm.go +++ b/cmd/uncloud/machine/rm.go @@ -206,7 +206,7 @@ func formatContainerTree(containers []api.ServiceContainer) string { // Add containers as children. for _, ctr := range ctrs { 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) } diff --git a/cmd/uncloud/service/inspect.go b/cmd/uncloud/service/inspect.go index c87252a3..85eef160 100644 --- a/cmd/uncloud/service/inspect.go +++ b/cmd/uncloud/service/inspect.go @@ -3,14 +3,13 @@ package service import ( "context" "fmt" - "os" "slices" - "text/tabwriter" "time" "github.com/docker/docker/pkg/stringid" "github.com/docker/go-units" "github.com/psviderski/uncloud/internal/cli" + "github.com/psviderski/uncloud/internal/cli/tui" "github.com/psviderski/uncloud/pkg/api" "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. - tw := tabwriter.NewWriter(os.Stdout, 0, 0, 3, ' ', 0) - if _, err = fmt.Fprintln(tw, "CONTAINER ID\tIMAGE\tCREATED\tSTATUS\tIP ADDRESS\tMACHINE"); err != nil { - return fmt.Errorf("write header: %w", err) - } + t := tui.NewTable() + t.Headers("CONTAINER ID", "IMAGE", "CREATED", "STATUS", "IP ADDRESS", "MACHINE") now := time.Now().UTC() for _, ctr := range svc.Containers { @@ -98,19 +95,16 @@ func inspect(ctx context.Context, uncli *cli.CLI, opts inspectOptions) error { ipStr = ip.String() } - _, err = fmt.Fprintf( - tw, - "%s\t%s\t%s\t%s\t%s\t%s\n", + t.Row( stringid.TruncateID(ctr.Container.ID), - ctr.Container.Config.Image, + tui.FormatImage(ctr.Container.Config.Image, tui.NoStyle), created, state, ipStr, machine, ) - if err != nil { - return fmt.Errorf("write row: %w", err) - } } - return tw.Flush() + + fmt.Println(t) + return nil } diff --git a/cmd/uncloud/service/ls.go b/cmd/uncloud/service/ls.go index e32b2a2c..1ccca21f 100644 --- a/cmd/uncloud/service/ls.go +++ b/cmd/uncloud/service/ls.go @@ -3,12 +3,11 @@ package service import ( "context" "fmt" - "os" "slices" "strings" - "text/tabwriter" "github.com/psviderski/uncloud/internal/cli" + "github.com/psviderski/uncloud/internal/cli/tui" "github.com/psviderski/uncloud/pkg/api" "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. - 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. + headers := []string{"NAME", "MODE", "REPLICAS", "IMAGE", "ENDPOINTS"} if haveDuplicateNames { - if _, err = fmt.Fprintf(tw, "ID\t"); err != nil { - 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) + headers = append([]string{"ID"}, headers...) } + t.Headers(headers...) + for _, s := range services { - images := strings.Join(s.Images(), ", ") - endpoints := strings.Join(s.Endpoints(), ", ") + images := s.Images() + 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 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 _, err = fmt.Fprintf(tw, "%s\t", s.ID); err != nil { - return fmt.Errorf("write row: %w", err) - } - } - 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 { - return fmt.Errorf("write row: %w", err) + row = append([]string{s.ID}, row...) } + t.Row(row...) } - return tw.Flush() + + fmt.Println(t) + return nil } diff --git a/cmd/uncloud/volume/ls.go b/cmd/uncloud/volume/ls.go index 19949c31..732ef495 100644 --- a/cmd/uncloud/volume/ls.go +++ b/cmd/uncloud/volume/ls.go @@ -3,12 +3,11 @@ package volume import ( "context" "fmt" - "os" "slices" "strings" - "text/tabwriter" "github.com/psviderski/uncloud/internal/cli" + "github.com/psviderski/uncloud/internal/cli/tui" "github.com/psviderski/uncloud/pkg/api" "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. - tw := tabwriter.NewWriter(os.Stdout, 0, 0, 3, ' ', 0) - fmt.Fprintln(tw, "NAME\tDRIVER\tMACHINE") + t := tui.NewTable() + t.Headers("NAME", "DRIVER", "MACHINE") for _, v := range volumes { - fmt.Fprintf(tw, "%s\t%s\t%s\n", - v.Volume.Name, - v.Volume.Driver, - v.MachineName, - ) + t.Row(v.Volume.Name, v.Volume.Driver, v.MachineName) } - return tw.Flush() + fmt.Println(t) + return nil } diff --git a/cmd/uncloud/wg/wg.go b/cmd/uncloud/wg/wg.go index 86baa003..4a8b99b7 100644 --- a/cmd/uncloud/wg/wg.go +++ b/cmd/uncloud/wg/wg.go @@ -3,13 +3,12 @@ package wg import ( "context" "fmt" - "os" "strings" - "text/tabwriter" "time" "github.com/docker/go-units" "github.com/psviderski/uncloud/internal/cli" + "github.com/psviderski/uncloud/internal/cli/tui" "github.com/spf13/cobra" "golang.zx2c4.com/wireguard/wgctrl/wgtypes" "google.golang.org/grpc/codes" @@ -97,10 +96,8 @@ func runShow(ctx context.Context, uncli *cli.CLI, opts showOptions) error { return nil } - tw := tabwriter.NewWriter(os.Stdout, 0, 0, 3, ' ', 0) - if _, err = fmt.Fprintln(tw, "PEER\tPUBLIC KEY\tENDPOINT\tHANDSHAKE\tRECEIVED\tSENT\tALLOWED IPS"); err != nil { - return fmt.Errorf("write header: %w", err) - } + t := tui.NewTable() + t.Headers("PEER", "PUBLIC KEY", "ENDPOINT", "HANDSHAKE", "RECEIVED", "SENT", "ALLOWED IPS") for _, peer := range resp.Peers { 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" } - _, err = fmt.Fprintf( - tw, - "%s\t%s\t%s\t%s\t%s\t%s\t%s\n", + t.Row( machineName, wgtypes.Key(peer.PublicKey).String(), peer.Endpoint, lastHandshake, units.HumanSize(float64(peer.ReceiveBytes)), 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 }