From 070394fae1de7148d757341dbd6ab158490e826b Mon Sep 17 00:00:00 2001 From: "Joel D. Elkins" Date: Sun, 3 Mar 2024 14:15:46 -0600 Subject: [PATCH] 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. --- cmd/ls.go | 70 +++++++++++++++++++++++++++++++++++++++++-------------- go.mod | 4 ++++ go.sum | 16 +++++++++++++ 3 files changed, 72 insertions(+), 18 deletions(-) diff --git a/cmd/ls.go b/cmd/ls.go index af71285..75359f5 100644 --- a/cmd/ls.go +++ b/cmd/ls.go @@ -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) } diff --git a/go.mod b/go.mod index 2f6b247..db1d33d 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index a3496d3..bbdeab4 100644 --- a/go.sum +++ b/go.sum @@ -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=