├── .github └── workflows │ └── workflow.yml ├── .gitignore ├── .golangci.yml ├── LICENSE ├── Makefile ├── README.md ├── bash ├── aliases.sh └── execute.sh ├── cmd └── cli │ └── main.go ├── go.mod ├── go.sum ├── internal ├── app │ ├── app.go │ ├── app_test.go │ ├── const.go │ ├── usage.go │ └── usage_test.go ├── cmd │ ├── contract.go │ ├── docker_compose_ps.go │ ├── docker_images.go │ ├── docker_ps.go │ ├── docker_stats.go │ └── parse.go ├── config │ └── config.go ├── layout │ ├── column.go │ ├── header.go │ ├── parse.go │ └── row.go ├── stderr │ └── stderr.go ├── stdin │ └── stdin.go ├── stdout │ └── stdout.go └── util │ ├── util.go │ └── util_test.go ├── pkg ├── color │ ├── color.go │ └── color_test.go └── util │ ├── assert │ └── assertions.go │ └── number │ ├── number.go │ └── number_test.go └── test └── data ├── in ├── docker_compose_ps.out ├── docker_compose_ps_1.out ├── docker_images.out ├── docker_ps.out ├── docker_ps_custom_cols.out ├── docker_ps_nullable_col.out ├── docker_ps_nullable_cols.out ├── docker_stats.out ├── invalid_cols.out └── no_first_line.out └── out ├── docker_compose_ps.out ├── docker_compose_ps_1.out ├── docker_images.out ├── docker_ps.out ├── docker_ps_custom_cols.out ├── docker_ps_nullable_col.out └── docker_stats.out /.github/workflows/workflow.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v4 15 | 16 | - name: Set up Go 17 | uses: actions/setup-go@v5 18 | with: 19 | go-version: ^1.21 20 | 21 | - name: Lint 22 | uses: golangci/golangci-lint-action@v3 23 | 24 | - name: Build 25 | run: make build 26 | 27 | - name: Coverage 28 | run: go test -race -coverprofile=coverage.out -covermode=atomic -tags test ./... 29 | 30 | - name: Upload coverage to Codecov 31 | uses: codecov/codecov-action@v4 32 | with: 33 | token: ${{ secrets.CODECOV_TOKEN }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bin 2 | dpkg 3 | 4 | # Temporary editor files 5 | *~ 6 | .gnupg 7 | *.sw[pno] 8 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | linters: 2 | enable-all: true 3 | disable: 4 | - depguard 5 | 6 | linters-settings: 7 | gci: 8 | sections: 9 | - standard 10 | - default 11 | - prefix(github.com/devemio/docker-color-output) 12 | varnamelen: 13 | ignore-names: 14 | - i, j, fn, in, tt 15 | - x, sb 16 | 17 | issues: 18 | exclude-rules: 19 | - path: '(.+)_test\.go' 20 | linters: 21 | - funlen 22 | - exhaustruct -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Sergey Sorokin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | BIN=$(CURDIR)/bin 2 | GO=$(shell which go) 3 | APP=docker-color-output 4 | 5 | .PHONY: build 6 | build: 7 | @CGO_ENABLED=0 $(GO) build -ldflags="-s -w" -o $(BIN)/$(APP) ./cmd/cli 8 | 9 | .PHONY: publish 10 | publish: clean 11 | # Mac 12 | GOOS=darwin GOARCH=amd64 CGO_ENABLED=0 $(GO) build -ldflags="-s -w" -o $(BIN)/$(APP)-darwin-amd64 ./cmd/cli 13 | GOOS=darwin GOARCH=arm64 CGO_ENABLED=0 $(GO) build -ldflags="-s -w" -o $(BIN)/$(APP)-darwin-arm64 ./cmd/cli 14 | # Linux 15 | GOOS=linux GOARCH=amd64 CGO_ENABLED=0 $(GO) build -ldflags="-s -w" -o $(BIN)/$(APP)-linux-amd64 ./cmd/cli 16 | GOOS=linux GOARCH=arm64 CGO_ENABLED=0 $(GO) build -ldflags="-s -w" -o $(BIN)/$(APP)-linux-arm64 ./cmd/cli 17 | # Windows 18 | GOOS=windows GOARCH=amd64 CGO_ENABLED=0 $(GO) build -ldflags="-s -w" -o $(BIN)/$(APP)-windows-amd64.exe ./cmd/cli 19 | # Cleanup 20 | mv ./bin/* ~/Downloads 21 | 22 | .PHONY: test 23 | test: 24 | @go test ./... 25 | 26 | .PHONY: test/cover 27 | test/cover: 28 | go test -v -race -buildvcs -coverprofile=/tmp/coverage.out ./... 29 | go tool cover -html=/tmp/coverage.out 30 | 31 | .PHONY: clean 32 | clean: 33 | @rm -f $(BIN)/$(APP)* 34 | 35 | .PHONY: lint 36 | lint: 37 | @golangci-lint run -v --fix 38 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | 5 |

6 | 7 | Coverage 8 | Downloads 9 | Release 10 | License 11 |

12 | 13 | Docker Color Output is a lightweight tool that enhances Docker's command output by adding vibrant, customizable colors. It processes the standard output through a pipeline, making it easier to read and more visually appealing. 14 | 15 | ## Features 🚀 16 | 17 | - **Cross-Platform Support:** Works on macOS, Linux, and Windows. 18 | - **Customizable Color Schemes:** Easily adjust the colors to your preference. 19 | - **Pipeline Integration:** Transforms Docker command outputs into colorful, structured displays. 20 | 21 | ## Installation 👨‍💻 22 | 23 | You can find all compiled binaries on the [releases](https://github.com/devemio/docker-color-output/releases) page. 24 | 25 | #### Mac 🍏 26 | 27 | ```bash 28 | brew install dldash/core/docker-color-output 29 | ``` 30 | 31 | #### Linux 🐧 32 | 33 | ```bash 34 | sudo add-apt-repository ppa:dldash/core 35 | sudo apt update 36 | sudo apt install docker-color-output 37 | ``` 38 | 39 | #### Windows 🪟 40 | 41 | Download the Windows build from the [releases](https://github.com/devemio/docker-color-output/releases) page. 42 | 43 | ## Configuration ⚙️ 44 | 45 | Easily tailor the color scheme to match your personal preferences. Simply run `docker-color-output` with the`-c` 46 | flag and provide the path to your custom configuration file. You can override any subset of the default 47 | colors—any color setting not specified in your file will automatically revert to the default. 48 | 49 | ```shell 50 | docker-color-output -c ~/.config/docker-color-output/config.json 51 | ``` 52 | 53 | ##### Default Configuration File 54 | 55 | ```json 56 | { 57 | "colors": { 58 | "reset": "\u001b[0m", 59 | "black": "\u001b[0;30m", 60 | "darkGray": "\u001b[1;30m", 61 | "red": "\u001b[0;31m", 62 | "lightRed": "\u001b[1;31m", 63 | "green": "\u001b[0;32m", 64 | "lightGreen": "\u001b[1;32m", 65 | "brown": "\u001b[0;33m", 66 | "yellow": "\u001b[1;33m", 67 | "blue": "\u001b[0;34m", 68 | "lightBlue": "\u001b[1;34m", 69 | "purple": "\u001b[0;35m", 70 | "lightPurple": "\u001b[1;35m", 71 | "cyan": "\u001b[0;36m", 72 | "lightCyan": "\u001b[1;36m", 73 | "lightGray": "\u001b[0;37m", 74 | "white": "\u001b[1;37m" 75 | } 76 | } 77 | ``` 78 | 79 | ### Silent Mode 🔇 80 | 81 | Silent Mode ensures a cleaner output by suppressing error messages. When enabled, if an error occurs, 82 | the tool will simply pass through the original Docker output without displaying any error notifications. 83 | 84 | ```bash 85 | docker ps | docker-color-output -s 86 | ``` 87 | 88 | ## Usage 📚 89 | 90 | Enhance your Docker workflow with these handy aliases and enjoy vibrant outputs. 91 | 92 | ### Aliases 🪄 93 | 94 | Utilize the bash functions provided in [aliases.sh](bash/aliases.sh) to streamline your commands. 95 | 96 | ### docker images 💡 97 | 98 | ```bash 99 | di # alias 100 | ``` 101 | 102 | ```bash 103 | docker images [--format] | docker-color-output 104 | ``` 105 | 106 | ![docker images](https://user-images.githubusercontent.com/5787193/93581956-7ae7f580-f9aa-11ea-8f81-d6922e1ca892.png) 107 | 108 | #### docker ps 💡 109 | 110 | ```bash 111 | dps # alias 112 | ``` 113 | 114 | ```bash 115 | docker ps [-a] [--format] | docker-color-output 116 | ``` 117 | 118 | ![docker ps](https://user-images.githubusercontent.com/5787193/93581144-69521e00-f9a9-11ea-86bb-c23d7879c689.png) 119 | 120 | #### docker compose ps 💡 121 | 122 | > [!NOTE] 123 | > The latest version supports docker compose `2.x`. 124 | 125 | ```bash 126 | dcps # alias 127 | ``` 128 | 129 | ```bash 130 | docker compose ps | docker-color-output 131 | ``` 132 | 133 | ![docker compose ps](https://user-images.githubusercontent.com/5787193/93630916-7267dd00-f9f3-11ea-9521-e69152fa86f1.png) 134 | 135 | #### docker stats 💡 136 | 137 | ```bash 138 | ds # alias 139 | ``` 140 | 141 | ```bash 142 | docker stats [--no-stream] | docker-color-output 143 | ``` 144 | 145 | ![docker stats](https://github.com/devemio/docker-color-output/assets/5787193/a3134ae9-707b-4ad7-a5ea-765150d535e8) 146 | 147 | ## License 📜 148 | 149 | This project is licensed under the [MIT License](https://opensource.org/licenses/MIT). 150 | -------------------------------------------------------------------------------- /bash/aliases.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | unalias dps 4 | 5 | di() { 6 | docker images "$@" | docker-color-output 7 | } 8 | 9 | dps() { 10 | docker ps "$@" | docker-color-output 11 | } 12 | 13 | dcps() { 14 | docker compose ps "$@" | docker-color-output 15 | } 16 | 17 | ds() { 18 | docker stats "$@" | docker-color-output 19 | } 20 | -------------------------------------------------------------------------------- /bash/execute.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Usage: 4 | # alias docker='bash execute.sh docker' 5 | if [[ "$1" == "docker" ]]; then 6 | if [[ "$2" == "ps" || "$2" == "images" ]]; then 7 | "$@" | docker-color-output 8 | elif [[ "$2" == "compose" && "$3" == "ps" ]]; then 9 | "$@" | docker-color-output 10 | else 11 | "$@" 12 | fi 13 | else 14 | "$@" 15 | fi 16 | -------------------------------------------------------------------------------- /cmd/cli/main.go: -------------------------------------------------------------------------------- 1 | //go:build !test 2 | 3 | package main 4 | 5 | import ( 6 | "fmt" 7 | "os" 8 | 9 | "github.com/devemio/docker-color-output/internal/app" 10 | "github.com/devemio/docker-color-output/internal/config" 11 | "github.com/devemio/docker-color-output/internal/stdin" 12 | "github.com/devemio/docker-color-output/internal/stdout" 13 | "github.com/devemio/docker-color-output/pkg/color" 14 | ) 15 | 16 | func main() { 17 | if err := run(); err != nil { 18 | app.Usage(err) 19 | os.Exit(1) 20 | } 21 | } 22 | 23 | func run() error { 24 | cfg, err := config.Get() 25 | if err != nil { 26 | return fmt.Errorf("cfg: %w", err) 27 | } 28 | 29 | color.SetPalette(color.Palette(cfg.Colors)) 30 | 31 | return stdin.Get(func(rows []string) error { //nolint:wrapcheck 32 | formatted, err := app.Run(rows) 33 | if err != nil { 34 | if !cfg.SilentMode { 35 | return fmt.Errorf("app: %w", err) 36 | } 37 | 38 | formatted = rows 39 | } 40 | 41 | for _, row := range formatted { 42 | stdout.Println(row) 43 | } 44 | 45 | return nil 46 | }) 47 | } 48 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/devemio/docker-color-output 2 | 3 | go 1.24 4 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devemio/docker-color-output/7c44fecbcfe1bff880c66260a83b9857f934834f/go.sum -------------------------------------------------------------------------------- /internal/app/app.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "strings" 7 | 8 | "github.com/devemio/docker-color-output/internal/cmd" 9 | "github.com/devemio/docker-color-output/internal/layout" 10 | "github.com/devemio/docker-color-output/internal/util" 11 | "github.com/devemio/docker-color-output/pkg/color" 12 | ) 13 | 14 | var ( 15 | ErrNoFirstLine = errors.New("no first line") 16 | ErrNullableColumns = errors.New("nullable columns more than one") 17 | ) 18 | 19 | func Run(in []string) ([]string, error) { 20 | if len(in) == 0 { 21 | return nil, ErrNoFirstLine 22 | } 23 | 24 | header := layout.ParseHeader(in) 25 | rows := layout.ParseRows(in, &header) 26 | 27 | if header.NullableCols() > 1 { 28 | return nil, ErrNullableColumns 29 | } 30 | 31 | command, err := cmd.Parse(header) 32 | if err != nil { 33 | return nil, fmt.Errorf("cmd: parse: %w", err) 34 | } 35 | 36 | res := make([]string, len(in)) 37 | 38 | // First line 39 | var sb strings.Builder 40 | for _, col := range header { 41 | sb.WriteString(util.Pad(color.LightBlue(string(col.Name)), col.MaxLength)) 42 | } 43 | 44 | res[0] = sb.String() 45 | 46 | // Rows 47 | for i, row := range rows { 48 | sb.Reset() 49 | 50 | for _, col := range header { 51 | sb.WriteString(util.Pad(command.Format(row, col.Name), col.MaxLength)) 52 | } 53 | 54 | res[i+1] = sb.String() 55 | } 56 | 57 | return res, nil 58 | } 59 | -------------------------------------------------------------------------------- /internal/app/app_test.go: -------------------------------------------------------------------------------- 1 | package app_test 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "runtime" 7 | "strings" 8 | "testing" 9 | 10 | "github.com/devemio/docker-color-output/internal/app" 11 | "github.com/devemio/docker-color-output/pkg/util/assert" 12 | ) 13 | 14 | func TestRun(t *testing.T) { 15 | t.Parallel() 16 | 17 | read := func(filename string) []string { 18 | _, b, _, _ := runtime.Caller(0) 19 | path := filepath.Dir(b) 20 | bytes, _ := os.ReadFile(path + "/../../test/data/" + filename) 21 | res := strings.Split(string(bytes), "\n") 22 | res = res[:len(res)-1] 23 | 24 | if len(res) == 0 { 25 | return nil 26 | } 27 | 28 | return res 29 | } 30 | 31 | tests := map[string]struct { 32 | in string 33 | want string 34 | wantErr bool 35 | }{ 36 | "no_stdin": {wantErr: true}, 37 | "no_first_line": {in: "no_first_line.out", wantErr: true}, 38 | "invalid_cols": {in: "invalid_cols.out", wantErr: true}, 39 | "docker_ps": {in: "docker_ps.out", want: "docker_ps.out"}, 40 | "docker_ps:custom_cols": {in: "docker_ps_custom_cols.out", want: "docker_ps_custom_cols.out"}, 41 | "docker_ps:nullable_col": {in: "docker_ps_nullable_col.out", want: "docker_ps_nullable_col.out"}, 42 | "docker_ps:nullable_cols": {in: "docker_ps_nullable_cols.out", wantErr: true}, 43 | "docker_images": {in: "docker_images.out", want: "docker_images.out"}, 44 | "docker_compose_ps": {in: "docker_compose_ps.out", want: "docker_compose_ps.out"}, 45 | "docker_compose_ps_1": {in: "docker_compose_ps_1.out", want: "docker_compose_ps_1.out"}, 46 | "docker_stats": {in: "docker_stats.out", want: "docker_stats.out"}, 47 | } 48 | 49 | for name, tt := range tests { 50 | t.Run(name, func(t *testing.T) { 51 | t.Parallel() 52 | 53 | rows, err := app.Run(read("in/" + tt.in)) 54 | assert.Equal(t, tt.wantErr, err != nil) 55 | assert.Equal(t, read("out/"+tt.want), rows) 56 | }) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /internal/app/const.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | const ( 4 | Ver = "2.6.1" 5 | Name = "docker-color-output" 6 | ) 7 | -------------------------------------------------------------------------------- /internal/app/usage.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "github.com/devemio/docker-color-output/internal/stderr" 5 | "github.com/devemio/docker-color-output/pkg/color" 6 | ) 7 | 8 | const indent = " " 9 | 10 | func Usage(err error) { 11 | if err != nil { 12 | stderr.Println(color.LightRed("💡 Error: " + err.Error())) 13 | } 14 | 15 | stderr.Println("💥 Version: " + color.Green(Ver)) 16 | 17 | stderr.Println("👌 Usage:") 18 | stderr.Println(indent + color.Green("docker compose ps") + " [-a] | " + color.Brown(Name)) 19 | stderr.Println(indent + color.Green("docker images") + " [--format] | " + color.Brown(Name)) 20 | stderr.Println(indent + color.Green("docker ps") + " [-a] [--format] | " + color.Brown(Name)) 21 | stderr.Println(indent + color.Green("docker stats") + " [--no-stream] | " + color.Brown(Name)) 22 | 23 | stderr.Println("🚀 Flags:") 24 | stderr.Println(indent + color.Green("-c") + " " + color.Brown("string") + " Path to configuration file") 25 | stderr.Println(indent + color.Green("-s") + " " + color.Brown("bool") + " Silent mode (suppress errors)") 26 | } 27 | -------------------------------------------------------------------------------- /internal/app/usage_test.go: -------------------------------------------------------------------------------- 1 | package app_test 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | 7 | "github.com/devemio/docker-color-output/internal/app" 8 | ) 9 | 10 | var errTest = errors.New("test") 11 | 12 | func TestUsage(*testing.T) { 13 | app.Usage(errTest) 14 | } 15 | -------------------------------------------------------------------------------- /internal/cmd/contract.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/devemio/docker-color-output/internal/layout" 5 | ) 6 | 7 | const ValidTotalParts = 2 8 | 9 | type Command interface { 10 | Format(rows layout.Row, col layout.Column) string 11 | } 12 | -------------------------------------------------------------------------------- /internal/cmd/docker_compose_ps.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/devemio/docker-color-output/internal/layout" 7 | "github.com/devemio/docker-color-output/pkg/color" 8 | ) 9 | 10 | const ( 11 | DockerComposePsName = "NAME" 12 | DockerComposePsImage = "IMAGE" 13 | DockerComposePsCommand = "COMMAND" 14 | DockerComposePsService = "SERVICE" 15 | DockerComposePsCreated = "CREATED" 16 | DockerComposePsStatus = "STATUS" 17 | DockerComposePsPorts = "PORTS" 18 | ) 19 | 20 | type DockerComposePs struct{} 21 | 22 | func (c *DockerComposePs) Columns() []string { 23 | return []string{ 24 | DockerComposePsName, // 25 | DockerComposePsImage, // 26 | DockerComposePsCommand, // 27 | DockerComposePsService, // 28 | DockerComposePsCreated, // 29 | DockerComposePsStatus, // 30 | DockerComposePsPorts, // nullable 31 | } 32 | } 33 | 34 | func (c *DockerComposePs) Format(row layout.Row, col layout.Column) string { 35 | x := string(row[col]) 36 | 37 | switch col { 38 | case DockerComposePsName: 39 | return c.Name(x, row) 40 | case DockerComposePsImage: 41 | return c.Image(x) 42 | case DockerComposePsCommand: 43 | return c.Command(x) 44 | case DockerComposePsService: 45 | return c.Service(x, row) 46 | case DockerComposePsCreated: 47 | return c.Created(x) 48 | case DockerComposePsStatus: 49 | return c.Status(x) 50 | case DockerComposePsPorts: 51 | return c.Ports(x) 52 | default: 53 | return x 54 | } 55 | } 56 | 57 | func (*DockerComposePs) Name(x string, row layout.Row) string { 58 | if strings.Contains(string(row[DockerComposePsStatus]), "exited") { 59 | return color.DarkGray(x) 60 | } 61 | 62 | return color.White(x) 63 | } 64 | 65 | func (*DockerComposePs) Image(x string) string { 66 | parts := strings.Split(x, ":") //nolint:ifshort 67 | if len(parts) == ValidTotalParts { 68 | return color.Yellow(parts[0]) + color.LightGreen(":"+parts[1]) 69 | } 70 | 71 | return color.Yellow(x) 72 | } 73 | 74 | func (*DockerComposePs) Command(x string) string { 75 | return color.DarkGray(x) 76 | } 77 | 78 | func (*DockerComposePs) Service(x string, row layout.Row) string { 79 | if strings.Contains(string(row[DockerComposePsStatus]), "exited") { 80 | return color.DarkGray(x) 81 | } 82 | 83 | return color.Yellow(x) 84 | } 85 | 86 | func (*DockerComposePs) Created(x string) string { 87 | if strings.Contains(x, "months") { 88 | return color.Brown(x) 89 | } 90 | 91 | if strings.Contains(x, "years") { 92 | return color.Red(x) 93 | } 94 | 95 | return color.Green(x) 96 | } 97 | 98 | func (*DockerComposePs) Status(x string) string { 99 | if strings.Contains(x, "exited") { 100 | return color.Red(x) 101 | } 102 | 103 | return color.LightGreen(x) 104 | } 105 | 106 | func (*DockerComposePs) Ports(x string) string { 107 | ports := make([]string, 0) 108 | 109 | for _, port := range strings.Split(x, ", ") { 110 | parts := strings.Split(port, "->") 111 | if len(parts) == ValidTotalParts { 112 | port = color.LightCyan(parts[0]) + "->" + parts[1] 113 | } 114 | 115 | ports = append(ports, port) 116 | } 117 | 118 | return strings.Join(ports, ", ") 119 | } 120 | -------------------------------------------------------------------------------- /internal/cmd/docker_images.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/devemio/docker-color-output/internal/layout" 7 | "github.com/devemio/docker-color-output/pkg/color" 8 | "github.com/devemio/docker-color-output/pkg/util/number" 9 | ) 10 | 11 | const ( 12 | DockerImagesRepository = "REPOSITORY" 13 | DockerImagesTag = "TAG" 14 | DockerImagesDigest = "DIGEST" 15 | DockerImagesImageID = "IMAGE ID" 16 | DockerImagesCreated = "CREATED" 17 | DockerImagesCreatedAt = "CREATED AT" 18 | DockerImagesSize = "SIZE" 19 | ) 20 | 21 | type DockerImages struct{} 22 | 23 | func (c *DockerImages) Columns() []string { 24 | return []string{ 25 | DockerImagesImageID, // 26 | DockerImagesRepository, // 27 | DockerImagesTag, // 28 | DockerImagesDigest, // opt 29 | DockerImagesCreated, // 30 | DockerImagesCreatedAt, // opt 31 | DockerImagesSize, // 32 | } 33 | } 34 | 35 | func (c *DockerImages) Format(row layout.Row, col layout.Column) string { 36 | x := string(row[col]) 37 | 38 | switch col { 39 | case DockerImagesRepository: 40 | return c.Repository(x) 41 | case DockerImagesTag: 42 | return c.Tag(x) 43 | case DockerImagesImageID: 44 | return c.ImageID(x) 45 | case DockerImagesCreated: 46 | return c.Created(x) 47 | case DockerImagesSize: 48 | return c.Size(x) 49 | default: 50 | return x 51 | } 52 | } 53 | 54 | func (*DockerImages) Repository(x string) string { 55 | if strings.Contains(x, "/") { 56 | return color.DarkGray(x) 57 | } 58 | 59 | return color.White(x) 60 | } 61 | 62 | func (*DockerImages) Tag(x string) string { 63 | if x == "latest" { 64 | return color.LightGreen(x) 65 | } 66 | 67 | return x 68 | } 69 | 70 | func (*DockerImages) ImageID(x string) string { 71 | return color.DarkGray(x) 72 | } 73 | 74 | func (*DockerImages) Created(x string) string { 75 | if strings.Contains(x, "hour") { 76 | return color.Green(x) 77 | } 78 | 79 | if strings.Contains(x, "days") { 80 | return color.Green(x) 81 | } 82 | 83 | if strings.Contains(x, "weeks") { 84 | return color.Green(x) 85 | } 86 | 87 | if strings.Contains(x, "months") { 88 | return color.Brown(x) 89 | } 90 | 91 | if strings.Contains(x, "years") { 92 | return color.Red(x) 93 | } 94 | 95 | return x 96 | } 97 | 98 | func (*DockerImages) Size(x string) string { 99 | if strings.Contains(x, "GB") { 100 | return color.Red(x) 101 | } 102 | 103 | if strings.Contains(x, "MB") && number.ParseFloat(x) >= 500 { 104 | return color.Brown(x) 105 | } 106 | 107 | return x 108 | } 109 | -------------------------------------------------------------------------------- /internal/cmd/docker_ps.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/devemio/docker-color-output/internal/layout" 7 | "github.com/devemio/docker-color-output/pkg/color" 8 | ) 9 | 10 | const ( 11 | DockerPsContainerID = "CONTAINER ID" 12 | DockerPsImage = "IMAGE" 13 | DockerPsCommand = "COMMAND" 14 | DockerPsCreatedAt = "CREATED AT" 15 | DockerPsCreated = "CREATED" 16 | DockerPsPorts = "PORTS" 17 | DockerPsState = "STATE" 18 | DockerPsStatus = "STATUS" 19 | DockerPsSize = "SIZE" 20 | DockerPsNames = "NAMES" 21 | DockerPsLabels = "LABELS" 22 | DockerPsMounts = "MOUNTS" 23 | DockerPsNetworks = "NETWORKS" 24 | ) 25 | 26 | type DockerPs struct{} 27 | 28 | func (c *DockerPs) Columns() []string { 29 | return []string{ 30 | DockerPsContainerID, // 31 | DockerPsImage, // 32 | DockerPsCommand, // 33 | DockerPsCreatedAt, // opt 34 | DockerPsCreated, // 35 | DockerPsPorts, // nullable 36 | DockerPsState, // opt 37 | DockerPsStatus, // 38 | DockerPsSize, // opt 39 | DockerPsNames, // 40 | DockerPsLabels, // opt 41 | DockerPsMounts, // opt | nullable 42 | DockerPsNetworks, // opt | nullable 43 | } 44 | } 45 | 46 | func (c *DockerPs) Format(rows layout.Row, col layout.Column) string { 47 | x := string(rows[col]) 48 | 49 | switch col { 50 | case DockerPsContainerID: 51 | return c.ContainerID(x) 52 | case DockerPsImage: 53 | return c.Image(x) 54 | case DockerPsCommand: 55 | return c.Command(x) 56 | case DockerPsCreated: 57 | return c.Created(x) 58 | case DockerPsStatus: 59 | return c.Status(x) 60 | case DockerPsPorts: 61 | return c.Ports(x) 62 | case DockerPsNames: 63 | return c.Names(x) 64 | default: 65 | return x 66 | } 67 | } 68 | 69 | func (*DockerPs) ContainerID(x string) string { 70 | return color.DarkGray(x) 71 | } 72 | 73 | func (*DockerPs) Image(x string) string { 74 | parts := strings.Split(x, ":") //nolint:ifshort 75 | if len(parts) == ValidTotalParts { 76 | return color.Yellow(parts[0]) + color.LightGreen(":"+parts[1]) 77 | } 78 | 79 | return color.Yellow(x) 80 | } 81 | 82 | func (*DockerPs) Command(x string) string { 83 | return color.DarkGray(x) 84 | } 85 | 86 | func (*DockerPs) Created(x string) string { 87 | if strings.Contains(x, "months") { 88 | return color.Brown(x) 89 | } 90 | 91 | if strings.Contains(x, "years") { 92 | return color.Red(x) 93 | } 94 | 95 | return color.Green(x) 96 | } 97 | 98 | func (*DockerPs) Status(x string) string { 99 | if strings.Contains(x, "Exited") { 100 | return color.Red(x) 101 | } 102 | 103 | return color.LightGreen(x) 104 | } 105 | 106 | func (*DockerPs) Ports(x string) string { 107 | ports := make([]string, 0) 108 | 109 | for _, port := range strings.Split(x, ", ") { 110 | parts := strings.Split(port, "->") 111 | if len(parts) == ValidTotalParts { 112 | port = color.LightCyan(parts[0]) + "->" + parts[1] 113 | } 114 | 115 | ports = append(ports, port) 116 | } 117 | 118 | return strings.Join(ports, ", ") 119 | } 120 | 121 | func (*DockerPs) Names(x string) string { 122 | return color.White(x) 123 | } 124 | -------------------------------------------------------------------------------- /internal/cmd/docker_stats.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/devemio/docker-color-output/internal/layout" 7 | "github.com/devemio/docker-color-output/pkg/color" 8 | "github.com/devemio/docker-color-output/pkg/util/number" 9 | ) 10 | 11 | const ( 12 | DockerStatsContainerID = "CONTAINER ID" 13 | DockerStatsName = "NAME" 14 | DockerStatsCPUPercent = "CPU %" 15 | DockerStatsMemUsage = "MEM USAGE / LIMIT" 16 | DockerStatsMemPercent = "MEM %" 17 | DockerStatsNetIO = "NET I/O" 18 | DockerStatsBlockIO = "BLOCK I/O" 19 | DockerStatsPIDs = "PIDS" 20 | ) 21 | 22 | const ( 23 | cpuPercentThresholdMedium = 50 24 | cpuPercentThresholdHigh = 90 25 | 26 | memPercentThresholdMedium = 50 27 | memPercentThresholdHigh = 90 28 | 29 | netIOThresholdHigh = 10 30 | 31 | blockIOThresholdHigh = 10 32 | 33 | pidsThresholdHigh = 100 34 | ) 35 | 36 | type DockerStats struct{} 37 | 38 | func (c *DockerStats) Columns() []string { 39 | return []string{ 40 | DockerStatsContainerID, // 41 | DockerStatsName, // 42 | DockerStatsCPUPercent, // 43 | DockerStatsMemUsage, // 44 | DockerStatsMemPercent, // 45 | DockerStatsNetIO, // 46 | DockerStatsBlockIO, // 47 | DockerStatsPIDs, // 48 | } 49 | } 50 | 51 | func (c *DockerStats) Format(rows layout.Row, col layout.Column) string { 52 | x := string(rows[col]) 53 | 54 | switch col { 55 | case DockerStatsContainerID: 56 | return c.ContainerID(x) 57 | case DockerStatsName: 58 | return c.Name(x) 59 | case DockerStatsCPUPercent: 60 | return c.CPUPercent(x) 61 | case DockerStatsMemUsage: 62 | return c.MemUsage(x) 63 | case DockerStatsMemPercent: 64 | return c.MemPercent(x) 65 | case DockerStatsNetIO: 66 | return c.NetIO(x) 67 | case DockerStatsBlockIO: 68 | return c.BlockIO(x) 69 | case DockerStatsPIDs: 70 | return c.PIDs(x) 71 | default: 72 | return x 73 | } 74 | } 75 | 76 | func (*DockerStats) ContainerID(x string) string { 77 | return color.DarkGray(x) 78 | } 79 | 80 | func (c *DockerStats) Name(x string) string { 81 | return color.White(x) 82 | } 83 | 84 | func (c *DockerStats) CPUPercent(x string) string { 85 | percent := number.ParseFloat(x) 86 | 87 | switch { 88 | case percent >= cpuPercentThresholdHigh: 89 | return color.Red(x) 90 | case percent >= cpuPercentThresholdMedium: 91 | return color.Brown(x) 92 | default: 93 | return x 94 | } 95 | } 96 | 97 | func (c *DockerStats) MemUsage(x string) string { 98 | parts := strings.Split(x, "/") 99 | 100 | return parts[0] + color.DarkGray("/"+parts[1]) 101 | } 102 | 103 | func (c *DockerStats) MemPercent(x string) string { 104 | percent := number.ParseFloat(x) 105 | 106 | switch { 107 | case percent >= memPercentThresholdHigh: 108 | return color.Red(x) 109 | case percent >= memPercentThresholdMedium: 110 | return color.Brown(x) 111 | default: 112 | return x 113 | } 114 | } 115 | 116 | func (*DockerStats) NetIO(x string) string { 117 | parts := strings.Split(x, "/") 118 | 119 | for i := range parts { 120 | if strings.Contains(parts[i], "GB") { 121 | if number.ParseFloat(parts[i]) >= netIOThresholdHigh { 122 | parts[i] = color.Red(parts[i]) 123 | } else { 124 | parts[i] = color.Brown(parts[i]) 125 | } 126 | } 127 | } 128 | 129 | return parts[0] + color.DarkGray("/") + parts[1] 130 | } 131 | 132 | func (*DockerStats) BlockIO(x string) string { 133 | parts := strings.Split(x, "/") 134 | 135 | for i := range parts { 136 | if strings.Contains(parts[i], "GB") { 137 | if number.ParseFloat(parts[i]) >= blockIOThresholdHigh { 138 | parts[i] = color.Red(parts[i]) 139 | } else { 140 | parts[i] = color.Brown(parts[i]) 141 | } 142 | } 143 | } 144 | 145 | return parts[0] + color.DarkGray("/") + parts[1] 146 | } 147 | 148 | func (*DockerStats) PIDs(x string) string { 149 | if number.ParseFloat(x) >= pidsThresholdHigh { 150 | return color.Red(x) 151 | } 152 | 153 | return color.LightCyan(x) 154 | } 155 | -------------------------------------------------------------------------------- /internal/cmd/parse.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/devemio/docker-color-output/internal/layout" 7 | "github.com/devemio/docker-color-output/internal/util" 8 | ) 9 | 10 | var ErrInvalidFirstLine = errors.New("invalid first line") 11 | 12 | func Parse(header layout.Header) (Command, error) { //nolint:ireturn 13 | columns := make([]string, len(header)) 14 | for i, col := range header { 15 | columns[i] = string(col.Name) 16 | } 17 | 18 | ps := &DockerPs{} 19 | if util.Intersect(columns, ps.Columns()) { 20 | return ps, nil 21 | } 22 | 23 | images := &DockerImages{} 24 | if util.Intersect(columns, images.Columns()) { 25 | return images, nil 26 | } 27 | 28 | composePs := &DockerComposePs{} 29 | if util.Intersect(columns, composePs.Columns()) { 30 | return composePs, nil 31 | } 32 | 33 | stats := &DockerStats{} 34 | if util.Intersect(columns, stats.Columns()) { 35 | return stats, nil 36 | } 37 | 38 | return nil, ErrInvalidFirstLine 39 | } 40 | -------------------------------------------------------------------------------- /internal/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "encoding/json" 5 | "flag" 6 | "fmt" 7 | "os" 8 | 9 | "github.com/devemio/docker-color-output/internal/app" 10 | ) 11 | 12 | type Config struct { 13 | SilentMode bool `json:"-"` 14 | Colors Colors `json:"colors"` 15 | } 16 | 17 | type Colors struct { 18 | Reset string `json:"reset"` 19 | Black string `json:"black"` 20 | DarkGray string `json:"darkGray"` 21 | Red string `json:"red"` 22 | LightRed string `json:"lightRed"` 23 | Green string `json:"green"` 24 | LightGreen string `json:"lightGreen"` 25 | Brown string `json:"brown"` 26 | Yellow string `json:"yellow"` 27 | Blue string `json:"blue"` 28 | LightBlue string `json:"lightBlue"` 29 | Purple string `json:"purple"` 30 | LightPurple string `json:"lightPurple"` 31 | Cyan string `json:"cyan"` 32 | LightCyan string `json:"lightCyan"` 33 | LightGray string `json:"lightGray"` 34 | White string `json:"white"` 35 | } 36 | 37 | func Get() (Config, error) { 38 | cfg := createDefault() 39 | 40 | flag.Usage = func() { 41 | app.Usage(nil) 42 | } 43 | 44 | cfgPath := flag.String("c", "", "Path to configuration file") 45 | silentMode := flag.Bool("s", false, "Silent mode (suppress errors)") 46 | flag.Parse() 47 | 48 | if *cfgPath != "" { 49 | data, err := os.ReadFile(*cfgPath) 50 | if err != nil { 51 | return Config{}, fmt.Errorf("read: %w", err) 52 | } 53 | 54 | if err = json.Unmarshal(data, &cfg); err != nil { 55 | return Config{}, fmt.Errorf("unmarshal: %w", err) 56 | } 57 | } 58 | 59 | if *silentMode { 60 | cfg.SilentMode = true 61 | } 62 | 63 | return cfg, nil 64 | } 65 | 66 | func createDefault() Config { 67 | return Config{ 68 | SilentMode: false, 69 | Colors: Colors{ 70 | Reset: "\u001B[0m", 71 | Black: "\u001B[0;30m", 72 | DarkGray: "\u001B[1;30m", 73 | Red: "\u001B[0;31m", 74 | LightRed: "\u001B[1;31m", 75 | Green: "\u001B[0;32m", 76 | LightGreen: "\u001B[1;32m", 77 | Brown: "\u001B[0;33m", 78 | Yellow: "\u001B[1;33m", 79 | Blue: "\u001B[0;34m", 80 | LightBlue: "\u001B[1;34m", 81 | Purple: "\u001B[0;35m", 82 | LightPurple: "\u001B[1;35m", 83 | Cyan: "\u001B[0;36m", 84 | LightCyan: "\u001B[1;36m", 85 | LightGray: "\u001B[0;37m", 86 | White: "\u001B[1;37m", 87 | }, 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /internal/layout/column.go: -------------------------------------------------------------------------------- 1 | package layout 2 | 3 | type Column string 4 | -------------------------------------------------------------------------------- /internal/layout/header.go: -------------------------------------------------------------------------------- 1 | package layout 2 | 3 | //nolint:gochecknoglobals 4 | var nullableCols = map[Column]struct{}{ 5 | "PORTS": {}, 6 | "MOUNTS": {}, 7 | "NETWORKS": {}, 8 | } 9 | 10 | type Cell struct { 11 | Name Column 12 | MaxLength int 13 | } 14 | 15 | func (c *Cell) IsNullable() bool { 16 | _, ok := nullableCols[c.Name] 17 | 18 | return ok 19 | } 20 | 21 | type Header []*Cell 22 | 23 | func (h Header) ToRow() Row { 24 | res := make(Row, len(h)) 25 | for _, col := range h { 26 | res[col.Name] = Value(col.Name) 27 | } 28 | 29 | return res 30 | } 31 | 32 | func (h Header) NullableCols() byte { 33 | var res byte 34 | 35 | for _, col := range h { 36 | if col.IsNullable() { 37 | res++ 38 | } 39 | } 40 | 41 | return res 42 | } 43 | -------------------------------------------------------------------------------- /internal/layout/parse.go: -------------------------------------------------------------------------------- 1 | package layout 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/devemio/docker-color-output/internal/util" 7 | ) 8 | 9 | func ParseHeader(rows []string) Header { 10 | parts := util.Split(rows[0]) 11 | 12 | res := make(Header, len(parts)) 13 | for i, part := range parts { 14 | res[i] = &Cell{ 15 | Name: Column(part), 16 | MaxLength: len(part), 17 | } 18 | } 19 | 20 | return res 21 | } 22 | 23 | func ParseRows(rows []string, header *Header) []Row { 24 | res := make([]Row, len(rows)-1) 25 | 26 | for i, row := range rows[1:] { 27 | offset := 0 28 | parts := util.Split(row) 29 | res[i] = make(Row, len(*header)) 30 | mismatch := len(parts) < len(*header) 31 | 32 | for j, col := range *header { 33 | if mismatch && col.IsNullable() { 34 | offset++ 35 | 36 | continue 37 | } 38 | 39 | x := parts[j-offset] 40 | 41 | length := len(x) 42 | if strings.Contains(x, "…") { 43 | length -= 2 44 | } 45 | 46 | if length > col.MaxLength { 47 | col.MaxLength = length 48 | } 49 | 50 | res[i][col.Name] = Value(x) 51 | } 52 | } 53 | 54 | return res 55 | } 56 | -------------------------------------------------------------------------------- /internal/layout/row.go: -------------------------------------------------------------------------------- 1 | package layout 2 | 3 | type Value string 4 | 5 | type Row map[Column]Value 6 | -------------------------------------------------------------------------------- /internal/stderr/stderr.go: -------------------------------------------------------------------------------- 1 | package stderr 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | ) 7 | 8 | func Println(in string) { 9 | _, _ = fmt.Fprintln(os.Stderr, in) 10 | } 11 | -------------------------------------------------------------------------------- /internal/stdin/stdin.go: -------------------------------------------------------------------------------- 1 | package stdin 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "errors" 7 | "fmt" 8 | "os" 9 | "runtime" 10 | 11 | "github.com/devemio/docker-color-output/internal/stdout" 12 | ) 13 | 14 | var ErrEmpty = errors.New("empty") 15 | 16 | const capRows = 50 17 | 18 | //nolint:cyclop 19 | func Get(fn func(rows []string) error) error { 20 | fi, err := os.Stdin.Stat() 21 | if err != nil { 22 | return fmt.Errorf("stdin: %w", err) 23 | } else if fi.Mode()&os.ModeNamedPipe == 0 && fi.Size() <= 0 { 24 | return fmt.Errorf("stdin: %w", ErrEmpty) 25 | } 26 | 27 | var ( 28 | xClearToEnd = []byte("\033[J") 29 | xClearToLineEnd = []byte("\033[K") 30 | xMoveCursorHome = []byte("\033[H") 31 | ) 32 | 33 | if runtime.GOOS != "darwin" { 34 | xClearToLineEnd = []byte(" \033[K") 35 | } 36 | 37 | var ( 38 | rows = make([]string, 0, capRows) 39 | scanner = bufio.NewScanner(os.Stdin) 40 | ) 41 | 42 | for scanner.Scan() { 43 | //nolint:wastedassign 44 | row, found := scanner.Bytes(), false 45 | 46 | // xClearToLineEnd 47 | if _, found = bytes.CutPrefix(row, xClearToLineEnd); found { 48 | stdout.Print(string(xClearToLineEnd)) 49 | 50 | continue 51 | } 52 | 53 | // xClearToLineEnd 54 | row = bytes.TrimSuffix(row, xClearToLineEnd) 55 | 56 | // xClearToEnd 57 | if row, found = bytes.CutPrefix(row, xClearToEnd); found { 58 | stdout.Print(string(xClearToEnd)) 59 | } 60 | 61 | // xMoveCursorHome 62 | if row, found = bytes.CutPrefix(row, xMoveCursorHome); found && len(rows) > 0 { 63 | stdout.Print(string(xMoveCursorHome)) 64 | 65 | if err = fn(rows); err != nil { 66 | return err 67 | } 68 | 69 | rows = rows[:0] 70 | } 71 | 72 | rows = append(rows, string(row)) 73 | } 74 | 75 | if err = scanner.Err(); err != nil { 76 | return fmt.Errorf("stdin: scan: %w", err) 77 | } 78 | 79 | return fn(rows) 80 | } 81 | -------------------------------------------------------------------------------- /internal/stdout/stdout.go: -------------------------------------------------------------------------------- 1 | package stdout 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | func Print(in string) { 8 | fmt.Print(in) //nolint:forbidigo 9 | } 10 | 11 | func Println(in string) { 12 | fmt.Println(in) //nolint:forbidigo 13 | } 14 | -------------------------------------------------------------------------------- /internal/util/util.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "fmt" 5 | "regexp" 6 | "strconv" 7 | "strings" 8 | ) 9 | 10 | const ( 11 | SpaceLength = 3 12 | NonPrintableCharactersLength = 11 13 | ) 14 | 15 | func Split(in string) []string { 16 | return regexp.MustCompile(`\s{2,}`).Split(in, -1) 17 | } 18 | 19 | func Pad(value string, length int) string { 20 | length += NonPrintableCharactersLength * strings.Count(value, "\033[0m") 21 | 22 | return fmt.Sprintf("%-"+strconv.Itoa(length+SpaceLength)+"s", value) 23 | } 24 | 25 | func Intersect(needle, haystack []string) bool { 26 | x := make(map[string]struct{}, len(haystack)) 27 | for _, v := range haystack { 28 | x[v] = struct{}{} 29 | } 30 | 31 | for _, v := range needle { 32 | if _, found := x[v]; !found { 33 | return false 34 | } 35 | } 36 | 37 | return true 38 | } 39 | -------------------------------------------------------------------------------- /internal/util/util_test.go: -------------------------------------------------------------------------------- 1 | package util_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/devemio/docker-color-output/internal/util" 7 | "github.com/devemio/docker-color-output/pkg/color" 8 | "github.com/devemio/docker-color-output/pkg/util/assert" 9 | ) 10 | 11 | func TestSplit(t *testing.T) { 12 | t.Parallel() 13 | 14 | tests := map[string]struct { 15 | in string 16 | want []string 17 | }{ 18 | "success": { 19 | in: "CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES", 20 | want: []string{"CONTAINER ID", "IMAGE", "COMMAND", "CREATED", "STATUS", "PORTS", "NAMES"}, 21 | }, 22 | } 23 | 24 | for name, tt := range tests { 25 | t.Run(name, func(t *testing.T) { 26 | t.Parallel() 27 | assert.Equal(t, tt.want, util.Split(tt.in)) 28 | }) 29 | } 30 | } 31 | 32 | func TestPad(t *testing.T) { 33 | t.Parallel() 34 | 35 | tests := map[string]struct { 36 | value string 37 | length int 38 | want string 39 | }{ 40 | "colored": { 41 | value: color.Red("IMAGE"), 42 | length: 10, 43 | want: "\u001B[0;31mIMAGE\u001B[0m ", 44 | }, 45 | "plain": { 46 | value: "CONTAINER ID", 47 | length: 15, 48 | want: "CONTAINER ID ", 49 | }, 50 | } 51 | 52 | for name, tt := range tests { 53 | t.Run(name, func(t *testing.T) { 54 | t.Parallel() 55 | assert.Equal(t, tt.want, util.Pad(tt.value, tt.length)) 56 | }) 57 | } 58 | } 59 | 60 | func TestIntersect(t *testing.T) { 61 | t.Parallel() 62 | 63 | tests := map[string]struct { 64 | needle []string 65 | haystack []string 66 | want bool 67 | }{ 68 | "empty": {[]string{}, []string{}, true}, 69 | "contains": {[]string{"1", "10"}, []string{"100", "1", "10"}, true}, 70 | "does_not_contain": {[]string{"1", "10"}, []string{"0", "1", "5"}, false}, 71 | "empty_value": {[]string{"", "1"}, []string{"1", "100", ""}, true}, 72 | } 73 | 74 | for name, tt := range tests { 75 | t.Run(name, func(t *testing.T) { 76 | t.Parallel() 77 | assert.Equal(t, tt.want, util.Intersect(tt.needle, tt.haystack)) 78 | }) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /pkg/color/color.go: -------------------------------------------------------------------------------- 1 | package color 2 | 3 | type Palette struct { 4 | Reset string 5 | Black string 6 | DarkGray string 7 | Red string 8 | LightRed string 9 | Green string 10 | LightGreen string 11 | Brown string 12 | Yellow string 13 | Blue string 14 | LightBlue string 15 | Purple string 16 | LightPurple string 17 | Cyan string 18 | LightCyan string 19 | LightGray string 20 | White string 21 | } 22 | 23 | //nolint:gochecknoglobals 24 | var palette = Palette{ 25 | Reset: "\u001B[0m", 26 | Black: "\u001B[0;30m", 27 | DarkGray: "\u001B[1;30m", 28 | Red: "\u001B[0;31m", 29 | LightRed: "\u001B[1;31m", 30 | Green: "\u001B[0;32m", 31 | LightGreen: "\u001B[1;32m", 32 | Brown: "\u001B[0;33m", 33 | Yellow: "\u001B[1;33m", 34 | Blue: "\u001B[0;34m", 35 | LightBlue: "\u001B[1;34m", 36 | Purple: "\u001B[0;35m", 37 | LightPurple: "\u001B[1;35m", 38 | Cyan: "\u001B[0;36m", 39 | LightCyan: "\u001B[1;36m", 40 | LightGray: "\u001B[0;37m", 41 | White: "\u001B[1;37m", 42 | } 43 | 44 | func SetPalette(p Palette) { 45 | palette = p 46 | } 47 | 48 | func Black(value string) string { 49 | return palette.Black + value + palette.Reset 50 | } 51 | 52 | func DarkGray(value string) string { 53 | return palette.DarkGray + value + palette.Reset 54 | } 55 | 56 | func Red(value string) string { 57 | return palette.Red + value + palette.Reset 58 | } 59 | 60 | func LightRed(value string) string { 61 | return palette.LightRed + value + palette.Reset 62 | } 63 | 64 | func Green(value string) string { 65 | return palette.Green + value + palette.Reset 66 | } 67 | 68 | func LightGreen(value string) string { 69 | return palette.LightGreen + value + palette.Reset 70 | } 71 | 72 | func Brown(value string) string { 73 | return palette.Brown + value + palette.Reset 74 | } 75 | 76 | func Yellow(value string) string { 77 | return palette.Yellow + value + palette.Reset 78 | } 79 | 80 | func Blue(value string) string { 81 | return palette.Blue + value + palette.Reset 82 | } 83 | 84 | func LightBlue(value string) string { 85 | return palette.LightBlue + value + palette.Reset 86 | } 87 | 88 | func Purple(value string) string { 89 | return palette.Purple + value + palette.Reset 90 | } 91 | 92 | func LightPurple(value string) string { 93 | return palette.LightPurple + value + palette.Reset 94 | } 95 | 96 | func Cyan(value string) string { 97 | return palette.Cyan + value + palette.Reset 98 | } 99 | 100 | func LightCyan(value string) string { 101 | return palette.LightCyan + value + palette.Reset 102 | } 103 | 104 | func LightGray(value string) string { 105 | return palette.LightGray + value + palette.Reset 106 | } 107 | 108 | func White(value string) string { 109 | return palette.White + value + palette.Reset 110 | } 111 | -------------------------------------------------------------------------------- /pkg/color/color_test.go: -------------------------------------------------------------------------------- 1 | package color_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/devemio/docker-color-output/pkg/color" 7 | "github.com/devemio/docker-color-output/pkg/util/assert" 8 | ) 9 | 10 | func TestColors(t *testing.T) { 11 | t.Parallel() 12 | 13 | msg := "Message" 14 | 15 | t.Run("Black", func(t *testing.T) { 16 | t.Parallel() 17 | assert.Equal(t, "\u001B[0;30m"+msg+"\u001B[0m", color.Black(msg)) 18 | }) 19 | 20 | t.Run("DarkGray", func(t *testing.T) { 21 | t.Parallel() 22 | assert.Equal(t, "\u001B[1;30m"+msg+"\u001B[0m", color.DarkGray(msg)) 23 | }) 24 | 25 | t.Run("Red", func(t *testing.T) { 26 | t.Parallel() 27 | assert.Equal(t, "\u001B[0;31m"+msg+"\u001B[0m", color.Red(msg)) 28 | }) 29 | 30 | t.Run("LightRed", func(t *testing.T) { 31 | t.Parallel() 32 | assert.Equal(t, "\u001B[1;31m"+msg+"\u001B[0m", color.LightRed(msg)) 33 | }) 34 | 35 | t.Run("Green", func(t *testing.T) { 36 | t.Parallel() 37 | assert.Equal(t, "\u001B[0;32m"+msg+"\u001B[0m", color.Green(msg)) 38 | }) 39 | 40 | t.Run("LightGreen", func(t *testing.T) { 41 | t.Parallel() 42 | assert.Equal(t, "\u001B[1;32m"+msg+"\u001B[0m", color.LightGreen(msg)) 43 | }) 44 | 45 | t.Run("Brown", func(t *testing.T) { 46 | t.Parallel() 47 | assert.Equal(t, "\u001B[0;33m"+msg+"\u001B[0m", color.Brown(msg)) 48 | }) 49 | 50 | t.Run("Yellow", func(t *testing.T) { 51 | t.Parallel() 52 | assert.Equal(t, "\u001B[1;33m"+msg+"\u001B[0m", color.Yellow(msg)) 53 | }) 54 | 55 | t.Run("Blue", func(t *testing.T) { 56 | t.Parallel() 57 | assert.Equal(t, "\u001B[0;34m"+msg+"\u001B[0m", color.Blue(msg)) 58 | }) 59 | 60 | t.Run("LightBlue", func(t *testing.T) { 61 | t.Parallel() 62 | assert.Equal(t, "\u001B[1;34m"+msg+"\u001B[0m", color.LightBlue(msg)) 63 | }) 64 | 65 | t.Run("Purple", func(t *testing.T) { 66 | t.Parallel() 67 | assert.Equal(t, "\u001B[0;35m"+msg+"\u001B[0m", color.Purple(msg)) 68 | }) 69 | 70 | t.Run("LightPurple", func(t *testing.T) { 71 | t.Parallel() 72 | assert.Equal(t, "\u001B[1;35m"+msg+"\u001B[0m", color.LightPurple(msg)) 73 | }) 74 | 75 | t.Run("Cyan", func(t *testing.T) { 76 | t.Parallel() 77 | assert.Equal(t, "\u001B[0;36m"+msg+"\u001B[0m", color.Cyan(msg)) 78 | }) 79 | 80 | t.Run("LightCyan", func(t *testing.T) { 81 | t.Parallel() 82 | assert.Equal(t, "\u001B[1;36m"+msg+"\u001B[0m", color.LightCyan(msg)) 83 | }) 84 | 85 | t.Run("LightGray", func(t *testing.T) { 86 | t.Parallel() 87 | assert.Equal(t, "\u001B[0;37m"+msg+"\u001B[0m", color.LightGray(msg)) 88 | }) 89 | 90 | t.Run("White", func(t *testing.T) { 91 | t.Parallel() 92 | assert.Equal(t, "\u001B[1;37m"+msg+"\u001B[0m", color.White(msg)) 93 | }) 94 | } 95 | 96 | func TestSetPalette(t *testing.T) { 97 | t.Parallel() 98 | 99 | color.SetPalette(color.Palette{ 100 | Reset: "\u001B[0m", 101 | Black: "\u001B[0;30m", 102 | DarkGray: "\u001B[1;30m", 103 | Red: "\u001B[0;31m", 104 | LightRed: "\u001B[1;31m", 105 | Green: "\u001B[0;32m", 106 | LightGreen: "\u001B[1;32m", 107 | Brown: "\u001B[0;33m", 108 | Yellow: "\u001B[1;33m", 109 | Blue: "\u001B[0;34m", 110 | LightBlue: "\u001B[1;34m", 111 | Purple: "\u001B[0;35m", 112 | LightPurple: "\u001B[1;35m", 113 | Cyan: "\u001B[0;36m", 114 | LightCyan: "\u001B[1;36m", 115 | LightGray: "\u001B[0;37m", 116 | White: "\u001B[1;37m", 117 | }) 118 | } 119 | -------------------------------------------------------------------------------- /pkg/util/assert/assertions.go: -------------------------------------------------------------------------------- 1 | package assert 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | func Equal(t *testing.T, expected, actual interface{}) { 9 | t.Helper() 10 | 11 | if !reflect.DeepEqual(expected, actual) { 12 | t.Errorf("Not equal:\nexpected: %s\nactual : %s", expected, actual) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /pkg/util/number/number.go: -------------------------------------------------------------------------------- 1 | package number 2 | 3 | import ( 4 | "strconv" 5 | "strings" 6 | "unicode" 7 | ) 8 | 9 | func ParseFloat(value string) float64 { 10 | res, _ := strconv.ParseFloat(strings.TrimFunc(value, func(r rune) bool { 11 | return !unicode.IsNumber(r) 12 | }), 64) 13 | 14 | return res 15 | } 16 | -------------------------------------------------------------------------------- /pkg/util/number/number_test.go: -------------------------------------------------------------------------------- 1 | package number_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/devemio/docker-color-output/pkg/util/assert" 7 | "github.com/devemio/docker-color-output/pkg/util/number" 8 | ) 9 | 10 | func TestParseFloat(t *testing.T) { 11 | t.Parallel() 12 | 13 | tests := map[string]struct { 14 | in string 15 | want float64 16 | }{ 17 | "plain": {in: "100", want: 100}, 18 | "float": {in: "100.10", want: 100.10}, 19 | "measurement": {in: "100MB", want: 100}, 20 | "trim": {in: " 100 ", want: 100}, 21 | "empty": {in: "", want: 0}, 22 | "unparsed": {in: "-", want: 0}, 23 | } 24 | 25 | for name, tt := range tests { 26 | t.Run(name, func(t *testing.T) { 27 | t.Parallel() 28 | assert.Equal(t, tt.want, number.ParseFloat(tt.in)) 29 | }) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /test/data/in/docker_compose_ps.out: -------------------------------------------------------------------------------- 1 | NAME COMMAND SERVICE STATUS PORTS 2 | cluster "docker-entrypoint.s…" cluster exited (0) 3 | redis "docker-entrypoint.s…" redis running 6379/tcp 4 | -------------------------------------------------------------------------------- /test/data/in/docker_compose_ps_1.out: -------------------------------------------------------------------------------- 1 | NAME IMAGE COMMAND SERVICE CREATED STATUS PORTS 2 | m-grafana grafana/grafana-enterprise:latest "/run.sh" grafana About a minute ago Up About a minute 127.0.0.1:3001->3000/tcp 3 | m-node-exporter prom/node-exporter:latest "/bin/node_exporter …" node-exporter About a minute ago Up About a minute 9100/tcp 4 | m-prometheus prom/prometheus:latest "/bin/prometheus --c…" prometheus About a minute ago Up About a minute 9090/tcp 5 | -------------------------------------------------------------------------------- /test/data/in/docker_images.out: -------------------------------------------------------------------------------- 1 | REPOSITORY TAG IMAGE ID CREATED SIZE 2 | dldash/home latest b310e6b0fb4d 15 hours ago 23.5MB 3 | redis latest bba24acba395 11 days ago 113MB 4 | golangci/golangci-lint latest 94f49b27b197 2 weeks ago 987MB 5 | -------------------------------------------------------------------------------- /test/data/in/docker_ps.out: -------------------------------------------------------------------------------- 1 | CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 2 | 24c5774ef118 dldash/home "/docker-entrypoint.…" 25 seconds ago Up 24 seconds 0.0.0.0:3000->80/tcp dldash.home 3 | 10f3c0ac6560 redis "docker-entrypoint.s…" About a minute ago Up About a minute 6379/tcp redis 4 | -------------------------------------------------------------------------------- /test/data/in/docker_ps_custom_cols.out: -------------------------------------------------------------------------------- 1 | NAMES CREATED STATUS NETWORKS 2 | dldash.home About an hour ago Up About an hour bridge 3 | redis About an hour ago Up About an hour bridge 4 | -------------------------------------------------------------------------------- /test/data/in/docker_ps_nullable_col.out: -------------------------------------------------------------------------------- 1 | CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 2 | fe47cddd7ede dldash/home "/docker-entrypoint.…" 42 seconds ago Exited (0) 40 seconds ago dldash.home 3 | 10f3c0ac6560 redis "docker-entrypoint.s…" 3 hours ago Up 3 hours 6379/tcp redis 4 | -------------------------------------------------------------------------------- /test/data/in/docker_ps_nullable_cols.out: -------------------------------------------------------------------------------- 1 | NAMES MOUNTS NAMES PORTS CREATED 2 | dldash.home dldash.home 0.0.0.0:3000->80/tcp 2 hours ago 3 | redis c06a7bd7d2af50… redis 6379/tcp 2 hours ago 4 | -------------------------------------------------------------------------------- /test/data/in/docker_stats.out: -------------------------------------------------------------------------------- 1 | CONTAINER ID NAME CPU % MEM USAGE / LIMIT MEM % NET I/O BLOCK I/O PIDS 2 | e39ba801c0e1 nginx 90.00% 13.53MiB / 7.657GiB 50.17% 10.8MB / 42.1GB 0B / 12.3kB 17 3 | -------------------------------------------------------------------------------- /test/data/in/invalid_cols.out: -------------------------------------------------------------------------------- 1 | NAME NULL 2 | dldash.home About an hour ago 3 | redis About an hour ago 4 | -------------------------------------------------------------------------------- /test/data/in/no_first_line.out: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /test/data/out/docker_compose_ps.out: -------------------------------------------------------------------------------- 1 | NAME COMMAND SERVICE STATUS PORTS 2 | cluster "docker-entrypoint.s…" cluster exited (0) 3 | redis "docker-entrypoint.s…" redis running 6379/tcp 4 | -------------------------------------------------------------------------------- /test/data/out/docker_compose_ps_1.out: -------------------------------------------------------------------------------- 1 | NAME IMAGE COMMAND SERVICE CREATED STATUS PORTS 2 | m-grafana grafana/grafana-enterprise:latest "/run.sh" grafana About a minute ago Up About a minute 127.0.0.1:3001->3000/tcp 3 | m-node-exporter prom/node-exporter:latest "/bin/node_exporter …" node-exporter About a minute ago Up About a minute 9100/tcp 4 | m-prometheus prom/prometheus:latest "/bin/prometheus --c…" prometheus About a minute ago Up About a minute 9090/tcp 5 | -------------------------------------------------------------------------------- /test/data/out/docker_images.out: -------------------------------------------------------------------------------- 1 | REPOSITORY TAG IMAGE ID CREATED SIZE 2 | dldash/home latest b310e6b0fb4d 15 hours ago 23.5MB 3 | redis latest bba24acba395 11 days ago 113MB 4 | golangci/golangci-lint latest 94f49b27b197 2 weeks ago 987MB 5 | -------------------------------------------------------------------------------- /test/data/out/docker_ps.out: -------------------------------------------------------------------------------- 1 | CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 2 | 24c5774ef118 dldash/home "/docker-entrypoint.…" 25 seconds ago Up 24 seconds 0.0.0.0:3000->80/tcp dldash.home 3 | 10f3c0ac6560 redis "docker-entrypoint.s…" About a minute ago Up About a minute 6379/tcp redis 4 | -------------------------------------------------------------------------------- /test/data/out/docker_ps_custom_cols.out: -------------------------------------------------------------------------------- 1 | NAMES CREATED STATUS NETWORKS 2 | dldash.home About an hour ago Up About an hour bridge 3 | redis About an hour ago Up About an hour bridge 4 | -------------------------------------------------------------------------------- /test/data/out/docker_ps_nullable_col.out: -------------------------------------------------------------------------------- 1 | CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 2 | fe47cddd7ede dldash/home "/docker-entrypoint.…" 42 seconds ago Exited (0) 40 seconds ago dldash.home 3 | 10f3c0ac6560 redis "docker-entrypoint.s…" 3 hours ago Up 3 hours 6379/tcp redis 4 | -------------------------------------------------------------------------------- /test/data/out/docker_stats.out: -------------------------------------------------------------------------------- 1 | CONTAINER ID NAME CPU % MEM USAGE / LIMIT MEM % NET I/O BLOCK I/O PIDS 2 | e39ba801c0e1 nginx 90.00% 13.53MiB / 7.657GiB 50.17% 10.8MB / 42.1GB 0B / 12.3kB 17 3 | --------------------------------------------------------------------------------