mirror of https://github.com/docker/cli.git
cmd/image/tree: refactor
Signed-off-by: Laura Brehm <laurabrehm@hey.com>
This commit is contained in:
parent
da9e984231
commit
6c86dcc219
|
@ -4,12 +4,9 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
|
||||||
"unicode/utf8"
|
|
||||||
|
|
||||||
"github.com/containerd/platforms"
|
"github.com/containerd/platforms"
|
||||||
"github.com/docker/cli/cli/command"
|
"github.com/docker/cli/cli/command"
|
||||||
"github.com/docker/cli/cli/streams"
|
|
||||||
"github.com/docker/docker/api/types/filters"
|
"github.com/docker/docker/api/types/filters"
|
||||||
imagetypes "github.com/docker/docker/api/types/image"
|
imagetypes "github.com/docker/docker/api/types/image"
|
||||||
"github.com/docker/docker/pkg/stringid"
|
"github.com/docker/docker/pkg/stringid"
|
||||||
|
@ -22,13 +19,6 @@ type treeOptions struct {
|
||||||
filters filters.Args
|
filters filters.Args
|
||||||
}
|
}
|
||||||
|
|
||||||
type treeView struct {
|
|
||||||
images []topImage
|
|
||||||
|
|
||||||
// imageSpacing indicates whether there should be extra spacing between images.
|
|
||||||
imageSpacing bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func runTree(ctx context.Context, dockerCLI command.Cli, opts treeOptions) error {
|
func runTree(ctx context.Context, dockerCLI command.Cli, opts treeOptions) error {
|
||||||
images, err := dockerCLI.Client().ImageList(ctx, imagetypes.ListOptions{
|
images, err := dockerCLI.Client().ImageList(ctx, imagetypes.ListOptions{
|
||||||
All: opts.all,
|
All: opts.all,
|
||||||
|
@ -39,28 +29,73 @@ func runTree(ctx context.Context, dockerCLI command.Cli, opts treeOptions) error
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
view := treeView{
|
warningColor := aec.LightYellowF
|
||||||
images: make([]topImage, 0, len(images)),
|
if !dockerCLI.Out().IsTerminal() {
|
||||||
|
warningColor = noColor{}
|
||||||
}
|
}
|
||||||
|
_, _ = fmt.Fprintln(dockerCLI.Out(), warningColor.Apply("WARNING: This is an experimental feature. The output may change and shouldn't be depended on."))
|
||||||
|
_, _ = fmt.Fprintln(dockerCLI.Out(), "")
|
||||||
|
|
||||||
|
out := dockerCLI.Out()
|
||||||
|
_, width := out.GetTtySize()
|
||||||
|
if width == 0 {
|
||||||
|
width = 80
|
||||||
|
}
|
||||||
|
if width < 20 {
|
||||||
|
width = 20
|
||||||
|
}
|
||||||
|
|
||||||
|
headerColor := aec.NewBuilder(aec.DefaultF, aec.Bold).ANSI
|
||||||
|
topNameColor := aec.NewBuilder(aec.BlueF, aec.Bold).ANSI
|
||||||
|
normalColor := aec.NewBuilder(aec.DefaultF).ANSI
|
||||||
|
greenColor := aec.NewBuilder(aec.GreenF).ANSI
|
||||||
|
untaggedColor := aec.NewBuilder(aec.Faint).ANSI
|
||||||
|
if !out.IsTerminal() {
|
||||||
|
headerColor = noColor{}
|
||||||
|
topNameColor = noColor{}
|
||||||
|
normalColor = noColor{}
|
||||||
|
greenColor = noColor{}
|
||||||
|
untaggedColor = noColor{}
|
||||||
|
}
|
||||||
|
|
||||||
|
columns := buildTableColumns(int(width), greenColor)
|
||||||
|
imageRows, anyImageHasChildren := buildTableRows(images)
|
||||||
|
columns = formatColumnsForOutput(int(width), columns, imageRows)
|
||||||
|
table := imageTreeTable{
|
||||||
|
columns: columns,
|
||||||
|
headerColor: headerColor,
|
||||||
|
indexNameColor: topNameColor,
|
||||||
|
untaggedColor: untaggedColor,
|
||||||
|
normalColor: normalColor,
|
||||||
|
spacing: anyImageHasChildren,
|
||||||
|
}
|
||||||
|
|
||||||
|
table.printTable(out, imageRows)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildTableRows(images []imagetypes.Summary) ([]ImageIndexRow, bool) {
|
||||||
|
imageRows := make([]ImageIndexRow, 0, len(images))
|
||||||
|
var hasChildren bool
|
||||||
for _, img := range images {
|
for _, img := range images {
|
||||||
details := imageDetails{
|
details := rowDetails{
|
||||||
ID: img.ID,
|
ID: img.ID,
|
||||||
DiskUsage: units.HumanSizeWithPrecision(float64(img.Size), 3),
|
DiskUsage: units.HumanSizeWithPrecision(float64(img.Size), 3),
|
||||||
InUse: img.Containers > 0,
|
InUse: img.Containers > 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
var totalContent int64
|
var totalContent int64
|
||||||
children := make([]subImage, 0, len(img.Manifests))
|
children := make([]ImageManifestRow, 0, len(img.Manifests))
|
||||||
for _, im := range img.Manifests {
|
for _, im := range img.Manifests {
|
||||||
if im.Kind != imagetypes.ManifestKindImage {
|
if im.Kind != imagetypes.ManifestKindImage {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
im := im
|
im := im
|
||||||
sub := subImage{
|
sub := ImageManifestRow{
|
||||||
Platform: platforms.Format(im.ImageData.Platform),
|
Platform: platforms.Format(im.ImageData.Platform),
|
||||||
Available: im.Available,
|
Available: im.Available,
|
||||||
Details: imageDetails{
|
Details: rowDetails{
|
||||||
ID: im.ID,
|
ID: im.ID,
|
||||||
DiskUsage: units.HumanSizeWithPrecision(float64(im.Size.Total), 3),
|
DiskUsage: units.HumanSizeWithPrecision(float64(im.Size.Total), 3),
|
||||||
InUse: len(im.ImageData.Containers) > 0,
|
InUse: len(im.ImageData.Containers) > 0,
|
||||||
|
@ -77,12 +112,12 @@ func runTree(ctx context.Context, dockerCLI command.Cli, opts treeOptions) error
|
||||||
children = append(children, sub)
|
children = append(children, sub)
|
||||||
|
|
||||||
// Add extra spacing between images if there's at least one entry with children.
|
// Add extra spacing between images if there's at least one entry with children.
|
||||||
view.imageSpacing = true
|
hasChildren = true
|
||||||
}
|
}
|
||||||
|
|
||||||
details.ContentSize = units.HumanSizeWithPrecision(float64(totalContent), 3)
|
details.ContentSize = units.HumanSizeWithPrecision(float64(totalContent), 3)
|
||||||
|
|
||||||
view.images = append(view.images, topImage{
|
imageRows = append(imageRows, ImageIndexRow{
|
||||||
Names: img.RepoTags,
|
Names: img.RepoTags,
|
||||||
Details: details,
|
Details: details,
|
||||||
Children: children,
|
Children: children,
|
||||||
|
@ -90,64 +125,14 @@ func runTree(ctx context.Context, dockerCLI command.Cli, opts treeOptions) error
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
sort.Slice(view.images, func(i, j int) bool {
|
sort.Slice(imageRows, func(i, j int) bool {
|
||||||
return view.images[i].created > view.images[j].created
|
return imageRows[i].created > imageRows[j].created
|
||||||
})
|
})
|
||||||
|
|
||||||
return printImageTree(dockerCLI, view)
|
return imageRows, hasChildren
|
||||||
}
|
}
|
||||||
|
|
||||||
type imageDetails struct {
|
func buildTableColumns(ttyWidth int, usedColumnColor aec.ANSI) []imgColumn {
|
||||||
ID string
|
|
||||||
DiskUsage string
|
|
||||||
InUse bool
|
|
||||||
ContentSize string
|
|
||||||
}
|
|
||||||
|
|
||||||
type topImage struct {
|
|
||||||
Names []string
|
|
||||||
Details imageDetails
|
|
||||||
Children []subImage
|
|
||||||
|
|
||||||
created int64
|
|
||||||
}
|
|
||||||
|
|
||||||
type subImage struct {
|
|
||||||
Platform string
|
|
||||||
Available bool
|
|
||||||
Details imageDetails
|
|
||||||
}
|
|
||||||
|
|
||||||
const columnSpacing = 3
|
|
||||||
|
|
||||||
func printImageTree(dockerCLI command.Cli, view treeView) error {
|
|
||||||
out := dockerCLI.Out()
|
|
||||||
_, width := out.GetTtySize()
|
|
||||||
if width == 0 {
|
|
||||||
width = 80
|
|
||||||
}
|
|
||||||
if width < 20 {
|
|
||||||
width = 20
|
|
||||||
}
|
|
||||||
|
|
||||||
warningColor := aec.LightYellowF
|
|
||||||
headerColor := aec.NewBuilder(aec.DefaultF, aec.Bold).ANSI
|
|
||||||
topNameColor := aec.NewBuilder(aec.BlueF, aec.Bold).ANSI
|
|
||||||
normalColor := aec.NewBuilder(aec.DefaultF).ANSI
|
|
||||||
greenColor := aec.NewBuilder(aec.GreenF).ANSI
|
|
||||||
untaggedColor := aec.NewBuilder(aec.Faint).ANSI
|
|
||||||
if !out.IsTerminal() {
|
|
||||||
headerColor = noColor{}
|
|
||||||
topNameColor = noColor{}
|
|
||||||
normalColor = noColor{}
|
|
||||||
greenColor = noColor{}
|
|
||||||
warningColor = noColor{}
|
|
||||||
untaggedColor = noColor{}
|
|
||||||
}
|
|
||||||
|
|
||||||
_, _ = fmt.Fprintln(out, warningColor.Apply("WARNING: This is an experimental feature. The output may change and shouldn't be depended on."))
|
|
||||||
_, _ = fmt.Fprintln(out, "")
|
|
||||||
|
|
||||||
columns := []imgColumn{
|
columns := []imgColumn{
|
||||||
{
|
{
|
||||||
Title: "Image",
|
Title: "Image",
|
||||||
|
@ -158,7 +143,7 @@ func printImageTree(dockerCLI command.Cli, view treeView) error {
|
||||||
Title: "ID",
|
Title: "ID",
|
||||||
Align: alignLeft,
|
Align: alignLeft,
|
||||||
Width: 12,
|
Width: 12,
|
||||||
DetailsValue: func(d *imageDetails) string {
|
DetailsValue: func(d *rowDetails) string {
|
||||||
return stringid.TruncateID(d.ID)
|
return stringid.TruncateID(d.ID)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -166,7 +151,7 @@ func printImageTree(dockerCLI command.Cli, view treeView) error {
|
||||||
Title: "Disk usage",
|
Title: "Disk usage",
|
||||||
Align: alignRight,
|
Align: alignRight,
|
||||||
Width: 10,
|
Width: 10,
|
||||||
DetailsValue: func(d *imageDetails) string {
|
DetailsValue: func(d *rowDetails) string {
|
||||||
return d.DiskUsage
|
return d.DiskUsage
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -174,7 +159,7 @@ func printImageTree(dockerCLI command.Cli, view treeView) error {
|
||||||
Title: "Content size",
|
Title: "Content size",
|
||||||
Align: alignRight,
|
Align: alignRight,
|
||||||
Width: 12,
|
Width: 12,
|
||||||
DetailsValue: func(d *imageDetails) string {
|
DetailsValue: func(d *rowDetails) string {
|
||||||
return d.ContentSize
|
return d.ContentSize
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -182,8 +167,8 @@ func printImageTree(dockerCLI command.Cli, view treeView) error {
|
||||||
Title: "In Use",
|
Title: "In Use",
|
||||||
Align: alignCenter,
|
Align: alignCenter,
|
||||||
Width: 6,
|
Width: 6,
|
||||||
Color: &greenColor,
|
Color: &usedColumnColor,
|
||||||
DetailsValue: func(d *imageDetails) string {
|
DetailsValue: func(d *rowDetails) string {
|
||||||
if d.InUse {
|
if d.InUse {
|
||||||
return "✔"
|
return "✔"
|
||||||
}
|
}
|
||||||
|
@ -192,202 +177,5 @@ func printImageTree(dockerCLI command.Cli, view treeView) error {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
nameWidth := int(width)
|
return columns
|
||||||
for idx, h := range columns {
|
|
||||||
if h.Width == 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
d := h.Width
|
|
||||||
if idx > 0 {
|
|
||||||
d += columnSpacing
|
|
||||||
}
|
|
||||||
// If the first column gets too short, remove remaining columns
|
|
||||||
if nameWidth-d < 12 {
|
|
||||||
columns = columns[:idx]
|
|
||||||
break
|
|
||||||
}
|
|
||||||
nameWidth -= d
|
|
||||||
}
|
|
||||||
|
|
||||||
images := view.images
|
|
||||||
// Try to make the first column as narrow as possible
|
|
||||||
widest := widestFirstColumnValue(columns, images)
|
|
||||||
if nameWidth > widest {
|
|
||||||
nameWidth = widest
|
|
||||||
}
|
|
||||||
columns[0].Width = nameWidth
|
|
||||||
|
|
||||||
// Print columns
|
|
||||||
for i, h := range columns {
|
|
||||||
if i > 0 {
|
|
||||||
_, _ = fmt.Fprint(out, strings.Repeat(" ", columnSpacing))
|
|
||||||
}
|
|
||||||
|
|
||||||
_, _ = fmt.Fprint(out, h.Print(headerColor, strings.ToUpper(h.Title)))
|
|
||||||
}
|
|
||||||
|
|
||||||
_, _ = fmt.Fprintln(out)
|
|
||||||
|
|
||||||
// Print images
|
|
||||||
for _, img := range images {
|
|
||||||
printNames(out, columns, img, topNameColor, untaggedColor)
|
|
||||||
printDetails(out, columns, normalColor, img.Details)
|
|
||||||
|
|
||||||
if len(img.Children) > 0 || view.imageSpacing {
|
|
||||||
_, _ = fmt.Fprintln(out)
|
|
||||||
}
|
|
||||||
printChildren(out, columns, img, normalColor)
|
|
||||||
_, _ = fmt.Fprintln(out)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func printDetails(out *streams.Out, headers []imgColumn, defaultColor aec.ANSI, details imageDetails) {
|
|
||||||
for _, h := range headers {
|
|
||||||
if h.DetailsValue == nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
_, _ = fmt.Fprint(out, strings.Repeat(" ", columnSpacing))
|
|
||||||
clr := defaultColor
|
|
||||||
if h.Color != nil {
|
|
||||||
clr = *h.Color
|
|
||||||
}
|
|
||||||
val := h.DetailsValue(&details)
|
|
||||||
_, _ = fmt.Fprint(out, h.Print(clr, val))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func printChildren(out *streams.Out, headers []imgColumn, img topImage, normalColor aec.ANSI) {
|
|
||||||
for idx, sub := range img.Children {
|
|
||||||
clr := normalColor
|
|
||||||
if !sub.Available {
|
|
||||||
clr = normalColor.With(aec.Faint)
|
|
||||||
}
|
|
||||||
|
|
||||||
if idx != len(img.Children)-1 {
|
|
||||||
_, _ = fmt.Fprint(out, headers[0].Print(clr, "├─ "+sub.Platform))
|
|
||||||
} else {
|
|
||||||
_, _ = fmt.Fprint(out, headers[0].Print(clr, "└─ "+sub.Platform))
|
|
||||||
}
|
|
||||||
|
|
||||||
printDetails(out, headers, clr, sub.Details)
|
|
||||||
_, _ = fmt.Fprintln(out, "")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func printNames(out *streams.Out, headers []imgColumn, img topImage, color, untaggedColor aec.ANSI) {
|
|
||||||
if len(img.Names) == 0 {
|
|
||||||
_, _ = fmt.Fprint(out, headers[0].Print(untaggedColor, "<untagged>"))
|
|
||||||
}
|
|
||||||
|
|
||||||
for nameIdx, name := range img.Names {
|
|
||||||
if nameIdx != 0 {
|
|
||||||
_, _ = fmt.Fprintln(out, "")
|
|
||||||
}
|
|
||||||
_, _ = fmt.Fprint(out, headers[0].Print(color, name))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type alignment int
|
|
||||||
|
|
||||||
const (
|
|
||||||
alignLeft alignment = iota
|
|
||||||
alignCenter
|
|
||||||
alignRight
|
|
||||||
)
|
|
||||||
|
|
||||||
type imgColumn struct {
|
|
||||||
Title string
|
|
||||||
Width int
|
|
||||||
Align alignment
|
|
||||||
|
|
||||||
DetailsValue func(*imageDetails) string
|
|
||||||
Color *aec.ANSI
|
|
||||||
}
|
|
||||||
|
|
||||||
func truncateRunes(s string, length int) string {
|
|
||||||
runes := []rune(s)
|
|
||||||
if len(runes) > length {
|
|
||||||
return string(runes[:length-3]) + "..."
|
|
||||||
}
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h imgColumn) Print(clr aec.ANSI, s string) string {
|
|
||||||
switch h.Align {
|
|
||||||
case alignCenter:
|
|
||||||
return h.PrintC(clr, s)
|
|
||||||
case alignRight:
|
|
||||||
return h.PrintR(clr, s)
|
|
||||||
case alignLeft:
|
|
||||||
}
|
|
||||||
return h.PrintL(clr, s)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h imgColumn) PrintC(clr aec.ANSI, s string) string {
|
|
||||||
ln := utf8.RuneCountInString(s)
|
|
||||||
|
|
||||||
if ln > h.Width {
|
|
||||||
return clr.Apply(truncateRunes(s, h.Width))
|
|
||||||
}
|
|
||||||
|
|
||||||
fill := h.Width - ln
|
|
||||||
|
|
||||||
l := fill / 2
|
|
||||||
r := fill - l
|
|
||||||
|
|
||||||
return strings.Repeat(" ", l) + clr.Apply(s) + strings.Repeat(" ", r)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h imgColumn) PrintL(clr aec.ANSI, s string) string {
|
|
||||||
ln := utf8.RuneCountInString(s)
|
|
||||||
if ln > h.Width {
|
|
||||||
return clr.Apply(truncateRunes(s, h.Width))
|
|
||||||
}
|
|
||||||
|
|
||||||
return clr.Apply(s) + strings.Repeat(" ", h.Width-ln)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h imgColumn) PrintR(clr aec.ANSI, s string) string {
|
|
||||||
ln := utf8.RuneCountInString(s)
|
|
||||||
if ln > h.Width {
|
|
||||||
return clr.Apply(truncateRunes(s, h.Width))
|
|
||||||
}
|
|
||||||
|
|
||||||
return strings.Repeat(" ", h.Width-ln) + clr.Apply(s)
|
|
||||||
}
|
|
||||||
|
|
||||||
type noColor struct{}
|
|
||||||
|
|
||||||
func (a noColor) With(_ ...aec.ANSI) aec.ANSI {
|
|
||||||
return a
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a noColor) Apply(s string) string {
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a noColor) String() string {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
// widestFirstColumnValue calculates the width needed to fully display the image names and platforms.
|
|
||||||
func widestFirstColumnValue(headers []imgColumn, images []topImage) int {
|
|
||||||
width := len(headers[0].Title)
|
|
||||||
for _, img := range images {
|
|
||||||
for _, name := range img.Names {
|
|
||||||
if len(name) > width {
|
|
||||||
width = len(name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for _, sub := range img.Children {
|
|
||||||
pl := len(sub.Platform) + len("└─ ")
|
|
||||||
if pl > width {
|
|
||||||
width = pl
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return width
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,265 @@
|
||||||
|
package image
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
|
"unicode/utf8"
|
||||||
|
|
||||||
|
"github.com/morikuni/aec"
|
||||||
|
)
|
||||||
|
|
||||||
|
type imageTreeTable struct {
|
||||||
|
columns []imgColumn
|
||||||
|
|
||||||
|
headerColor aec.ANSI
|
||||||
|
indexNameColor aec.ANSI
|
||||||
|
untaggedColor aec.ANSI
|
||||||
|
normalColor aec.ANSI
|
||||||
|
highlightColor aec.ANSI
|
||||||
|
|
||||||
|
spacing bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type ImageIndexRow struct {
|
||||||
|
Names []string
|
||||||
|
Details rowDetails
|
||||||
|
Children []ImageManifestRow
|
||||||
|
|
||||||
|
created int64
|
||||||
|
}
|
||||||
|
|
||||||
|
type ImageManifestRow struct {
|
||||||
|
Platform string
|
||||||
|
Available bool
|
||||||
|
Highlight bool
|
||||||
|
Details rowDetails
|
||||||
|
}
|
||||||
|
|
||||||
|
// rowDetails is used by both ImageIndexRow and ImageManifestRow
|
||||||
|
type rowDetails struct {
|
||||||
|
ID string
|
||||||
|
DiskUsage string
|
||||||
|
InUse bool
|
||||||
|
ContentSize string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *imageTreeTable) printTable(out io.Writer, imgs []ImageIndexRow) {
|
||||||
|
t.printHeaders(out)
|
||||||
|
for _, img := range imgs {
|
||||||
|
t.printIndex(out, img)
|
||||||
|
_, _ = fmt.Fprintln(out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *imageTreeTable) printHeaders(out io.Writer) {
|
||||||
|
for i, h := range t.columns {
|
||||||
|
if i > 0 {
|
||||||
|
_, _ = fmt.Fprint(out, strings.Repeat(" ", columnSpacing))
|
||||||
|
}
|
||||||
|
|
||||||
|
_, _ = fmt.Fprint(out, h.Print(t.headerColor, strings.ToUpper(h.Title)))
|
||||||
|
}
|
||||||
|
|
||||||
|
_, _ = fmt.Fprintln(out)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *imageTreeTable) printIndex(out io.Writer, img ImageIndexRow) {
|
||||||
|
// print the names for the index
|
||||||
|
printNames(out, t.columns, img, t.indexNameColor, t.untaggedColor)
|
||||||
|
// print the rest of the columns/details for the header
|
||||||
|
printDetails(out, t.columns, t.normalColor, img.Details)
|
||||||
|
|
||||||
|
// print the manifest rows, with their details
|
||||||
|
if len(img.Children) > 0 || t.spacing {
|
||||||
|
_, _ = fmt.Fprintln(out)
|
||||||
|
}
|
||||||
|
|
||||||
|
printChildren(out, t.columns, img, t.normalColor)
|
||||||
|
}
|
||||||
|
|
||||||
|
func printDetails(out io.Writer, headers []imgColumn, defaultColor aec.ANSI, details rowDetails) {
|
||||||
|
for _, h := range headers {
|
||||||
|
if h.DetailsValue == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
_, _ = fmt.Fprint(out, strings.Repeat(" ", columnSpacing))
|
||||||
|
clr := defaultColor
|
||||||
|
if h.Color != nil {
|
||||||
|
clr = *h.Color
|
||||||
|
}
|
||||||
|
val := h.DetailsValue(&details)
|
||||||
|
_, _ = fmt.Fprint(out, h.Print(clr, val))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func printChildren(out io.Writer, headers []imgColumn, img ImageIndexRow, normalColor aec.ANSI) {
|
||||||
|
for idx, sub := range img.Children {
|
||||||
|
clr := normalColor
|
||||||
|
if !sub.Available {
|
||||||
|
clr = normalColor.With(aec.Faint)
|
||||||
|
}
|
||||||
|
if sub.Highlight {
|
||||||
|
clr = normalColor.With(aec.Bold)
|
||||||
|
}
|
||||||
|
|
||||||
|
if idx != len(img.Children)-1 {
|
||||||
|
_, _ = fmt.Fprint(out, headers[0].Print(clr, "├─ "+sub.Platform))
|
||||||
|
} else {
|
||||||
|
_, _ = fmt.Fprint(out, headers[0].Print(clr, "└─ "+sub.Platform))
|
||||||
|
}
|
||||||
|
|
||||||
|
printDetails(out, headers, clr, sub.Details)
|
||||||
|
_, _ = fmt.Fprintln(out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func printNames(out io.Writer, headers []imgColumn, img ImageIndexRow, color, untaggedColor aec.ANSI) {
|
||||||
|
if len(img.Names) == 0 {
|
||||||
|
_, _ = fmt.Fprint(out, headers[0].Print(untaggedColor, "<untagged>"))
|
||||||
|
}
|
||||||
|
|
||||||
|
for nameIdx, name := range img.Names {
|
||||||
|
if nameIdx != 0 {
|
||||||
|
_, _ = fmt.Fprintln(out)
|
||||||
|
}
|
||||||
|
_, _ = fmt.Fprint(out, headers[0].Print(color, name))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type alignment int
|
||||||
|
|
||||||
|
const (
|
||||||
|
alignLeft alignment = iota
|
||||||
|
alignCenter
|
||||||
|
alignRight
|
||||||
|
)
|
||||||
|
|
||||||
|
type imgColumn struct {
|
||||||
|
Title string
|
||||||
|
Width int
|
||||||
|
Align alignment
|
||||||
|
|
||||||
|
DetailsValue func(*rowDetails) string
|
||||||
|
Color *aec.ANSI
|
||||||
|
}
|
||||||
|
|
||||||
|
// formatColumnsForOutput resizes the table columns for the provided tty
|
||||||
|
// size. The first column is made as narrow as possible, and columns are
|
||||||
|
// removed from the table output if the tty is not wide enough to
|
||||||
|
// accomodate the entire table.
|
||||||
|
func formatColumnsForOutput(ttyWidth int, columns []imgColumn, images []ImageIndexRow) []imgColumn {
|
||||||
|
nameWidth := ttyWidth
|
||||||
|
for idx, h := range columns {
|
||||||
|
if h.Width == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
d := h.Width
|
||||||
|
if idx > 0 {
|
||||||
|
d += columnSpacing
|
||||||
|
}
|
||||||
|
// If the first column gets too short, remove remaining columns
|
||||||
|
if nameWidth-d < 12 {
|
||||||
|
columns = columns[:idx]
|
||||||
|
break
|
||||||
|
}
|
||||||
|
nameWidth -= d
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to make the first column as narrow as possible
|
||||||
|
widest := widestFirstColumnValue(columns, images)
|
||||||
|
if nameWidth > widest {
|
||||||
|
nameWidth = widest
|
||||||
|
}
|
||||||
|
columns[0].Width = nameWidth
|
||||||
|
|
||||||
|
return columns
|
||||||
|
}
|
||||||
|
|
||||||
|
const columnSpacing = 3
|
||||||
|
|
||||||
|
func truncateRunes(s string, length int) string {
|
||||||
|
runes := []rune(s)
|
||||||
|
if len(runes) > length {
|
||||||
|
return string(runes[:length-3]) + "..."
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h imgColumn) Print(clr aec.ANSI, s string) string {
|
||||||
|
switch h.Align {
|
||||||
|
case alignCenter:
|
||||||
|
return h.PrintC(clr, s)
|
||||||
|
case alignRight:
|
||||||
|
return h.PrintR(clr, s)
|
||||||
|
case alignLeft:
|
||||||
|
}
|
||||||
|
return h.PrintL(clr, s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h imgColumn) PrintC(clr aec.ANSI, s string) string {
|
||||||
|
ln := utf8.RuneCountInString(s)
|
||||||
|
|
||||||
|
if ln > h.Width {
|
||||||
|
return clr.Apply(truncateRunes(s, h.Width))
|
||||||
|
}
|
||||||
|
|
||||||
|
fill := h.Width - ln
|
||||||
|
|
||||||
|
l := fill / 2
|
||||||
|
r := fill - l
|
||||||
|
|
||||||
|
return strings.Repeat(" ", l) + clr.Apply(s) + strings.Repeat(" ", r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h imgColumn) PrintL(clr aec.ANSI, s string) string {
|
||||||
|
ln := utf8.RuneCountInString(s)
|
||||||
|
if ln > h.Width {
|
||||||
|
return clr.Apply(truncateRunes(s, h.Width))
|
||||||
|
}
|
||||||
|
|
||||||
|
return clr.Apply(s) + strings.Repeat(" ", h.Width-ln)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h imgColumn) PrintR(clr aec.ANSI, s string) string {
|
||||||
|
ln := utf8.RuneCountInString(s)
|
||||||
|
if ln > h.Width {
|
||||||
|
return clr.Apply(truncateRunes(s, h.Width))
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.Repeat(" ", h.Width-ln) + clr.Apply(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
type noColor struct{}
|
||||||
|
|
||||||
|
func (a noColor) With(_ ...aec.ANSI) aec.ANSI {
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a noColor) Apply(s string) string {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a noColor) String() string {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// widestFirstColumnValue calculates the width needed to fully display the image names and platforms.
|
||||||
|
func widestFirstColumnValue(headers []imgColumn, images []ImageIndexRow) int {
|
||||||
|
width := len(headers[0].Title)
|
||||||
|
for _, img := range images {
|
||||||
|
for _, name := range img.Names {
|
||||||
|
if len(name) > width {
|
||||||
|
width = len(name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, sub := range img.Children {
|
||||||
|
pl := len(sub.Platform) + len("└─ ")
|
||||||
|
if pl > width {
|
||||||
|
width = pl
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return width
|
||||||
|
}
|
Loading…
Reference in New Issue