Add colorized output for ls subcommand

Initiative is turning into a little hackathon. Trying to beautify ls output
a little since the number of configured containers on my main server is
growing to a large number (currently 44). text/tabwriter can't handle
SGR codes correctly. Investigations led me to the "ansiterm" library. I'm not
crazy about the API really, but it handles all the corners I'm concerned
about.
This commit is contained in:
Joel Elkins 2024-03-03 14:15:46 -06:00
parent 40357ba9b8
commit 070394fae1
Signed by: joel
GPG Key ID: 133589DC38921AE2
3 changed files with 72 additions and 18 deletions

View File

@ -26,9 +26,11 @@ package cmd
import (
"encoding/json"
"fmt"
"text/tabwriter"
"slices"
"gitea.elkins.co/Networking/ccl/internal/pkg/config"
"gitea.elkins.co/Networking/ccl/internal/pkg/container"
"github.com/juju/ansiterm"
"github.com/spf13/cobra"
)
@ -37,6 +39,8 @@ var (
lsCountRunning bool
lsCountNotRunning bool
lsJsonFormat bool
lsSortRuntime bool
lsNoColor bool
)
type lsContainerObj struct {
@ -103,6 +107,18 @@ ccl ls squid`,
}
}
if lsSortRuntime {
slices.SortFunc(conts, func(a, b *container.Container) int {
if a.RunningTime().Seconds() > b.RunningTime().Seconds() {
return -1
}
if a.RunningTime().Seconds() < b.RunningTime().Seconds() {
return 1
}
return 0
})
}
if lsJsonFormat {
out := make([]interface{}, 0)
for _, c := range conts {
@ -148,44 +164,57 @@ ccl ls squid`,
return
}
tw := tabwriter.NewWriter(w, 0, 0, 2, ' ', 0)
tw := ansiterm.NewTabWriter(w, 0, 0, 2, ' ', 0)
defcolor := ansiterm.Foreground(ansiterm.Default)
red := ansiterm.Foreground(ansiterm.Red)
yellow := ansiterm.Foreground(ansiterm.Yellow)
bold := ansiterm.Styles(ansiterm.Bold)
ital := ansiterm.Styles(ansiterm.Italic)
defer tw.Flush()
if conn != nil {
titlemsg := "CATEGORY\tGROUP\tNAME\tIMAGE\tCREATED\t RUNNING\t CPU%\t MEM%\t\n"
block := "%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t\n"
fmt.Fprint(tw, titlemsg)
bold.Fprint(tw, "CATEGORY\tGROUP\tNAME\tIMAGE\tCREATED\t RUNNING\t CPU%\t MEM%\t\n")
for _, c := range conts {
data := make([]any, 0, 8)
data = append(data, c.Category, fmt.Sprintf("%5d", c.StartGroup), c.Name, c.Image)
defcolor.Fprintf(tw, "%s\t%5d\t", c.Category, c.StartGroup)
ital.Fprintf(tw, "%s\t", c.Name)
defcolor.Fprintf(tw, "%s\t", c.Image)
if c.IsCreated() {
data = append(data, " ✓")
defcolor.Fprint(tw, " ✓\t")
} else {
data = append(data, "")
red.Fprint(tw, " ✗\t")
}
if c.IsRunning() {
raw := int64(c.RunningTime().Seconds())
seconds := raw % 60
minutes := (raw / 60) % 60
hours := (raw / 60) / 60
disp := fmt.Sprintf("%3d:%02d:%02d", hours, minutes, seconds)
data = append(data, disp)
defcolor.Fprintf(tw, "%3d:%02d:%02d\t", hours, minutes, seconds)
} else {
data = append(data, "")
red.Fprint(tw, " ✗\t")
}
if stats := c.GetStats(); c.IsRunning() && stats != nil {
data = append(data, fmt.Sprintf("%5.1f", stats.CPU), fmt.Sprintf("%5.1f", stats.MemPerc))
for _, st := range []float64{stats.CPU, stats.MemPerc} {
hi := defcolor
if st > 5.0 {
hi = yellow
}
if st > 20.0 {
hi = red
}
hi.Fprintf(tw, "%5.1f\t", st)
}
} else {
data = append(data, "", "")
red.Fprint(tw, " -\t -\t")
}
fmt.Fprintf(tw, block, data...)
fmt.Fprint(tw, "\n")
}
} else {
titlemsg := "CATEGORY\tGROUP\tNAME\tIMAGE"
fmt.Fprintf(tw, "%s\n", titlemsg)
titlemsg := "CATEGORY\tGROUP\tNAME\tIMAGE\n"
bold.Fprint(tw, titlemsg)
for _, c := range conts {
data := []interface{}{c.Category, c.StartGroup, c.Name, c.Image}
fmt.Fprintf(tw, "%s\t%5d\t%s\t%s\n", data...)
defcolor.Fprintf(tw, "%s\t%5d\t%s\t%s\n", data...)
}
}
},
@ -196,6 +225,11 @@ func init() {
lsCmd.Flags().BoolVarP(&lsCountRunning, "running", "R", false, "Return only the count of running items")
lsCmd.Flags().BoolVarP(&lsCountNotRunning, "not-running", "N", false, "Return only the count of stopped/failed items")
lsCmd.Flags().BoolVarP(&lsJsonFormat, "json", "J", false, "Output results as a json array")
lsCmd.Flags().BoolVarP(&lsSortRuntime, "sort-runtime", "T", false, "Sort by running time (descending)")
lsCmd.Flags().BoolVarP(&lsNoColor, "no-color", "K", false, "Suppress ansi color sequences in output")
lsCmd.MarkFlagsMutuallyExclusive("count", "running", "not-running")
lsCmd.MarkFlagsMutuallyExclusive("sort-runtime", "json")
lsCmd.MarkFlagsMutuallyExclusive("sort-runtime", "running")
lsCmd.MarkFlagsMutuallyExclusive("sort-runtime", "not-running")
rootCmd.AddCommand(lsCmd)
}

