package cli import ( "encoding/json" "fmt" "io" "os" "sort" "strings" "text/tabwriter" "banger/internal/api" "banger/internal/cli/style" "banger/internal/model" "banger/internal/system" ) // anyWriter is the minimal writer surface every printer needs. Split // out from io.Writer because some of our callers already hold a // tabwriter/bytes.Buffer by value. type anyWriter interface { Write(p []byte) (n int, err error) } // -- small helpers -------------------------------------------------- func humanSize(bytes int64) string { if bytes <= 0 { return "-" } const ( kib = 1024 mib = 1024 * kib gib = 1024 * mib ) switch { case bytes >= gib: return fmt.Sprintf("%.1fGiB", float64(bytes)/float64(gib)) case bytes >= mib: return fmt.Sprintf("%.1fMiB", float64(bytes)/float64(mib)) case bytes >= kib: return fmt.Sprintf("%.1fKiB", float64(bytes)/float64(kib)) default: return fmt.Sprintf("%dB", bytes) } } func dashIfEmpty(s string) string { if strings.TrimSpace(s) == "" { return "-" } return s } func emptyDash(value string) string { value = strings.TrimSpace(value) if value == "" { return "-" } return value } // -- generic printers ----------------------------------------------- func printJSON(out anyWriter, v any) error { data, err := json.MarshalIndent(v, "", " ") if err != nil { return err } _, err = fmt.Fprintln(out, string(data)) return err } // -- VM printers ---------------------------------------------------- func printVMSummary(out anyWriter, vm model.VMRecord) error { _, err := fmt.Fprintf( out, "%s\t%s\t%s\t%s\t%s\t%s\n", shortID(vm.ID), vm.Name, vm.State, vm.Runtime.GuestIP, model.FormatSizeBytes(vm.Spec.WorkDiskSizeBytes), vm.Runtime.DNSName, ) return err } func printVMIDList(out anyWriter, vms []model.VMRecord) error { for _, vm := range vms { if _, err := fmt.Fprintln(out, vm.ID); err != nil { return err } } return nil } func printVMListTable(out anyWriter, vms []model.VMRecord, imageNames map[string]string) error { w := tabwriter.NewWriter(out, 0, 8, 2, ' ', 0) if _, err := fmt.Fprintln(w, "ID\tNAME\tSTATE\tIMAGE\tIP\tVCPU\tMEM\tDISK\tCREATED"); err != nil { return err } for _, vm := range vms { if _, err := fmt.Fprintf( w, "%s\t%s\t%s\t%s\t%s\t%d\t%d MiB\t%s\t%s\n", shortID(vm.ID), vm.Name, vm.State, vmImageLabel(vm.ImageID, imageNames), vm.Runtime.GuestIP, vm.Spec.VCPUCount, vm.Spec.MemoryMiB, model.FormatSizeBytes(vm.Spec.WorkDiskSizeBytes), relativeTime(vm.CreatedAt), ); err != nil { return err } } return w.Flush() } func printVMPortsTable(out anyWriter, result api.VMPortsResult) error { type portRow struct { Proto string Endpoint string Process string Command string Port int } rows := make([]portRow, 0, len(result.Ports)) for _, port := range result.Ports { rows = append(rows, portRow{ Proto: port.Proto, Endpoint: port.Endpoint, Process: port.Process, Command: port.Command, Port: port.Port, }) } sort.Slice(rows, func(i, j int) bool { if rows[i].Proto != rows[j].Proto { return rows[i].Proto < rows[j].Proto } if rows[i].Port != rows[j].Port { return rows[i].Port < rows[j].Port } if rows[i].Process != rows[j].Process { return rows[i].Process < rows[j].Process } return rows[i].Command < rows[j].Command }) if len(rows) == 0 { return nil } w := tabwriter.NewWriter(out, 0, 8, 2, ' ', 0) if _, err := fmt.Fprintln(w, "PROTO\tENDPOINT\tPROCESS\tCOMMAND"); err != nil { return err } for _, row := range rows { if _, err := fmt.Fprintf( w, "%s\t%s\t%s\t%s\n", row.Proto, emptyDash(row.Endpoint), emptyDash(row.Process), emptyDash(row.Command), ); err != nil { return err } } return w.Flush() } // -- image printers ------------------------------------------------- func printImageSummary(out anyWriter, image model.Image) error { _, err := fmt.Fprintf(out, "%s\t%s\t%t\t%s\n", shortID(image.ID), image.Name, image.Managed, image.RootfsPath) return err } func imageNameIndex(images []model.Image) map[string]string { index := make(map[string]string, len(images)) for _, image := range images { index[image.ID] = image.Name } return index } func vmImageLabel(imageID string, imageNames map[string]string) string { if name := strings.TrimSpace(imageNames[imageID]); name != "" { return name } return shortID(imageID) } func printImageListTable(out anyWriter, images []model.Image) error { w := tabwriter.NewWriter(out, 0, 8, 2, ' ', 0) if _, err := fmt.Fprintln(w, "ID\tNAME\tMANAGED\tROOTFS SIZE\tCREATED"); err != nil { return err } for _, image := range images { if _, err := fmt.Fprintf( w, "%s\t%s\t%t\t%s\t%s\n", shortID(image.ID), image.Name, image.Managed, rootfsSizeLabel(image.RootfsPath), relativeTime(image.CreatedAt), ); err != nil { return err } } return w.Flush() } func rootfsSizeLabel(path string) string { info, err := os.Stat(path) if err != nil { return "-" } if info.Size() <= 0 { return "0" } return model.FormatSizeBytes(info.Size()) } // -- kernel printers ------------------------------------------------ func printKernelListTable(out anyWriter, entries []api.KernelEntry) error { w := tabwriter.NewWriter(out, 0, 8, 2, ' ', 0) if _, err := fmt.Fprintln(w, "NAME\tDISTRO\tARCH\tKERNEL\tIMPORTED"); err != nil { return err } for _, entry := range entries { if _, err := fmt.Fprintf( w, "%s\t%s\t%s\t%s\t%s\n", entry.Name, dashIfEmpty(entry.Distro), dashIfEmpty(entry.Arch), dashIfEmpty(entry.KernelVersion), dashIfEmpty(entry.ImportedAt), ); err != nil { return err } } return w.Flush() } func printKernelCatalogTable(out anyWriter, entries []api.KernelCatalogEntry) error { w := tabwriter.NewWriter(out, 0, 8, 2, ' ', 0) if _, err := fmt.Fprintln(w, "NAME\tDISTRO\tARCH\tKERNEL\tSIZE\tSTATE"); err != nil { return err } for _, entry := range entries { state := "available" if entry.Pulled { state = "pulled" } if _, err := fmt.Fprintf( w, "%s\t%s\t%s\t%s\t%s\t%s\n", entry.Name, dashIfEmpty(entry.Distro), dashIfEmpty(entry.Arch), dashIfEmpty(entry.KernelVersion), humanSize(entry.SizeBytes), state, ); err != nil { return err } } return w.Flush() } // -- doctor printer ------------------------------------------------- func printDoctorReport(out anyWriter, report system.Report) error { colorWriter, _ := out.(io.Writer) for _, check := range report.Checks { status := strings.ToUpper(string(check.Status)) if colorWriter != nil { switch check.Status { case system.CheckStatusPass: status = style.Pass(colorWriter, status) case system.CheckStatusFail: status = style.Fail(colorWriter, status) case system.CheckStatusWarn: status = style.Warn(colorWriter, status) } } if _, err := fmt.Fprintf(out, "%s\t%s\n", status, check.Name); err != nil { return err } for _, detail := range check.Details { if _, err := fmt.Fprintf(out, " - %s\n", detail); err != nil { return err } } } return nil }