├── .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 |
8 |
9 |
10 |
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 | 
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 | 
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 | 
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 | 
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 | [1;34mNAME[0m [1;34mCOMMAND[0m [1;34mSERVICE[0m [1;34mSTATUS[0m [1;34mPORTS[0m
2 | [1;30mcluster[0m [1;30m"docker-entrypoint.s…"[0m [1;30mcluster[0m [0;31mexited (0)[0m
3 | [1;37mredis[0m [1;30m"docker-entrypoint.s…"[0m [1;33mredis[0m [1;32mrunning[0m 6379/tcp
4 |
--------------------------------------------------------------------------------
/test/data/out/docker_compose_ps_1.out:
--------------------------------------------------------------------------------
1 | [1;34mNAME[0m [1;34mIMAGE[0m [1;34mCOMMAND[0m [1;34mSERVICE[0m [1;34mCREATED[0m [1;34mSTATUS[0m [1;34mPORTS[0m
2 | [1;37mm-grafana[0m [1;33mgrafana/grafana-enterprise[0m[1;32m:latest[0m [1;30m"/run.sh"[0m [1;33mgrafana[0m [0;32mAbout a minute ago[0m [1;32mUp About a minute[0m [1;36m127.0.0.1:3001[0m->3000/tcp
3 | [1;37mm-node-exporter[0m [1;33mprom/node-exporter[0m[1;32m:latest[0m [1;30m"/bin/node_exporter …"[0m [1;33mnode-exporter[0m [0;32mAbout a minute ago[0m [1;32mUp About a minute[0m 9100/tcp
4 | [1;37mm-prometheus[0m [1;33mprom/prometheus[0m[1;32m:latest[0m [1;30m"/bin/prometheus --c…"[0m [1;33mprometheus[0m [0;32mAbout a minute ago[0m [1;32mUp About a minute[0m 9090/tcp
5 |
--------------------------------------------------------------------------------
/test/data/out/docker_images.out:
--------------------------------------------------------------------------------
1 | [1;34mREPOSITORY[0m [1;34mTAG[0m [1;34mIMAGE ID[0m [1;34mCREATED[0m [1;34mSIZE[0m
2 | [1;30mdldash/home[0m [1;32mlatest[0m [1;30mb310e6b0fb4d[0m [0;32m15 hours ago[0m 23.5MB
3 | [1;37mredis[0m [1;32mlatest[0m [1;30mbba24acba395[0m [0;32m11 days ago[0m 113MB
4 | [1;30mgolangci/golangci-lint[0m [1;32mlatest[0m [1;30m94f49b27b197[0m [0;32m2 weeks ago[0m [0;33m987MB[0m
5 |
--------------------------------------------------------------------------------
/test/data/out/docker_ps.out:
--------------------------------------------------------------------------------
1 | [1;34mCONTAINER ID[0m [1;34mIMAGE[0m [1;34mCOMMAND[0m [1;34mCREATED[0m [1;34mSTATUS[0m [1;34mPORTS[0m [1;34mNAMES[0m
2 | [1;30m24c5774ef118[0m [1;33mdldash/home[0m [1;30m"/docker-entrypoint.…"[0m [0;32m25 seconds ago[0m [1;32mUp 24 seconds[0m [1;36m0.0.0.0:3000[0m->80/tcp [1;37mdldash.home[0m
3 | [1;30m10f3c0ac6560[0m [1;33mredis[0m [1;30m"docker-entrypoint.s…"[0m [0;32mAbout a minute ago[0m [1;32mUp About a minute[0m 6379/tcp [1;37mredis[0m
4 |
--------------------------------------------------------------------------------
/test/data/out/docker_ps_custom_cols.out:
--------------------------------------------------------------------------------
1 | [1;34mNAMES[0m [1;34mCREATED[0m [1;34mSTATUS[0m [1;34mNETWORKS[0m
2 | [1;37mdldash.home[0m [0;32mAbout an hour ago[0m [1;32mUp About an hour[0m bridge
3 | [1;37mredis[0m [0;32mAbout an hour ago[0m [1;32mUp About an hour[0m bridge
4 |
--------------------------------------------------------------------------------
/test/data/out/docker_ps_nullable_col.out:
--------------------------------------------------------------------------------
1 | [1;34mCONTAINER ID[0m [1;34mIMAGE[0m [1;34mCOMMAND[0m [1;34mCREATED[0m [1;34mSTATUS[0m [1;34mPORTS[0m [1;34mNAMES[0m
2 | [1;30mfe47cddd7ede[0m [1;33mdldash/home[0m [1;30m"/docker-entrypoint.…"[0m [0;32m42 seconds ago[0m [0;31mExited (0) 40 seconds ago[0m [1;37mdldash.home[0m
3 | [1;30m10f3c0ac6560[0m [1;33mredis[0m [1;30m"docker-entrypoint.s…"[0m [0;32m3 hours ago[0m [1;32mUp 3 hours[0m 6379/tcp [1;37mredis[0m
4 |
--------------------------------------------------------------------------------
/test/data/out/docker_stats.out:
--------------------------------------------------------------------------------
1 | [1;34mCONTAINER ID[0m [1;34mNAME[0m [1;34mCPU %[0m [1;34mMEM USAGE / LIMIT[0m [1;34mMEM %[0m [1;34mNET I/O[0m [1;34mBLOCK I/O[0m [1;34mPIDS[0m
2 | [1;30me39ba801c0e1[0m [1;37mnginx[0m [0;31m90.00%[0m 13.53MiB [1;30m/ 7.657GiB[0m [0;33m50.17%[0m 10.8MB [1;30m/[0m[0;31m 42.1GB[0m 0B [1;30m/[0m 12.3kB [1;36m17[0m
3 |
--------------------------------------------------------------------------------