4
go.mod
View File

@ -8,6 +8,7 @@ require (
github.com/containers/common v0.57.1-0.20240222191224-9b0d0a77eb2e
github.com/containers/podman/v4 v4.5.0-rc1.0.20240207150443-caee76ed57c9
github.com/emirpasic/gods v1.18.1
github.com/juju/ansiterm v1.0.0
github.com/miekg/dns v1.1.58
github.com/opencontainers/runtime-spec v1.2.0
github.com/pelletier/go-toml v1.9.5
@ -84,8 +85,11 @@ require (
github.com/klauspost/pgzip v1.2.6 // indirect
github.com/kr/fs v0.1.0 // indirect
github.com/letsencrypt/boulder v0.0.0-20240221225126-96f124060393 // indirect
github.com/lunixbochs/vtclean v1.0.0 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/manifoldco/promptui v0.9.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.15 // indirect
github.com/mattn/go-shellwords v1.0.12 // indirect
github.com/mattn/go-sqlite3 v1.14.22 // indirect

16
go.sum
View File

@ -568,6 +568,8 @@ github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHm
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/juju/ansiterm v1.0.0 h1:gmMvnZRq7JZJx6jkfSq9/+2LMrVEwGwt7UR6G+lmDEg=
github.com/juju/ansiterm v1.0.0/go.mod h1:PyXUpnI3olx3bsPcHt98FGPX/KCFZ1Fi+hw1XLI6384=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
@ -601,6 +603,8 @@ github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/letsencrypt/boulder v0.0.0-20240221225126-96f124060393 h1:MLDVKJgZgs43ITIeWJkdDGJZ9mrETJCXX4pMmvKh+k4=
github.com/letsencrypt/boulder v0.0.0-20240221225126-96f124060393/go.mod h1:NTo9eSR9oPoLqZmiH0dzr2WNa1UplMjQnNhPLhqn2vk=
github.com/linuxkit/virtsock v0.0.0-20201010232012-f8cee7dfc7a3/go.mod h1:3r6x7q95whyfWQpmGZTu3gk3v2YkMi05HEzl7Tf7YEo=
github.com/lunixbochs/vtclean v1.0.0 h1:xu2sLAri4lGiovBDQKxl5mrXyESr3gUr5m5SM5+LVb8=
github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI=
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
@ -612,7 +616,15 @@ github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYt
github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg=
github.com/marstr/guid v1.1.0/go.mod h1:74gB1z2wpxxInTG6yaqA7KrtM0NZ+RbrcqDvYHefzho=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.1.10/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
@ -1115,6 +1127,7 @@ golang.org/x/sys v0.0.0-20191210023423-ac6580df4449/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200120151820-655fe14d7479/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@ -1147,6 +1160,7 @@ golang.org/x/sys v0.0.0-20210426230700-d19ff857e887/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210906170528-6f6e22806c34/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211116061358-0a5406a5449c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@ -1155,10 +1169,12 @@ golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220817070843-5a390386f1f2/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220823224334-20c2bfdbfe24/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=