diff --git a/internal/cli/cli.go b/internal/cli/cli.go index 2c752c94e4..02f7e399c5 100644 --- a/internal/cli/cli.go +++ b/internal/cli/cli.go @@ -82,16 +82,17 @@ func Run(ctx context.Context, io *iostreams.IOStreams, args ...string) int { cmd.SetArgs(args) cmd.SilenceErrors = true + cs := io.ColorScheme() + // configure help templates and helpers cobra.AddTemplateFuncs(template.FuncMap{ "wrapFlagUsages": wrapFlagUsages, "wrapText": wrapText, + "purple": cs.Purple, }) cmd.SetUsageTemplate(usageTemplate) cmd.SetHelpTemplate(helpTemplate) - cs := io.ColorScheme() - cmd, err = cmd.ExecuteContextC(ctx) if cmd != nil { @@ -239,7 +240,7 @@ const helpTemplate = `{{with (or .Long .Short)}}{{. | trimTrailingWhitespaces | {{end}}{{if or .Runnable .HasSubCommands}}{{.UsageString}}{{end}}` -// identical to the default cobra usage template, but utilizes wrapFlagUsages +// identical to the default cobra usage template, but utilizes wrapFlagUsages and adds purple color to command names // https://github.com/spf13/cobra/blob/fd865a44e3c48afeb6a6dbddadb8a5519173e029/command.go#L539-L568 const usageTemplate = `Usage:{{if .Runnable}} {{.UseLine}}{{end}}{{if .HasAvailableSubCommands}} @@ -252,13 +253,13 @@ Examples: {{.Example}}{{end}}{{if .HasAvailableSubCommands}}{{$cmds := .Commands}}{{if eq (len .Groups) 0}} Available Commands:{{range $cmds}}{{if (or .IsAvailableCommand (eq .Name "help"))}} - {{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{else}}{{range $group := .Groups}} + {{rpad .Name .NamePadding | purple}} {{.Short}}{{end}}{{end}}{{else}}{{range $group := .Groups}} {{.Title}}{{range $cmds}}{{if (and (eq .GroupID $group.ID) (or .IsAvailableCommand (eq .Name "help")))}} - {{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{end}}{{if not .AllChildCommandsHaveGroup}} + {{rpad .Name .NamePadding | purple}} {{.Short}}{{end}}{{end}}{{end}}{{if not .AllChildCommandsHaveGroup}} Additional Commands:{{range $cmds}}{{if (and (eq .GroupID "") (or .IsAvailableCommand (eq .Name "help")))}} - {{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{end}}{{end}}{{end}}{{if .HasAvailableLocalFlags}} + {{rpad .Name .NamePadding | purple}} {{.Short}}{{end}}{{end}}{{end}}{{end}}{{end}}{{if .HasAvailableLocalFlags}} Flags: {{wrapFlagUsages .LocalFlags | trimTrailingWhitespaces}}{{end}}{{if .HasAvailableInheritedFlags}} @@ -267,7 +268,7 @@ Global Flags: {{wrapFlagUsages .InheritedFlags | trimTrailingWhitespaces}}{{end}}{{if .HasHelpSubCommands}} Additional help topics:{{range .Commands}}{{if .IsAdditionalHelpTopicCommand}} - {{rpad .CommandPath .CommandPathPadding}} {{.Short}}{{end}}{{end}}{{end}}{{if .HasAvailableSubCommands}} + {{rpad .CommandPath .CommandPathPadding | purple}} {{.Short}}{{end}}{{end}}{{end}}{{if .HasAvailableSubCommands}} Use "{{.CommandPath}} [command] --help" for more information about a command.{{end}} ` diff --git a/internal/command/apps/list.go b/internal/command/apps/list.go index 58180c748c..bde1a48d36 100644 --- a/internal/command/apps/list.go +++ b/internal/command/apps/list.go @@ -64,7 +64,8 @@ func runList(ctx context.Context) (err error) { return } - out := iostreams.FromContext(ctx).Out + io := iostreams.FromContext(ctx) + out := io.Out if cfg.JSONOutput { _ = render.JSON(out, apps) @@ -72,11 +73,14 @@ func runList(ctx context.Context) (err error) { } verbose := flag.GetBool(ctx, "verbose") + colorize := io.ColorScheme() + termWidth := io.TerminalWidth() + const minWidthForFull = 100 // Minimum width needed for full table rows := make([][]string, 0, len(apps)) if silence { for _, app := range apps { - rows = append(rows, []string{app.Name}) + rows = append(rows, []string{colorize.Purple(app.Name)}) } _ = render.Table(out, "", rows) return @@ -87,19 +91,34 @@ func runList(ctx context.Context) (err error) { latestDeploy = format.RelativeTime(app.CurrentRelease.CreatedAt) } + appName := app.Name if !verbose && strings.HasPrefix(app.Name, "flyctl-interactive-shells-") { - app.Name = "(interactive shells app)" + appName = "(interactive shells app)" } - rows = append(rows, []string{ - app.Name, - app.Organization.Slug, - app.Status, - latestDeploy, - }) + if termWidth < minWidthForFull { + // Narrow terminal: show compact view without Latest Deploy + rows = append(rows, []string{ + colorize.Purple(appName), + app.Organization.Slug, + app.Status, + }) + } else { + // Wide terminal: show full table + rows = append(rows, []string{ + colorize.Purple(appName), + app.Organization.Slug, + app.Status, + latestDeploy, + }) + } } - _ = render.Table(out, "", rows, "Name", "Owner", "Status", "Latest Deploy") + if termWidth < minWidthForFull { + _ = render.Table(out, "", rows, "Name", "Owner", "Status") + } else { + _ = render.Table(out, "", rows, "Name", "Owner", "Status", "Latest Deploy") + } return } diff --git a/internal/command/machine/list.go b/internal/command/machine/list.go index 30eac61a77..71976ba3f6 100644 --- a/internal/command/machine/list.go +++ b/internal/command/machine/list.go @@ -50,10 +50,11 @@ func newList() *cobra.Command { func runMachineList(ctx context.Context) (err error) { var ( - appName = appconfig.NameFromContext(ctx) - io = iostreams.FromContext(ctx) - silence = flag.GetBool(ctx, "quiet") - cfg = config.FromContext(ctx) + appName = appconfig.NameFromContext(ctx) + io = iostreams.FromContext(ctx) + silence = flag.GetBool(ctx, "quiet") + cfg = config.FromContext(ctx) + colorize = io.ColorScheme() ) flapsClient, err := flapsutil.NewClientWithOptions(ctx, flaps.NewClientOpts{ @@ -86,97 +87,211 @@ func runMachineList(ctx context.Context) (err error) { if !silence { fmt.Fprintf(io.Out, "%d machines have been retrieved from app %s.\n%s\n\n", len(machines), appName, listOfMachinesLink) } + + termWidth := io.TerminalWidth() + const minWidthForTable = 180 // Width needed to show all columns in table format + if silence { for _, machine := range machines { - rows = append(rows, []string{machine.ID}) + rows = append(rows, []string{colorize.Purple(machine.ID)}) } _ = render.Table(io.Out, "", rows) } else { unreachableMachines := false - for _, machine := range machines { - var volName string - if machine.Config != nil && len(machine.Config.Mounts) > 0 { - volName = machine.Config.Mounts[0].Volume - } + if termWidth < minWidthForTable { + // Block layout - each machine as a card + for i, machine := range machines { + if i > 0 { + fmt.Fprintln(io.Out, "================================================================================") + fmt.Fprintln(io.Out) + } + + var volName string + if machine.Config != nil && len(machine.Config.Mounts) > 0 { + volName = machine.Config.Mounts[0].Volume + } - machineProcessGroup := "" - size := "" + machineProcessGroup := "" + size := "" - if machine.Config != nil { + if machine.Config != nil { + if processGroup := machine.ProcessGroup(); processGroup != "" { + machineProcessGroup = processGroup + } - if processGroup := machine.ProcessGroup(); processGroup != "" { - machineProcessGroup = processGroup + if machine.Config.Guest != nil { + size = fmt.Sprintf("%s:%dMB", machine.Config.Guest.ToSize(), machine.Config.Guest.MemoryMB) + } } - if machine.Config.Guest != nil { - size = fmt.Sprintf("%s:%dMB", machine.Config.Guest.ToSize(), machine.Config.Guest.MemoryMB) + note := "" + unreachable := machine.HostStatus != fly.HostStatusOk + if unreachable { + unreachableMachines = true + note = " *" } - } - note := "" - unreachable := machine.HostStatus != fly.HostStatusOk - if unreachable { - unreachableMachines = true - note = "*" - } + checksTotal := 0 + checksPassing := 0 + role := "" + for _, c := range machine.Checks { + checksTotal += 1 + if c.Status == "passing" { + checksPassing += 1 + } + if c.Name == "role" { + role = c.Output + } + } + + checksSummary := "-" + if checksTotal > 0 { + checksSummary = fmt.Sprintf("%d/%d", checksPassing, checksTotal) + } - checksTotal := 0 - checksPassing := 0 - role := "" - for _, c := range machine.Checks { - checksTotal += 1 + // Print machine block + fmt.Fprintf(io.Out, "Machine %s (%s)%s\n", + colorize.Purple(machine.ID), + machine.Name, + note) - if c.Status == "passing" { - checksPassing += 1 + // Line 1: State, Region, Checks + fmt.Fprintf(io.Out, " %s %-12s %s %-6s %s %s", + colorize.Yellow("State:"), + machine.State, + colorize.Yellow("Region:"), + machine.Region, + colorize.Yellow("Checks:"), + lo.Ternary(unreachable, "-", checksSummary)) + if role != "" { + fmt.Fprintf(io.Out, " %s %s", colorize.Yellow("Role:"), role) } + fmt.Fprintln(io.Out) - if c.Name == "role" { - role = c.Output + // Line 2: Image, IP, Volume + imageRef := lo.Ternary(unreachable, "-", machine.ImageRefWithVersion()) + ipAddr := lo.Ternary(unreachable, "-", machine.PrivateIP) + if volName == "" { + volName = "-" } + fmt.Fprintf(io.Out, " %s %-30s %s %-15s %s %s\n", + colorize.Yellow("Image:"), + imageRef, + colorize.Yellow("IP:"), + ipAddr, + colorize.Yellow("Volume:"), + volName) + + // Line 3: Created, Updated, Process, Size + created := lo.Ternary(unreachable, "-", machine.CreatedAt) + updated := lo.Ternary(unreachable, "-", machine.UpdatedAt) + if machineProcessGroup == "" { + machineProcessGroup = "-" + } + if size == "" { + size = "-" + } + fmt.Fprintf(io.Out, " %s %-12s %s %-12s %s %-8s %s %s\n", + colorize.Yellow("Created:"), + created, + colorize.Yellow("Updated:"), + updated, + colorize.Yellow("Process:"), + machineProcessGroup, + colorize.Yellow("Size:"), + size) } - checksSummary := "" - if checksTotal > 0 { - checksSummary = fmt.Sprintf("%d/%d", checksPassing, checksTotal) + if unreachableMachines { + fmt.Fprintln(io.Out) + fmt.Fprintln(io.Out, "* These Machines' hosts could not be reached.") } + } else { + // Table layout for wide terminals + for _, machine := range machines { + var volName string + if machine.Config != nil && len(machine.Config.Mounts) > 0 { + volName = machine.Config.Mounts[0].Volume + } - rows = append(rows, []string{ - machine.ID + note, - machine.Name, - machine.State, - lo.Ternary(unreachable, "", checksSummary), - machine.Region, - role, - lo.Ternary(unreachable, "", machine.ImageRefWithVersion()), - lo.Ternary(unreachable, "", machine.PrivateIP), - volName, - lo.Ternary(unreachable, "", machine.CreatedAt), - lo.Ternary(unreachable, "", machine.UpdatedAt), - machineProcessGroup, - size, - }) - } + machineProcessGroup := "" + size := "" - headers := []string{ - "ID", - "Name", - "State", - "Checks", - "Region", - "Role", - "Image", - "IP Address", - "Volume", - "Created", - "Last Updated", - "Process Group", - "Size", - } + if machine.Config != nil { + if processGroup := machine.ProcessGroup(); processGroup != "" { + machineProcessGroup = processGroup + } + + if machine.Config.Guest != nil { + size = fmt.Sprintf("%s:%dMB", machine.Config.Guest.ToSize(), machine.Config.Guest.MemoryMB) + } + } + + note := "" + unreachable := machine.HostStatus != fly.HostStatusOk + if unreachable { + unreachableMachines = true + note = "*" + } + + checksTotal := 0 + checksPassing := 0 + role := "" + for _, c := range machine.Checks { + checksTotal += 1 + if c.Status == "passing" { + checksPassing += 1 + } + if c.Name == "role" { + role = c.Output + } + } - _ = render.Table(io.Out, appName, rows, headers...) - if unreachableMachines { - fmt.Fprintln(io.Out, "* These Machines' hosts could not be reached.") + checksSummary := "" + if checksTotal > 0 { + checksSummary = fmt.Sprintf("%d/%d", checksPassing, checksTotal) + } + + machineID := colorize.Purple(machine.ID + note) + + rows = append(rows, []string{ + machineID, + machine.Name, + machine.State, + lo.Ternary(unreachable, "", checksSummary), + machine.Region, + role, + lo.Ternary(unreachable, "", machine.ImageRefWithVersion()), + lo.Ternary(unreachable, "", machine.PrivateIP), + volName, + lo.Ternary(unreachable, "", machine.CreatedAt), + lo.Ternary(unreachable, "", machine.UpdatedAt), + machineProcessGroup, + size, + }) + } + + headers := []string{ + "ID", + "Name", + "State", + "Checks", + "Region", + "Role", + "Image", + "IP Address", + "Volume", + "Created", + "Last Updated", + "Process Group", + "Size", + } + _ = render.Table(io.Out, appName, rows, headers...) + + if unreachableMachines { + fmt.Fprintln(io.Out, "* These Machines' hosts could not be reached.") + } } } return nil diff --git a/internal/command/mpg/list.go b/internal/command/mpg/list.go index 4c4bb4b781..2f40dda5a8 100644 --- a/internal/command/mpg/list.go +++ b/internal/command/mpg/list.go @@ -51,7 +51,9 @@ func runList(ctx context.Context) error { } cfg := config.FromContext(ctx) - out := iostreams.FromContext(ctx).Out + io := iostreams.FromContext(ctx) + out := io.Out + colorize := io.ColorScheme() org, err := orgs.OrgFromFlagOrSelect(ctx) if err != nil { @@ -87,17 +89,37 @@ func runList(ctx context.Context) error { return render.JSON(out, clusters.Data) } + // Check terminal width to decide on display format + termWidth := io.TerminalWidth() + const minWidthForFull = 100 // Minimum width needed for full table + rows := make([][]string, 0, len(clusters.Data)) for _, cluster := range clusters.Data { - rows = append(rows, []string{ - cluster.Id, - cluster.Name, - cluster.Organization.Slug, - cluster.Region, - cluster.Status, - cluster.Plan, - }) + clusterID := colorize.Purple(cluster.Id) + + if termWidth < minWidthForFull { + // Narrow terminal: show compact view with essential info only + rows = append(rows, []string{ + clusterID, + cluster.Name, + cluster.Region, + cluster.Status, + }) + } else { + // Wide terminal: show full table + rows = append(rows, []string{ + clusterID, + cluster.Name, + cluster.Organization.Slug, + cluster.Region, + cluster.Status, + cluster.Plan, + }) + } } + if termWidth < minWidthForFull { + return render.Table(out, "", rows, "ID", "Name", "Region", "Status") + } return render.Table(out, "", rows, "ID", "Name", "Org", "Region", "Status", "Plan") }