@@ -10,11 +10,13 @@ import (
1010 "github.com/containerd/platforms"
1111 "github.com/docker/cli/cli/command"
1212 "github.com/docker/cli/cli/streams"
13+ "github.com/docker/cli/internal/tui"
1314 "github.com/docker/docker/api/types/filters"
1415 imagetypes "github.com/docker/docker/api/types/image"
1516 "github.com/docker/docker/pkg/stringid"
1617 "github.com/docker/go-units"
1718 "github.com/morikuni/aec"
19+ "github.com/opencontainers/go-digest"
1820)
1921
2022type treeOptions struct {
@@ -42,6 +44,8 @@ func runTree(ctx context.Context, dockerCLI command.Cli, opts treeOptions) error
4244 view := treeView {
4345 images : make ([]topImage , 0 , len (images )),
4446 }
47+ attested := make (map [digest.Digest ]bool )
48+
4549 for _ , img := range images {
4650 details := imageDetails {
4751 ID : img .ID ,
@@ -52,6 +56,10 @@ func runTree(ctx context.Context, dockerCLI command.Cli, opts treeOptions) error
5256 var totalContent int64
5357 children := make ([]subImage , 0 , len (img .Manifests ))
5458 for _ , im := range img .Manifests {
59+ if im .Kind == imagetypes .ManifestKindAttestation {
60+ attested [im .AttestationData .For ] = true
61+ continue
62+ }
5563 if im .Kind != imagetypes .ManifestKindImage {
5664 continue
5765 }
@@ -89,6 +97,15 @@ func runTree(ctx context.Context, dockerCLI command.Cli, opts treeOptions) error
8997 })
9098 }
9199
100+ for _ , img := range view .images {
101+ for idx , sub := range img .Children {
102+ if attested [digest .Digest (sub .Details .ID )] {
103+ sub .Details .Attested = true
104+ img .Children [idx ] = sub
105+ }
106+ }
107+ }
108+
92109 sort .Slice (view .images , func (i , j int ) bool {
93110 return view .images [i ].created > view .images [j ].created
94111 })
@@ -101,6 +118,7 @@ type imageDetails struct {
101118 DiskUsage string
102119 InUse bool
103120 ContentSize string
121+ Attested bool
104122}
105123
106124type topImage struct {
@@ -133,18 +151,45 @@ func printImageTree(dockerCLI command.Cli, view treeView) error {
133151 headerColor := aec .NewBuilder (aec .DefaultF , aec .Bold ).ANSI
134152 topNameColor := aec .NewBuilder (aec .BlueF , aec .Bold ).ANSI
135153 normalColor := aec .NewBuilder (aec .DefaultF ).ANSI
136- greenColor := aec .NewBuilder (aec .GreenF ).ANSI
137154 untaggedColor := aec .NewBuilder (aec .Faint ).ANSI
155+ var noneColor aec.ANSI = noColor {}
138156 if ! out .IsTerminal () {
139157 headerColor = noColor {}
140158 topNameColor = noColor {}
141159 normalColor = noColor {}
142- greenColor = noColor {}
143160 warningColor = noColor {}
144161 untaggedColor = noColor {}
145162 }
163+ isTerm := out .IsTerminal ()
146164
147165 _ , _ = fmt .Fprintln (out , warningColor .Apply ("WARNING: This is an experimental feature. The output may change and shouldn't be depended on." ))
166+
167+ chipAttested := tui.Printable {
168+ Plain : "A" ,
169+ Fancy : tui .Chip (233 , 49 , " A " ),
170+ }
171+ chipInUse := tui.Printable {
172+ Plain : "U" ,
173+ Fancy : tui .Chip (255 , 177 , " U " ),
174+ }
175+
176+ var legend string
177+ // Print legend
178+ legend += tui.Printable {
179+ Plain : "ADDITIONAL Legend: " ,
180+ Fancy : aec .Bold .Apply ("Legend → " ),
181+ }.String (isTerm )
182+ legend += chipAttested .String (isTerm )
183+ legend += " Attested | "
184+ legend += chipInUse .String (isTerm )
185+ legend += " In Use"
186+
187+ r := int (width ) - countChars (legend )
188+ if r < 0 {
189+ r = 0
190+ }
191+ _ , _ = fmt .Fprintln (out , strings .Repeat (" " , r )+ legend )
192+
148193 _ , _ = fmt .Fprintln (out , "" )
149194
150195 columns := []imgColumn {
@@ -178,15 +223,19 @@ func printImageTree(dockerCLI command.Cli, view treeView) error {
178223 },
179224 },
180225 {
181- Title : "In Use " ,
182- Align : alignCenter ,
183- Width : 6 ,
184- Color : & greenColor ,
226+ Title : "Additional " ,
227+ Align : alignLeft ,
228+ Width : len ( "Additional" ) ,
229+ Color : & noneColor ,
185230 DetailsValue : func (d * imageDetails ) string {
231+ var out string
232+ if d .Attested {
233+ out += chipAttested .String (isTerm )
234+ }
186235 if d .InUse {
187- return "✔"
236+ out += chipInUse . String ( isTerm )
188237 }
189- return " "
238+ return out
190239 },
191240 },
192241 }
@@ -307,11 +356,28 @@ type imgColumn struct {
307356}
308357
309358func truncateRunes (s string , length int ) string {
310- runes := []rune (s )
311- if len (runes ) > length {
312- return string (runes [:length - 3 ]) + "..."
359+ var out []rune
360+ ln := 0
361+ inEscape := false
362+ for _ , r := range s {
363+ if r == '\x1b' {
364+ inEscape = true
365+ continue
366+ }
367+ if inEscape {
368+ if r == 'm' {
369+ inEscape = false
370+ }
371+ continue
372+ }
373+
374+ ln += 1
375+ if ln == length - 3 {
376+ return string (out ) + "..."
377+ }
378+ out = append (out , r )
313379 }
314- return s
380+ return string ( out )
315381}
316382
317383func (h imgColumn ) Print (clr aec.ANSI , s string ) string {
@@ -325,8 +391,26 @@ func (h imgColumn) Print(clr aec.ANSI, s string) string {
325391 return h .PrintL (clr , s )
326392}
327393
394+ func cleanANSI (s string ) string {
395+ for {
396+ start := strings .Index (s , "\x1b " )
397+ if start == - 1 {
398+ return s
399+ }
400+ end := strings .Index (s [start :], "m" )
401+ if end == - 1 {
402+ return s
403+ }
404+ s = s [:start ] + s [start + end + 1 :]
405+ }
406+ }
407+
408+ func countChars (s string ) int {
409+ return utf8 .RuneCountInString (cleanANSI (s ))
410+ }
411+
328412func (h imgColumn ) PrintC (clr aec.ANSI , s string ) string {
329- ln := utf8 . RuneCountInString (s )
413+ ln := countChars (s )
330414
331415 if ln > h .Width {
332416 return clr .Apply (truncateRunes (s , h .Width ))
@@ -341,7 +425,7 @@ func (h imgColumn) PrintC(clr aec.ANSI, s string) string {
341425}
342426
343427func (h imgColumn ) PrintL (clr aec.ANSI , s string ) string {
344- ln := utf8 . RuneCountInString (s )
428+ ln := countChars (s )
345429 if ln > h .Width {
346430 return clr .Apply (truncateRunes (s , h .Width ))
347431 }
@@ -350,7 +434,7 @@ func (h imgColumn) PrintL(clr aec.ANSI, s string) string {
350434}
351435
352436func (h imgColumn ) PrintR (clr aec.ANSI , s string ) string {
353- ln := utf8 . RuneCountInString (s )
437+ ln := countChars (s )
354438 if ln > h .Width {
355439 return clr .Apply (truncateRunes (s , h .Width ))
356440 }
0 commit comments