Full Code of Mikescher/better-docker-ps for AI

master 1f05ac9afc61 cached
38 files
114.5 KB
37.3k tokens
138 symbols
1 requests
Download .txt
Repository: Mikescher/better-docker-ps
Branch: master
Commit: 1f05ac9afc61
Files: 38
Total size: 114.5 KB

Directory structure:
gitextract_b7umzfrm/

├── .gitignore
├── .idea/
│   ├── .gitignore
│   ├── better-docker-ps.iml
│   ├── inspectionProfiles/
│   │   └── Project_Default.xml
│   ├── modules.xml
│   └── vcs.xml
├── LICENSE
├── Makefile
├── README.md
├── _data/
│   └── package-data/
│       ├── aur-bin/
│       │   ├── .gitignore
│       │   └── PKGBUILD
│       ├── aur-bin.sh
│       ├── aur-git/
│       │   ├── .gitignore
│       │   └── PKGBUILD
│       ├── aur-git.sh
│       ├── homebrew/
│       │   └── dops.rb
│       ├── homebrew.sh
│       └── sanitycheck.sh
├── cli/
│   ├── argTuple.go
│   ├── context.go
│   ├── options.go
│   └── parser.go
├── cmd/
│   └── dops/
│       └── main.go
├── consts/
│   ├── api.go
│   ├── exitcode.go
│   ├── version.go
│   └── version.sh
├── docker/
│   ├── api.go
│   ├── schema.go
│   └── util.go
├── go.mod
├── go.sum
├── impl/
│   ├── columns.go
│   └── impl.go
├── install.sh
├── printer/
│   └── printer.go
└── pserr/
    ├── err.go
    └── util.go

================================================
FILE CONTENTS
================================================

================================================
FILE: .gitignore
================================================

########## GOLAND ##########

.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/**/shelf
.idea/**/aws.xml
.idea/**/contentModel.xml
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
.idea/**/dbnavigator.xml
.idea/**/gradle.xml
.idea/**/libraries
.idea/**/mongoSettings.xml
*.iws
atlassian-ide-plugin.xml
.idea/httpRequests
.idea/caches/build_file_checksums.ser
.idea/$CACHE_FILE$

########## Linux ##########

*~
.fuse_hidden*
.directory
.Trash-*
.nfs*

########## Custom ##########


_out/*

input.test
input1.test
input2.test
input3.test
input.json
input1.json
input2.json
input_small.json
input1_small.json
input2_small.json

================================================
FILE: .idea/.gitignore
================================================
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml


================================================
FILE: .idea/better-docker-ps.iml
================================================
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
  <component name="Go" enabled="true" />
  <component name="NewModuleRootManager">
    <content url="file://$MODULE_DIR$" />
    <orderEntry type="inheritedJdk" />
    <orderEntry type="sourceFolder" forTests="false" />
  </component>
</module>

================================================
FILE: .idea/inspectionProfiles/Project_Default.xml
================================================
<component name="InspectionProjectProfileManager">
  <profile version="1.0">
    <option name="myName" value="Project Default" />
    <inspection_tool class="LanguageDetectionInspection" enabled="false" level="WARNING" enabled_by_default="false" />
    <inspection_tool class="SpellCheckingInspection" enabled="false" level="TYPO" enabled_by_default="false">
      <option name="processCode" value="true" />
      <option name="processLiterals" value="true" />
      <option name="processComments" value="true" />
    </inspection_tool>
  </profile>
</component>

================================================
FILE: .idea/modules.xml
================================================
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
  <component name="ProjectModuleManager">
    <modules>
      <module fileurl="file://$PROJECT_DIR$/.idea/better-docker-ps.iml" filepath="$PROJECT_DIR$/.idea/better-docker-ps.iml" />
    </modules>
  </component>
</project>

================================================
FILE: .idea/vcs.xml
================================================
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
  <component name="VcsDirectoryMappings">
    <mapping directory="$PROJECT_DIR$" vcs="Git" />
  </component>
</project>

================================================
FILE: LICENSE
================================================
MIT License

Copyright (c) 2022 Mike Schwörer

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


================================================
FILE: Makefile
================================================
build:
	go generate ./...
	CGO_ENABLED=0 go build -o _out/dops ./cmd/dops

run: build
	./_out/dops

clean:
	go clean
	rm ./_out/*

package:
	@echo "Make sure you have updated file://$(shell pwd)/consts/version.go"
	@echo "Make sure youhave created+pished a matching tag"
	@read -p "Continue?"

	go clean
	rm -rf ./_out/*

	_data/package-data/sanitycheck.sh

	GOARCH=386   GOOS=linux   CGO_ENABLED=0 go build -o _out/dops_linux-386-static                     ./cmd/dops  # Linux - 32 bit
	GOARCH=amd64 GOOS=linux   CGO_ENABLED=0 go build -o _out/dops_linux-amd64-static                   ./cmd/dops  # Linux - 64 bit
	GOARCH=arm64 GOOS=linux   CGO_ENABLED=0 go build -o _out/dops_linux-arm64-static                   ./cmd/dops  # Linux - ARM
	GOARCH=386   GOOS=linux                 go build -o _out/dops_linux-386                            ./cmd/dops  # Linux - 32 bit
	GOARCH=amd64 GOOS=linux                 go build -o _out/dops_linux-amd64                          ./cmd/dops  # Linux - 64 bit
	GOARCH=arm64 GOOS=linux                 go build -o _out/dops_linux-arm64                          ./cmd/dops  # Linux - ARM
	GOARCH=arm   GOOS=linux   GOARM=5       go build -o _out/dops_linux-arm32v5                        ./cmd/dops  # Linux - ARM32 v5 (e.g. Raspberry 3)
	GOARCH=arm   GOOS=linux   GOARM=6       go build -o _out/dops_linux-arm32v6                        ./cmd/dops  # Linux - ARM32 v6
	GOARCH=arm   GOOS=linux   GOARM=7       go build -o _out/dops_linux-arm32v7                        ./cmd/dops  # Linux - ARM32 v7
	GOARCH=amd64 GOOS=darwin                go build -o _out/dops_macos-amd64                          ./cmd/dops  # macOS - 64 bit
	GOARCH=arm64 GOOS=darwin                go build -o _out/dops_macos-arm64                          ./cmd/dops  # macOS (Apple Silicon)
	GOARCH=amd64 GOOS=openbsd               go build -o _out/dops_openbsd-amd64                        ./cmd/dops  # OpenBSD - 64 bit
	GOARCH=arm64 GOOS=openbsd               go build -o _out/dops_openbsd-arm64                        ./cmd/dops  # OpenBSD - ARM
	GOARCH=amd64 GOOS=freebsd               go build -o _out/dops_freebsd-amd64                        ./cmd/dops  # FreeBSD - 64 bit
	GOARCH=arm64 GOOS=freebsd               go build -o _out/dops_freebsd-arm64                        ./cmd/dops  # FreeBSD - ARM

	_data/package-data/aur-git.sh
	_data/package-data/aur-bin.sh
	_data/package-data/homebrew.sh

	echo ""
	echo "[TODO]: call 'make package-push-aur-git'  "
	echo "[TODO]: call 'make package-push-aur-bin'  "
	echo "[TODO]: call 'make package-push-homebrew' "
	echo "[TODO]: create github release"
	echo ""

package-push-aur-git:
	cd _out/dops-git && git push

package-push-aur-bin:
	cd _out/dops-bin && git push

package-push-homebrew:
	cd _out/homebrew-tap && git push


================================================
FILE: README.md
================================================
# ./dops - better `docker ps` 
A replacement for the default docker-ps that tries really hard to fit within your terminal width.

![](readme.d/main.png)

## Rationale

By default, my `docker ps` output is really wide and every line wraps around into three.
This (obviously) breaks the tabular display and makes everything chaotic.  
*(This gets especially bad if one container has multiple port mappings, and they are all displayed in a single row)*
It doesn't look like we'll get improved output in the foreseeable future (see [moby#7477](https://github.com/moby/moby/issues/7477)), so I decided to make my own drop-in replacement.  

## Features

 - All normal commandline flags/options from docker-ps work *(almost)* the same.
 - Write multi-value data (like multiple port mappings, multiple networks, etc.) into multiple lines instead of concatenating them.
 - Add color to the STATE and STATUS column (green / yellow / red).
 - Automatically remove columns in the output until it fits in the current terminal width.
 - sort the output with the `--sort` argument
 - Enter watch mode with the `--watch` argument

More Changes from default docker-ps:
 - Show (by default) the container-cmd without arguments.
 - Show the ImageName (by default) without the registry prefix, and split ImageName and ImageTag into two columns.
 - Added the columns IP and NETWORK to the default column set (if they fit)
 - Added support for a few new columns (via --format):  
   `{{.ImageName}`, `{{.ImageTag}`, `{{.Tag}`, `{{.ImageRegistry}`, `{{.Registry}`, `{{.ShortCommand}`, `{{.LabelKeys}`, `{{.IP}`                         
 - Added options to control the color-output, the used socket, the time-zone and time-format, etc (see `./dops --help`) 

## Getting started

### Generic Linux (e.g. Debian/Fedora/...)
 - Download the latest binary from the [releases page](https://github.com/Mikescher/better-docker-ps/releases) and put it into your PATH (eg /usr/local/bin)
 - You can also use the following one-liner (afterwards you can use the `dops` command everywhere):
```
sudo wget "https://github.com/Mikescher/better-docker-ps/releases/latest/download/dops_linux-amd64-static" -O "/usr/local/bin/dops" && sudo chmod +x "/usr/local/bin/dops"
```

### ArchLinux
 - Alternatively you can use one of the AUR packages (under Arch Linux):
    * https://aur.archlinux.org/packages/dops-bin (installs `dops` into your PATH)
    * https://aur.archlinux.org/packages/dops-git (installs `dops` into your PATH)
 - or the homebrew package: 
    * `brew tap mikescher/tap && brew install dops`

### Optional steps
 - Alias the docker ps command to `dops` (see [section below](#usage-as-drop-in-replacement))

### Building from source

If you want to build `dops` from source, you need to have Go installed.

```sh
git clone https://github.com/Mikescher/better-docker-ps.git
cd better-docker-ps
make build
mv _out/dops "$HOME/.local/bin/"
```

## Screenshots

![](readme.d/fullsize.png)  
All (default) columns visible

&nbsp;

![](readme.d/default.png)  
Output on a medium sized terminal

&nbsp;

![](readme.d/small.png)  
Output on a small terminal

&nbsp;

## Usage as drop-in replacement

You can fully replace docker ps by creating a shell function in your `.bashrc` / `.zshrc`...

~~~sh
docker() {
  case $1 in
    ps)
      shift
      command dops "$@"
      ;;
    *)
      command docker "$@";;
  esac
}
~~~

This will alias every call to `docker ps ...` with `dops ...` (be sure to have the dops binary in your PATH).

If you are using the fish-shell you have to create a (similar) function:

~~~fish
function docker
    if test -n "$argv[1]"
        switch $argv[1]
            case ps
                dops $argv[2..-1]
            case '*'
                command docker $argv[1..-1]
        end
    end
end
~~~

## Changing the output format

By default dops tries to be "intelligent" and find the best output format for your terminal width.
The current output formats (= table columns) are defined in the [options.go](https://github.com/Mikescher/better-docker-ps/blob/master/cli/options.go).
The first format that fits in your terminal width is used.

But you can also override it by supplying a `--format` parameter. If you supply more than one `--format` parameter the first one that fits your terminal is used (same logic as with the default ones...)

Normally only simple columns aka `{{.Status}}` are supported.  
But you can also use the full golang template syntax (e.g. `{{ printf "%.15s" .Command }}`).
In this case it can be useful to specify the column header by prefixing it with a colon (`SHORTENED NAME:{{ printf "%.10s" (join .Names ";") }}`)

The following functions are defined in these templates (plus the [default go functions](https://pkg.go.dev/text/template)):
 - `join`: strings.Join
 - `array_last`: v\[-1\]
 - `array_slice`: v\[a..b\] 
 - `in_array`: v1.contains(v2)
 - `json`: json.Marshal(v)
 - `json_indent`: json.MarshalIndent(v, "", "  ")
 - `json_pretty`:  json.Indent(v, "", "  ")
 - `coalesce`: v1 ?? v2
 - `to_string`: fmt.Sprintf("%v", v)
 - `deref`: *v
 - `now`: time.Now()
 - `uniqid`: UUID

Examples:
~~~~
$ ./dops --format "table {{.ID}}"
$ ./dops --format "table {{.ID}}\\t{{.Names}}\\t{{.State}}"

$ ./dops --format "idlist"

$ ./dops --format "table {{.ID}}\\t{{.Names}}\\t{{.State}}"  --format "table {{.ID}}\\t{{.Names}}" --format "table {{.ID}}"

$ ./dops --format "ID: {{.ID}}; Name: {{.Names}}"

$ ./dops -aq

$ ./dops --sort "IP" --sort-direction "ASC"

$ ./dops --format "table {{.ID}}\\tCMD:{{ printf \"%.15s\" .Command }}"
$ ./dops --format "table {{.ID}}\\tNAME:{{ printf \"%.10s\" (join .Names \";\") }}"

~~~~

## Persistant configuration

You can also configure some/most of the options via a configuration file.  
Place a TOML formatted file in `$HOME/.config/dops.conf` / `$XDG_CONFIG_HOME/dops.conf`.  
( `~/Library/Application Support/dops.conf` under macOS )

The following keys are supported:
 - verbose
 - silent
 - timezone
 - timeformat
 - timeformat-header
 - color
 - socket
 - all
 - size
 - filter (= string array)
 - search
 - format (= string array)
 - last
 - latest
 - truncate
 - header (= true / false / simple)
 - sort (= string array)
 - sort-direction (= string array)

Example:
```toml
verbose = 0

timezone = "Europe/Berlin"

format = [
   "table {{.ID}}\t{{.Names}}\t{{.State}}\t{{.Status}}",
   "table {{.ID}}\t{{.Names}}\t{{.State}}",
   "table {{.ID}}\t{{.Names}}",
   "table {{.ID}}",
]

header = "simple"
```

## Manual

Output of `./dops --help`:

~~~~~~
better-docker-ps

Usage:
  dops [OPTIONS]                     List docker container

Options (default):
  -h, --help                         Show this screen.
  --version                          Show version.
  --all , -a                         Show all containers (default shows just running)
  --filter <ftr>, -f <ftr>           Filter output based on conditions provided
  --search <str>, -g <str>           Filter output by substring match across all visible columns (case-insensitive)
  --format <fmt>                     Pretty-print containers using a Go template
  --last , -n                        Show n last created containers (includes all states)
  --latest , -l                      Show the latest created container (includes all states)
  --no-trunc                         Don't truncate output (eg ContainerIDs, Sha256 Image references, commandline)
  --quiet , -q                       Only display container IDs
  --size , -s                        Display total file sizes

Options (extra | do not exist in `docker ps`):
  --silent                           Do not print any output
  --timezone                         Specify the timezone for date outputs
  --color <true|false>               Enable/Disable terminal color output
  --no-color                         Disable terminal color output
  --socket <filepath>                Specify the docker socket location (Default: `auto` - which calls the docker cli to determine the socket)
  --timeformat <go-time-fmt>         Specify the datetime output format (golang syntax)
  --no-header                        Do not print the table header
  --simple-header                    Do not print the lines under the header
  --format <fmt>                     You can specify multiple formats and the first one that fits your terminal widt will be used
  --sort <col>                       Sort output by a specific column, use the same identifier as in --format, only useful together with table formats 
  --sort-direction <ASC|DESC>        The sort direction, only useful in combination with --sort
  --watch <interval>, -w <interval>  Automatically refresh output periodically (interval is optional, default: 2s) 

Available --format keys (default):
  {{.ID}}                            Container ID
  {{.Image}}                         Image ID
  {{.Command}}                       Quoted command
  {{.CreatedAt}}                     Time when the container was created.
  {{.RunningFor}}                    Elapsed time since the container was started.
  {{.Ports}}                         Published ports. ([!] differs from docker CLI, these are only the published ports)
  {{.State}}                         Container status
  {{.Status}}                        Container status with details
  {{.Size}}                          Container disk size.
  {{.Names}}                         Container names.
  {{.Labels}}                        All labels assigned to the container.
  {{.Label}}                         [!] Unsupported
  {{.Mounts}}                        Names of the volumes mounted in this container.
  {{.Networks}}                      Names of the networks attached to this container.

Available --format keys (extra | do not exist in `docker ps`):
  {{.ImageName}}                     Image ID (without tag and registry)
  {{.ImageTag}}, {{.Tag}}            Image Tag
  {{.ImageRegistry}}, {{.Registry}}  Image Registry
  {{.ShortCommand}}                  Command without arguments
  {{.LabelKeys}}                     All labels assigned to the container (keys only)
  {{.ShortPublishedPorts}}           Published ports, shorter output than {{.Ports}}
  {{.LongPublishedPorts}}            Published ports, full output with IP
  {{.ExposedPorts}}                  Exposed ports
  {{.PublishedPorts}}                Published ports
  {{.NotPublishedPorts}}             Exposed but not published ports
  {{.PublicPorts}}                   Only the public part of published ports
  {{.IP}}                            Internal IP Address
~~~~~~


================================================
FILE: _data/package-data/aur-bin/.gitignore
================================================
pkg/
src/
dops/
dops-bin/
dops-git/
.SRCINFO
*.tar.zst

================================================
FILE: _data/package-data/aur-bin/PKGBUILD
================================================
# Maintainer: Mikescher <aur@mikescher.com>
# Repo:       https://github.com/Mikescher/better-docker-ps

pkgname=dops-bin
pkgver=1.13
pkgrel=1

pkgdesc="A replacement for the default docker-ps that tries really hard to fit into the width of your terminal."

url="https://github.com/Mikescher/better-docker-ps"
license=('Apache')

arch=('x86_64')

_binary="dops_linux-amd64"

source=(
  "https://github.com/Mikescher/better-docker-ps/releases/download/v${pkgver}/${_binary}"
)

_bin_sha='48addb268291151b0dd6752675829dc3ba81523d1515d6094ccf18269e26dfd6'

sha256sums=(
  "$_bin_sha"
)

package()
{
  install -D -m755 "$srcdir/${_binary}" "${pkgdir}/usr/bin/dops"
}



================================================
FILE: _data/package-data/aur-bin.sh
================================================
#!/bin/bash

set -o nounset   # disallow usage of unset vars  ( set -u )
set -o errexit   # Exit immediately if a pipeline returns non-zero.  ( set -e )
set -o errtrace  # Allow the above trap be inherited by all functions in the script.  ( set -E )
set -o pipefail  # Return value of a pipeline is the value of the last (rightmost) command to exit with a non-zero status
IFS=$'\n\t'      # Set $IFS to only newline and tab.

cd "$(dirname "$0")/aur-bin"
git clean -ffdX

version="$(cd ../../../ && git tag --sort=-v:refname | grep -P 'v[0-9\.]' | head -1 | cut -c2-)"
cs0="$(cd ../../../ && sha256sum _out/dops_linux-amd64 | cut -d ' ' -f 1)"

echo "Version: ${version} (${cs0})"

sed --regexp-extended  -i "s/pkgver=[0-9\.]+/pkgver=${version}/g"         PKGBUILD
sed --regexp-extended  -i "s/_bin_sha='[A-Za-z0-9]+'/_bin_sha='${cs0}'/g" PKGBUILD

namcap PKGBUILD
makepkg --printsrcinfo > .SRCINFO
# makepkg #(do not makepkg, release is probably not live)


cd ../../../
git clone ssh://aur@aur.archlinux.org/dops-bin.git _out/dops-bin
cp -v _data/package-data/aur-bin/PKGBUILD _out/dops-bin/PKGBUILD
cp -v _data/package-data/aur-bin/.SRCINFO _out/dops-bin/.SRCINFO



cd _out/dops-bin

git add PKGBUILD
git add .SRCINFO

if [ -z "$(git status --porcelain)" ]; then 
  echo "(!) Nothing changed -- nothing to commit"
else 
  git commit -m "v${version}"
fi


cd "../../_data/package-data/aur-bin"
git clean -ffdX

# git push manually (!)


================================================
FILE: _data/package-data/aur-git/.gitignore
================================================
pkg/
src/
dops/
dops-bin/
dops-git/
.SRCINFO
*.tar.zst

================================================
FILE: _data/package-data/aur-git/PKGBUILD
================================================
# Maintainer: Mikescher <aur@mikescher.com>
# Repo:       https://github.com/Mikescher/better-docker-ps

pkgname=dops-git
pkgver=1.13
pkgrel=1

pkgdesc="A replacement for the default docker-ps that tries really hard to fit into the width of your terminal."

url="https://github.com/Mikescher/better-docker-ps"
license=('Apache')

makedepends=('go' 'git')

arch=('any')

source=("$pkgname::git+https://github.com/Mikescher/better-docker-ps")

sha256sums=('SKIP')

build() {
  cd "$pkgname"
  go build -o dops  cmd/dops/main.go
}

package()
{
  install -D -m755 "$pkgname/dops" "${pkgdir}/usr/bin/dops"
}



================================================
FILE: _data/package-data/aur-git.sh
================================================
#!/bin/bash

set -o nounset   # disallow usage of unset vars  ( set -u )
set -o errexit   # Exit immediately if a pipeline returns non-zero.  ( set -e )
set -o errtrace  # Allow the above trap be inherited by all functions in the script.  ( set -E )
set -o pipefail  # Return value of a pipeline is the value of the last (rightmost) command to exit with a non-zero status
IFS=$'\n\t'      # Set $IFS to only newline and tab.

cd "$(dirname "$0")/aur-git"
git clean -ffdX

version=$(cd ../../../ && git tag --sort=-v:refname | grep -P 'v[0-9\.]' | head -1 | cut -c2-)

echo "Version: ${version}"

sed --regexp-extended  -i "s/pkgver=[0-9\.]+/pkgver=${version}/g" PKGBUILD



namcap PKGBUILD
makepkg --printsrcinfo > .SRCINFO
makepkg


cd ../../../
pwd
git clone ssh://aur@aur.archlinux.org/dops-git.git _out/dops-git
cp _data/package-data/aur-git/PKGBUILD _out/dops-git/PKGBUILD
cp _data/package-data/aur-git/.SRCINFO _out/dops-git/.SRCINFO


cd _out/dops-git

git add PKGBUILD
git add .SRCINFO

if [ -z "$(git status --porcelain)" ]; then 
  echo "(!) Nothing changed -- nothing to commit"
else 
  git commit -m "v${version}"
fi


cd "../../_data/package-data/aur-git"
git clean -ffdX

# git push manually (!)


================================================
FILE: _data/package-data/homebrew/dops.rb
================================================
class Dops < Formula

  desc     " A replacement for the default docker-ps that tries really hard to fit into the width of your terminal. "
  homepage "https://github.com/Mikescher/better-docker-ps"
  url      "https://github.com/Mikescher/better-docker-ps/releases/download/v<<version>>/dops_macos-arm64"
  sha256   "<<shahash>>"

  def install
    bin.install "dops_macos-arm64" => "dops"
  end

  test do
    assert true
  end

end


================================================
FILE: _data/package-data/homebrew.sh
================================================
#!/bin/bash

set -o nounset   # disallow usage of unset vars  ( set -u )
set -o errexit   # Exit immediately if a pipeline returns non-zero.  ( set -e )
set -o errtrace  # Allow the above trap be inherited by all functions in the script.  ( set -E )
set -o pipefail  # Return value of a pipeline is the value of the last (rightmost) command to exit with a non-zero status
IFS=$'\n\t'      # Set $IFS to only newline and tab.

set -o functrace

cd "$(dirname "$0")/homebrew"

cp dops.rb dops_patch.rb


version="$(cd ../../../ && git tag --sort=-v:refname | grep -P 'v[0-9\.]' | head -1 | cut -c2-)"
cs0="$(cd ../../../ && sha256sum _out/dops_macos-arm64 | cut -d ' ' -f 1)"

echo "Version: ${version} (${cs0})"

sed --regexp-extended  -i "s/<<version>>/${version}/g"  dops_patch.rb
sed --regexp-extended  -i "s/<<shahash>>/${cs0}/g"      dops_patch.rb

cd ../../../
git clone https://github.com/Mikescher/homebrew-tap.git _out/homebrew-tap

cp "_data/package-data/homebrew/dops_patch.rb" _out/homebrew-tap/dops.rb
rm "_data/package-data/homebrew/dops_patch.rb"


cd _out/homebrew-tap/

git add dops.rb

if [ -z "$(git status --porcelain)" ]; then 
  echo "(!) Nothing changed -- nothing to commit"
else 
  git commit -m "dops v${version}"
fi



# git push manually (!)


================================================
FILE: _data/package-data/sanitycheck.sh
================================================
#!/bin/bash

cd "$(dirname "$0")"

version_tag="$(cd ../../ && git tag --sort=-v:refname | grep -P 'v[0-9\.]' | head -1 | cut -c2-)"

version_code="$(cd ../../ && cat consts/version.go | grep -oP 'BETTER_DOCKER_PS_VERSION = .*' | grep -oP '"[0-9\.]+"' | grep -oP '[0-9\.]+' )"


if [ "$version_tag" != "$version_code" ]; then

  echo "Git-Tag version and Code-const version do not match!"
  echo "[GIT-TAG] := $version_tag"
  echo "[GO-CODE] := $version_code"

  exit 1

else

  echo "Version ('$version_tag') ok"

fi

================================================
FILE: cli/argTuple.go
================================================
package cli

type ArgumentTuple struct {
	Key   string
	Value *string
}


================================================
FILE: cli/context.go
================================================
package cli

import (
	"better-docker-ps/pserr"
	"context"
	"encoding/hex"
	"fmt"
	"os"
	"strings"
	"time"

	"git.blackforestbytes.com/BlackForestBytes/goext/langext"
	"git.blackforestbytes.com/BlackForestBytes/goext/termext"
)

type PSContext struct {
	context.Context
	Opt   Options
	Cache map[string]any
}

func (c PSContext) PrintPrimaryOutput(msg string) {
	if c.Opt.Quiet {
		return
	}

	c.printPrimaryRaw(msg + "\n")
}

func (c PSContext) PrintFatalMessage(msg string) {
	if c.Opt.Quiet {
		return
	}

	c.printErrorRaw(msg + "\n")
}

func (c PSContext) PrintFatalError(e error) {
	if c.Opt.Quiet {
		return
	}

	c.printErrorRaw(pserr.FormatError(e, c.Opt.Verbose) + "\n")
}

func (c PSContext) PrintErrorMessage(msg string) {
	if c.Opt.Quiet {
		return
	}

	c.printErrorRaw(msg + "\n")
}

func (c PSContext) PrintVerbose(msg string) {
	if c.Opt.Quiet || !c.Opt.Verbose {
		return
	}

	c.printVerboseRaw(msg + "\n")
}

func (c PSContext) PrintVerboseHeader(msg string) {
	if c.Opt.Quiet || !c.Opt.Verbose {
		return
	}

	c.printVerboseRaw("\n")
	c.printVerboseRaw("========================================" + "\n")
	c.printVerboseRaw(msg + "\n")
	c.printVerboseRaw("========================================" + "\n")
	c.printVerboseRaw("\n")
}

func (c PSContext) PrintVerboseKV(key string, vval any) {
	if c.Opt.Quiet || !c.Opt.Verbose {
		return
	}

	termlen := 236
	keylen := 28

	var val = ""
	switch v := vval.(type) {
	case []byte:
		val = hex.EncodeToString(v)
	case string:
		val = v
	case time.Time:
		val = v.In(c.Opt.TimeZone).Format(time.RFC3339Nano)
	default:
		val = fmt.Sprintf("%v", v)
	}

	if len(val) > (termlen-keylen-4) || strings.Contains(val, "\n") {

		c.printVerboseRaw(key + " :=\n" + val + "\n")

	} else {

		padkey := langext.StrPadRight(key, " ", keylen)
		c.printVerboseRaw(padkey + " := " + val + "\n")

	}
}

func (c PSContext) ClearTerminal() {
	fmt.Print("\033[H\033[2J")
}

func (c PSContext) printPrimaryRaw(msg string) {
	if c.Opt.Quiet {
		return
	}

	writeStdout(msg)
}

func (c PSContext) printErrorRaw(msg string) {
	if c.Opt.Quiet {
		return
	}

	if c.Opt.OutputColor {
		writeStderr(termext.Red(msg))
	} else {
		writeStderr(msg)
	}
}

func (c PSContext) printVerboseRaw(msg string) {
	if c.Opt.Quiet {
		return
	}

	if c.Opt.OutputColor {
		writeStdout(termext.Gray(msg))
	} else {
		writeStdout(msg)
	}
}

func writeStdout(msg string) {
	_, err := os.Stdout.WriteString(msg)
	if err != nil {
		panic("failed to write to stdout: " + err.Error())
	}
}

func writeStderr(msg string) {
	_, err := os.Stderr.WriteString(msg)
	if err != nil {
		panic("failed to write to stdout: " + err.Error())
	}
}

func NewContext(opt Options) (*PSContext, error) {
	return &PSContext{
		Context: context.Background(),
		Opt:     opt,
		Cache:   make(map[string]any),
	}, nil
}

func NewEarlyContext() *PSContext {
	return &PSContext{
		Context: context.Background(),
		Opt:     DefaultCLIOptions(),
		Cache:   make(map[string]any),
	}
}

func (c PSContext) Finish() {
	// ...
}

func (c *PSContext) GetIntFromCache(key string, calc func() int) int {
	if v1, ok := c.Cache[key]; ok {
		if v2, ok := v1.(int); ok {
			return v2
		}
		panic(fmt.Sprintf("Wrong type in cache type(%s) = %T  (expected: int)", key, v1))
	}

	val := calc()
	c.Cache[key] = val
	return val
}


================================================
FILE: cli/options.go
================================================
package cli

import (
	"encoding/json"
	"os"
	"path/filepath"
	"regexp"
	"runtime"
	"strings"
	"time"

	"git.blackforestbytes.com/BlackForestBytes/goext/cmdext"
	"git.blackforestbytes.com/BlackForestBytes/goext/termext"
)

type SortDirection string

const (
	SortASC  SortDirection = "ASC"
	SortDESC SortDirection = "DESC"
)

type Options struct {
	Version          bool
	Help             bool
	Socket           string
	Quiet            bool
	Verbose          bool
	OutputColor      bool
	TimeZone         *time.Location
	TimeFormat       string
	TimeFormatHeader string
	Input            *string
	All              bool
	WithSize         bool
	Filter           *map[string][]string
	Search           *string
	Limit            int
	DefaultFormat    bool
	Format           []string // if more than 1 value, we use the later values as fallback for too-small terminal
	PrintHeader      bool
	PrintHeaderLines bool
	Truncate         bool
	SortColumns      []string
	SortDirection    []SortDirection
	WatchInterval    *time.Duration
}

func DefaultCLIOptions() Options {
	return Options{
		Version:          false,
		Help:             false,
		Quiet:            false,
		Verbose:          false,
		OutputColor:      termext.SupportsColors(),
		TimeZone:         time.Local,
		TimeFormatHeader: "Z07:00 MST",
		TimeFormat:       "2006-01-02 15:04:05",
		Socket:           "auto",
		Input:            nil,
		All:              false,
		WithSize:         false,
		Limit:            -1,
		DefaultFormat:    true,
		Format: []string{
			"table {{.ID}}\\t{{.Names}}\\t{{.ImageName}}\\t{{.Tag}}\\t{{.ShortCommand}}\\t{{.CreatedAt}}\\t{{.State}}\\t{{.Status}}\\t{{.LongPublishedPorts}}\\t{{.Networks}}\\t{{.IP}}",
			"table {{.ID}}\\t{{.Names}}\\t{{.ImageName}}\\t{{.Tag}}\\t{{.ShortCommand}}\\t{{.CreatedAt}}\\t{{.State}}\\t{{.Status}}\\t{{.LongPublishedPorts}}\\t{{.IP}}",
			"table {{.ID}}\\t{{.Names}}\\t{{.ImageName}}\\t{{.Tag}}\\t{{.CreatedAt}}\\t{{.State}}\\t{{.Status}}\\t{{.LongPublishedPorts}}\\t{{.IP}}",
			"table {{.ID}}\\t{{.Names}}\\t{{.ImageName}}\\t{{.Tag}}\\t{{.CreatedAt}}\\t{{.State}}\\t{{.Status}}\\t{{.PublishedPorts}}\\t{{.IP}}",
			"table {{.ID}}\\t{{.Names}}\\t{{.ImageName}}\\t{{.Tag}}\\t{{.CreatedAt}}\\t{{.State}}\\t{{.Status}}\\t{{.PublishedPorts}}",
			"table {{.ID}}\\t{{.Names}}\\t{{.ImageName}}\\t{{.Tag}}\\t{{.State}}\\t{{.Status}}\\t{{.PublishedPorts}}",
			"table {{.ID}}\\t{{.Names}}\\t{{.Tag}}\\t{{.State}}\\t{{.Status}}\\t{{.PublishedPorts}}",
			"table {{.ID}}\\t{{.Names}}\\t{{.Tag}}\\t{{.State}}\\t{{.Status}}\\t{{.ShortPublishedPorts}}",
			"table {{.ID}}\\t{{.Names}}\\t{{.Tag}}\\t{{.State}}\\t{{.Status}}",
			"table {{.ID}}\\t{{.Names}}\\t{{.State}}\\t{{.Status}}",
			"table {{.ID}}\\t{{.Names}}\\t{{.State}}",
			"table {{.Names}}\\t{{.State}}",
			"table {{.Names}}",
			"table {{.ID}}",
		},
		PrintHeader:      true,
		PrintHeaderLines: true,
		Truncate:         true,
		SortColumns:      make([]string, 0),
		SortDirection:    make([]SortDirection, 0),
		WatchInterval:    nil,
	}
}

func getDefaultSocket() string {
	if runtime.GOOS == "darwin" {
		home, err := os.UserHomeDir()
		if err != nil {
			return "/var/run/docker.sock"
		}
		return filepath.Join(home, ".docker/run/docker.sock")
	}
	return "/var/run/docker.sock"
}

func (o Options) GetSocket() string {

	// [1] Manually specified socket

	if o.Socket != "auto" {
		return o.Socket
	}

	// [2] Auto-detect from current docker context

	res, err := cmdext.Runner("docker").Arg("context").Arg("list").Arg("--format").Arg("json").Timeout(10 * time.Second).FailOnTimeout().FailOnExitCode().Run()
	if err == nil {
		for _, line := range strings.Split(res.StdOut, "\n") {
			var context dockerContext
			err = json.Unmarshal([]byte(line), &context)
			if err != nil {
				continue
			}
			if context.Current {
				return context.socket()
			}
		}
	}

	// [3] MacOS homedir

	if runtime.GOOS == "darwin" {
		if home, err := os.UserHomeDir(); err == nil {
			fp := filepath.Join(home, ".docker/run/docker.sock")
			if _, err = os.Stat(fp); err == nil {
				return fp
			}
		}
	}

	// [4] Default

	return "/var/run/docker.sock"
}

type dockerContext struct {
	Name           string
	Description    string
	DockerEndpoint string
	Current        bool
	Error          string
	ContextType    string
}

var unixSocketPrefixPat = regexp.MustCompile("^unix://")

func (ctx dockerContext) socket() string {
	return unixSocketPrefixPat.ReplaceAllString(ctx.DockerEndpoint, "")
}

func p(v bool) *bool {
	return &v
}


================================================
FILE: cli/parser.go
================================================
package cli

import (
	"better-docker-ps/pserr"
	"fmt"
	"github.com/BurntSushi/toml"
	"github.com/kirsle/configdir"
	"git.blackforestbytes.com/BlackForestBytes/goext/timeext"
	"os"
	"path/filepath"
	"strconv"
	"strings"
	"time"

	"github.com/joomcode/errorx"
	"git.blackforestbytes.com/BlackForestBytes/goext/langext"
)

func ParseCommandline(columnKeys []string) (Options, error) {
	o, err := parseCommandlineInternal(columnKeys)
	if err != nil {
		return Options{}, errorx.Decorate(err, "failed to parse commandline")
	}
	return o, nil
}

func parseCommandlineInternal(columnKeys []string) (Options, error) {
	unprocessedArgs := os.Args[1:]

	allOptionArguments := make([]ArgumentTuple, 0)

	// Parse Commandline KeyValue pairs

	for len(unprocessedArgs) > 0 {
		arg := unprocessedArgs[0]
		unprocessedArgs = unprocessedArgs[1:]

		if strings.HasPrefix(arg, "--") {

			arg = arg[2:]

			if strings.Contains(arg, "=") {
				key := arg[0:strings.Index(arg, "=")]
				val := arg[strings.Index(arg, "=")+1:]

				if len(key) <= 1 {
					return Options{}, pserr.DirectOutput.New("Unknown/Misplaced argument: " + arg)
				}

				allOptionArguments = append(allOptionArguments, ArgumentTuple{Key: key, Value: langext.Ptr(val)})
				continue
			} else {

				key := arg

				if len(key) <= 1 {
					return Options{}, pserr.DirectOutput.New("Unknown/Misplaced argument: " + arg)
				}

				if len(unprocessedArgs) == 0 || strings.HasPrefix(unprocessedArgs[0], "-") {
					allOptionArguments = append(allOptionArguments, ArgumentTuple{Key: key, Value: nil})
					continue
				} else {
					val := unprocessedArgs[0]
					unprocessedArgs = unprocessedArgs[1:]
					allOptionArguments = append(allOptionArguments, ArgumentTuple{Key: key, Value: langext.Ptr(val)})
					continue
				}

			}

		} else if strings.HasPrefix(arg, "-") {

			arg = arg[1:]

			if len(arg) > 1 {
				for i := 0; i < len(arg); i++ {
					allOptionArguments = append(allOptionArguments, ArgumentTuple{Key: arg[i : i+1], Value: nil})
				}
				continue
			}

			key := arg

			if key == "" {
				return Options{}, pserr.DirectOutput.New("Unknown/Misplaced argument: " + arg)
			}

			if len(unprocessedArgs) == 0 || strings.HasPrefix(unprocessedArgs[0], "-") {
				allOptionArguments = append(allOptionArguments, ArgumentTuple{Key: key, Value: nil})
				continue
			} else {
				val := unprocessedArgs[0]
				unprocessedArgs = unprocessedArgs[1:]
				allOptionArguments = append(allOptionArguments, ArgumentTuple{Key: key, Value: langext.Ptr(val)})
				continue
			}

		} else {
			return Options{}, pserr.DirectOutput.New("Unknown/Misplaced argument: " + arg)
		}
	}

	// Process common options

	opt := DefaultCLIOptions()

	confPath := filepath.Join(configdir.LocalConfig(), "dops.conf")

	if v, err := os.ReadFile(confPath); err == nil {

		tomldata := make(map[string]any)
		_, err = toml.Decode(string(v), &tomldata)
		if err != nil {
			return Options{}, pserr.DirectOutput.New("Failed to parse config file '" + confPath + "': " + err.Error())
		}

		for tk, tvany := range tomldata {

			tv := fmt.Sprintf("%v", tvany)
			var tvarr []string = nil
			if varr1, ok := tvany.([]string); ok {
				tvarr = varr1
			} else if varr2, ok := tvany.([]any); ok && langext.ArrAll(varr2, func(v any) bool { _, ok := v.(string); return ok }) {
				tvarr = langext.ArrCastSafe[any, string](varr2)
			} else {
				tvarr = []string{tv}
			}

			if tk == "verbose" {
				opt.Verbose, err = strconv.ParseBool(tv)
				if err != nil {
					return Options{}, pserr.DirectOutput.New("Failed to parse config file '" + confPath + "' (invalid value for 'verbose'): " + err.Error())
				}
			} else if tk == "silent" {
				opt.Quiet, err = strconv.ParseBool(tv)
				if err != nil {
					return Options{}, pserr.DirectOutput.New("Failed to parse config file '" + confPath + "' (invalid value for 'silent'): " + err.Error())
				}
			} else if tk == "timezone" {
				loc, err := time.LoadLocation(tv)
				if err != nil {
					return Options{}, pserr.DirectOutput.New("Failed to parse config file '" + confPath + ": Unknown timezone: " + tv)
				}
				opt.TimeZone = loc
			} else if tk == "timeformat" {
				opt.TimeFormat = tv
			} else if tk == "timeformat-header" {
				opt.TimeFormatHeader = tv
			} else if tk == "color" {
				opt.OutputColor, err = strconv.ParseBool(tv)
				if err != nil {
					return Options{}, pserr.DirectOutput.New("Failed to parse config file '" + confPath + "' (invalid value for 'color'): " + err.Error())
				}
			} else if tk == "socket" {
				opt.Socket = tv
			} else if tk == "all" {
				opt.All, err = strconv.ParseBool(tv)
				if err != nil {
					return Options{}, pserr.DirectOutput.New("Failed to parse config file '" + confPath + "' (invalid value for 'all'): " + err.Error())
				}
			} else if tk == "size" {
				opt.WithSize, err = strconv.ParseBool(tv)
				if err != nil {
					return Options{}, pserr.DirectOutput.New("Failed to parse config file '" + confPath + "' (invalid value for 'size'): " + err.Error())
				}
			} else if tk == "filter" {
				for _, elem := range tvarr {
					spl := strings.SplitN(elem, "=", 2)
					if len(spl) != 2 {
						return Options{}, pserr.DirectOutput.New("Failed to parse config file '" + confPath + "': Filter value must have a key and a value (a=b): " + elem)
					}
					if opt.Filter == nil {
						_v := make(map[string][]string)
						opt.Filter = &_v
					}
					filter := *opt.Filter
					filter[spl[0]] = []string{spl[1]}

					opt.Filter = &filter
				}
			} else if tk == "search" {
				opt.Search = langext.Ptr(tv)
			} else if tk == "format" {
				for _, elem := range tvarr {
					if opt.DefaultFormat {
						opt.Format = make([]string, 0)
					}
					opt.Format = append(opt.Format, elem)
					opt.DefaultFormat = false
				}
			} else if tk == "last" {
				if vint, err := strconv.ParseInt(tv, 10, 32); err == nil {
					opt.Limit = int(vint)
					opt.All = true
					continue
				} else {
					return Options{}, pserr.DirectOutput.New("Failed to parse config file '" + confPath + "': Failed to parse number of field 'last': '" + tv + "'")
				}

			} else if tk == "latest" {
				vbool, err := strconv.ParseBool(tv)
				if err != nil {
					return Options{}, pserr.DirectOutput.New("Failed to parse config file '" + confPath + "': Failed to parse boolean value of 'latest': '" + tv + "'")
				}
				if vbool {
					opt.Limit = -1
					opt.All = true
				} else {
					opt.Limit = 1
					opt.All = false
				}
			} else if tk == "truncate" {
				opt.Truncate, err = strconv.ParseBool(tv)
				if err != nil {
					return Options{}, pserr.DirectOutput.New("Failed to parse config file '" + confPath + "' (invalid value for 'truncate'): " + err.Error())
				}
			} else if tk == "header" {
				if strings.EqualFold(tv, "no") {
					opt.PrintHeader = false
				} else if strings.EqualFold(tv, "simple") {
					opt.PrintHeader = true
					opt.PrintHeaderLines = false
				} else {
					vbool, err := strconv.ParseBool(tv)
					if err != nil {
						return Options{}, pserr.DirectOutput.New("Failed to parse config file '" + confPath + "': Failed to parse boolean value of 'latest': '" + tv + "'")
					}
					if vbool {
						opt.PrintHeader = true
						opt.PrintHeaderLines = true
					} else {
						opt.PrintHeader = false
						opt.PrintHeaderLines = false
					}
				}
			} else if tk == "sort" {
				opt.SortColumns = tvarr
			} else if tk == "sort-direction" {
				opt.SortDirection = make([]SortDirection, 0)
				for _, sdv := range tvarr {
					if strings.ToUpper(sdv) == "ASC" {
						opt.SortDirection = append(opt.SortDirection, SortASC)
						continue
					}
					if strings.ToUpper(sdv) == "DESC" {
						opt.SortDirection = append(opt.SortDirection, SortDESC)
						continue
					}
					return Options{}, pserr.DirectOutput.New(fmt.Sprintf("Failed to parse config file '"+confPath+"': Failed to parse sort-direction argument '%s'", sdv))
				}
			} else {
				return Options{}, pserr.DirectOutput.New("Failed to parse config file '" + confPath + "' (unknown key '" + tk + "')")
			}
		}
	}

	for _, arg := range allOptionArguments {

		if (arg.Key == "h" || arg.Key == "help") && arg.Value == nil {
			return Options{Help: true}, nil
		}

		if arg.Key == "version" && arg.Value == nil {
			return Options{Version: true}, nil
		}

		if (arg.Key == "v" || arg.Key == "verbose") && arg.Value == nil {
			opt.Verbose = true
			continue
		}

		if (arg.Key == "silent") && arg.Value == nil {
			opt.Quiet = true
			continue
		}

		if (arg.Key == "q" || arg.Key == "quiet") && arg.Value == nil {
			opt.Format = []string{"idlist"}
			opt.DefaultFormat = false
			continue
		}

		if arg.Key == "timezone" && arg.Value != nil {
			loc, err := time.LoadLocation(*arg.Value)
			if err != nil {
				return Options{}, pserr.DirectOutput.New("Unknown timezone: " + *arg.Value)
			}
			opt.TimeZone = loc
			continue
		}

		if arg.Key == "timeformat" && arg.Value != nil {
			opt.TimeFormat = *arg.Value
			opt.TimeFormatHeader = ""
			continue
		}

		if arg.Key == "timeformat-header" && arg.Value != nil {
			opt.TimeFormatHeader = *arg.Value
			continue
		}

		if arg.Key == "color" && arg.Value != nil && (strings.ToLower(*arg.Value) == "true" || *arg.Value == "1") {
			opt.OutputColor = true
			continue
		}

		if arg.Key == "color" && arg.Value != nil && (strings.ToLower(*arg.Value) == "false" || *arg.Value == "0") {
			opt.OutputColor = false
			continue
		}

		if arg.Key == "no-color" && arg.Value == nil {
			opt.OutputColor = false
			continue
		}

		if (arg.Key == "socket") && arg.Value != nil {
			opt.Socket = *arg.Value
			continue
		}

		if (arg.Key == "input") && arg.Value != nil {
			// used for testing
			opt.Input = langext.Ptr(*arg.Value)
			continue
		}

		if (arg.Key == "all" || arg.Key == "a") && arg.Value == nil {
			opt.All = true
			continue
		}

		if (arg.Key == "size") && arg.Value == nil {
			opt.WithSize = true
			continue
		}

		if (arg.Key == "filter" || arg.Key == "f") && arg.Value != nil {
			spl := strings.SplitN(*arg.Value, "=", 2)
			if len(spl) != 2 {
				return Options{}, pserr.DirectOutput.New("Filter argument must have a key and a value (a=b): " + arg.Key)
			}
			if opt.Filter == nil {
				_v := make(map[string][]string)
				opt.Filter = &_v
			}
			filter := *opt.Filter
			if spl[0] == "project" {
				spl[0] = "label"
				spl[1] = "com.docker.compose.project=" + spl[1]
			}
			filter[spl[0]] = []string{spl[1]}
			opt.Filter = &filter
			continue
		}

		if (arg.Key == "search" || arg.Key == "g") && arg.Value != nil {
			opt.Search = langext.Ptr(*arg.Value)
			continue
		}

		if (arg.Key == "format") && arg.Value != nil {
			if opt.DefaultFormat {
				opt.Format = make([]string, 0)
			}
			opt.Format = append(opt.Format, *arg.Value)
			opt.DefaultFormat = false
			continue
		}

		if (arg.Key == "last" || arg.Key == "n") && arg.Value != nil {
			if v, err := strconv.ParseInt(*arg.Value, 10, 32); err == nil {
				opt.Limit = int(v)
				opt.All = true
				continue
			}
			return Options{}, pserr.DirectOutput.New("Failed to parse number argument '--last': '" + *arg.Value + "'")
		}

		if (arg.Key == "latest" || arg.Key == "l") && arg.Value != nil {
			opt.Limit = 1
			opt.All = true
			continue
		}

		if (arg.Key == "no-trunc" || arg.Key == "no-truncate") && arg.Value == nil {
			opt.Truncate = false
			continue
		}

		if (arg.Key == "no-header") && arg.Value == nil {
			opt.PrintHeader = false
			continue
		}

		if (arg.Key == "simple-header") && arg.Value == nil {
			opt.PrintHeaderLines = false
			continue
		}

		if arg.Key == "sort" && arg.Value != nil {
			opt.SortColumns = strings.Split(*arg.Value, ",")
			continue
		}

		if arg.Key == "sort-direction" && arg.Value != nil {
			opt.SortDirection = make([]SortDirection, 0)
			for _, sdv := range strings.Split(*arg.Value, ",") {
				if strings.ToUpper(sdv) == "ASC" {
					opt.SortDirection = append(opt.SortDirection, SortASC)
					continue
				}
				if strings.ToUpper(sdv) == "DESC" {
					opt.SortDirection = append(opt.SortDirection, SortDESC)
					continue
				}
				return Options{}, pserr.DirectOutput.New(fmt.Sprintf("Failed to parse sort-direction argument '%s'", sdv))
			}
			continue
		}

		if arg.Key == "watch" || arg.Key == "w" {
			d, err := timeext.ParseDurationShortString(langext.Coalesce(arg.Value, "2s"))
			if err != nil {
				return Options{}, pserr.DirectOutput.New("Failed to parse duration argument of '--watch': '" + *arg.Value + "'")
			}
			opt.WatchInterval = &d
			continue
		}

		return Options{}, pserr.DirectOutput.New("Unknown argument: " + arg.Key)
	}

	// Post Processing

	if len(opt.SortDirection) == 0 && len(opt.SortColumns) > 0 {
		for i := 0; i < len(opt.SortColumns); i++ {
			opt.SortDirection = append(opt.SortDirection, SortASC) // default sort (if not specified) is ASC on all sort columns
		}
	}

	if len(opt.SortDirection) != len(opt.SortColumns) {
		return Options{}, pserr.DirectOutput.New(fmt.Sprintf("Must specify the same number of values in --sort and --sort-direction ( %d <> %d )", len(opt.SortDirection), len(opt.SortColumns)))
	}

	for _, colkey := range opt.SortColumns {
		if !langext.InArray(colkey, columnKeys) {
			return Options{}, pserr.DirectOutput.New(fmt.Sprintf("Unknown column : '%s' in --sort", colkey))
		}
	}

	return opt, nil
}


================================================
FILE: cmd/dops/main.go
================================================
package main

import (
	"better-docker-ps/cli"
	"better-docker-ps/consts"
	"better-docker-ps/impl"
	"better-docker-ps/pserr"
	"fmt"
	"os"
	"runtime/debug"

	"git.blackforestbytes.com/BlackForestBytes/goext/langext"
)

// Inspiration: https://github.com/moby/moby/issues/7477

func main() {
	defer func() {
		if err := recover(); err != nil {
			_, _ = os.Stderr.WriteString(fmt.Sprintf("%v\n\n%s", err, string(debug.Stack())))
			os.Exit(consts.ExitcodePanic)
		}
	}()

	opt, err := cli.ParseCommandline(langext.MapKeyArr(impl.ColumnMap))
	if err != nil {
		ctx := cli.NewEarlyContext()
		ctx.PrintFatalError(err)
		os.Exit(pserr.GetExitCode(err, consts.ExitcodeCLIParse))
		return
	}

	ctx, err := cli.NewContext(opt)
	if err != nil {
		ctx.PrintFatalError(err)
		os.Exit(pserr.GetExitCode(err, consts.ExitcodeError))
		return
	}

	defer ctx.Finish()

	if opt.Version {
		ctx.PrintPrimaryOutput("better-docker-ps v" + consts.BETTER_DOCKER_PS_VERSION)
		os.Exit(0)
		return
	}

	if opt.Help {
		printHelp(ctx)
		os.Exit(0)
		return
	}

	if opt.WatchInterval == nil {

		err = impl.Execute(ctx)
		if err != nil {
			ctx.PrintFatalError(err)
			os.Exit(pserr.GetExitCode(err, consts.ExitcodeError))
			return
		}

		os.Exit(0)

	} else {

		err = impl.Watch(ctx, *opt.WatchInterval)
		if err != nil {
			ctx.PrintFatalError(err)
			os.Exit(pserr.GetExitCode(err, consts.ExitcodeError))
			return
		}

		os.Exit(0)

	}

}

func printHelp(ctx *cli.PSContext) {
	ctx.PrintPrimaryOutput("better-docker-ps")
	ctx.PrintPrimaryOutput("")
	ctx.PrintPrimaryOutput("Usage:")
	ctx.PrintPrimaryOutput("  dops [OPTIONS]                     List docker container")
	ctx.PrintPrimaryOutput("")
	ctx.PrintPrimaryOutput("Options (default):")
	ctx.PrintPrimaryOutput("  -h, --help                         Show this screen.")
	ctx.PrintPrimaryOutput("  --version                          Show version.")
	ctx.PrintPrimaryOutput("  --all , -a                         Show all containers (default shows just running)")
	ctx.PrintPrimaryOutput("  --filter <ftr>, -f <ftr>           Filter output based on conditions provided")
	ctx.PrintPrimaryOutput("  --search <str>, -g <str>           Filter output by substring match across all visible columns (case-insensitive)")
	ctx.PrintPrimaryOutput("  --format <fmt>                     Pretty-print containers using a Go template")
	ctx.PrintPrimaryOutput("  --last , -n                        Show n last created containers (includes all states)")
	ctx.PrintPrimaryOutput("  --latest , -l                      Show the latest created container (includes all states)")
	ctx.PrintPrimaryOutput("  --no-trunc                         Don't truncate output (eg ContainerIDs, Sha256 Image references, commandline)")
	ctx.PrintPrimaryOutput("  --quiet , -q                       Only display container IDs")
	ctx.PrintPrimaryOutput("  --size , -s                        Display total file sizes")
	ctx.PrintPrimaryOutput("")
	ctx.PrintPrimaryOutput("Options (extra | do not exist in `docker ps`):")
	ctx.PrintPrimaryOutput("  --silent                           Do not print any output")
	ctx.PrintPrimaryOutput("  --timezone                         Specify the timezone for date outputs")
	ctx.PrintPrimaryOutput("  --color <true|false>               Enable/Disable terminal color output")
	ctx.PrintPrimaryOutput("  --no-color                         Disable terminal color output")
	ctx.PrintPrimaryOutput("  --socket <filepath>                Specify the docker socket location (Default: `auto` - which calls the docker cli to determine the socket)")
	ctx.PrintPrimaryOutput("  --timeformat <go-time-fmt>         Specify the datetime output format (golang syntax)")
	ctx.PrintPrimaryOutput("  --no-header                        Do not print the table header")
	ctx.PrintPrimaryOutput("  --simple-header                    Do not print the lines under the header")
	ctx.PrintPrimaryOutput("  --format <fmt>                     You can specify multiple formats and the first one that fits your terminal widt will be used")
	ctx.PrintPrimaryOutput("  --sort <col>                       Sort output by a specific column, use the same identifier as in --format, only useful together with table formats ")
	ctx.PrintPrimaryOutput("  --sort-direction <ASC|DESC>        The sort direction, only useful in combination with --sort")
	ctx.PrintPrimaryOutput("  --watch <interval>                 Automatically refresh output periodically (interval is optional, default: 2s)")
	ctx.PrintPrimaryOutput("")
	ctx.PrintPrimaryOutput("Available --format keys (default):")
	ctx.PrintPrimaryOutput("  {{.ID}}                            Container ID")
	ctx.PrintPrimaryOutput("  {{.Image}}                         Image ID")
	ctx.PrintPrimaryOutput("  {{.Command}}                       Quoted command")
	ctx.PrintPrimaryOutput("  {{.CreatedAt}}                     Time when the container was created.")
	ctx.PrintPrimaryOutput("  {{.RunningFor}}                    Elapsed time since the container was started.")
	ctx.PrintPrimaryOutput("  {{.Ports}}                         Published ports. ([!] differs from docker CLI, these are only the published ports)")
	ctx.PrintPrimaryOutput("  {{.State}}                         Container status")
	ctx.PrintPrimaryOutput("  {{.Status}}                        Container status with details")
	ctx.PrintPrimaryOutput("  {{.Size}}                          Container disk size.")
	ctx.PrintPrimaryOutput("  {{.Names}}                         Container names.")
	ctx.PrintPrimaryOutput("  {{.Labels}}                        All labels assigned to the container.")
	ctx.PrintPrimaryOutput("  {{.Label}}                         [!] Unsupported")
	ctx.PrintPrimaryOutput("  {{.Mounts}}                        Names of the volumes mounted in this container.")
	ctx.PrintPrimaryOutput("  {{.Networks}}                      Names of the networks attached to this container.")
	ctx.PrintPrimaryOutput("")
	ctx.PrintPrimaryOutput("Available --format keys (extra | do not exist in `docker ps`):")
	ctx.PrintPrimaryOutput("  {{.ImageName}                      Image ID (without tag and registry)")
	ctx.PrintPrimaryOutput("  {{.ImageTag}, {{.Tag}              Image Tag")
	ctx.PrintPrimaryOutput("  {{.ImageRegistry}, {{.Registry}    Image Registry")
	ctx.PrintPrimaryOutput("  {{.ShortCommand}                   Command without arguments")
	ctx.PrintPrimaryOutput("  {{.LabelKeys}                      All labels assigned to the container (keys only)")
	ctx.PrintPrimaryOutput("  {{.ShortPublishedPorts}}           Published ports, shorter output than {{.Ports}}")
	ctx.PrintPrimaryOutput("  {{.LongPublishedPorts}}            Published ports, full output with IP")
	ctx.PrintPrimaryOutput("  {{.ExposedPorts}}                  Exposed ports")
	ctx.PrintPrimaryOutput("  {{.NotPublishedPorts}}             Exposed but not published ports")
	ctx.PrintPrimaryOutput("  {{.PublishedPorts}}                Published ports")
	ctx.PrintPrimaryOutput("  {{.PublicPorts}}                   Only the public part of published ports")
	ctx.PrintPrimaryOutput("  {{.IP}                             Internal IP Address")
	ctx.PrintPrimaryOutput("")
}


================================================
FILE: consts/api.go
================================================
package consts

// DockerAPIContainerList -> see https://docs.docker.com/engine/api/v1.41/#tag/Container/operation/ContainerList
const DockerAPIContainerList = "http://localhost/v1.44/containers/json"


================================================
FILE: consts/exitcode.go
================================================
package consts

const (
	ExitcodeError                   = 60
	ExitcodePanic                   = 61
	ExitcodeNoArguments             = 62
	ExitcodeCLIParse                = 63
	ExitcodeNoLogin                 = 64
	ExitcodeUnsupportedOutputFormat = 65
	ExitcodeRecordNotFound          = 66
)

const (
	ExitcodeInvalidSession            = 81
	ExitcodePasswordNotFound          = 82
	ExitcodeParentNotAFolder          = 83
	ExitcodeInvalidPosition           = 84
	ExitcodeBookmarkFieldNotSupported = 85
)


================================================
FILE: consts/version.go
================================================
package consts

//go:generate /bin/bash version.sh

const BETTER_DOCKER_PS_VERSION = "1.17"


================================================
FILE: consts/version.sh
================================================
#!/bin/bash

sed -i 's/const BETTER_DOCKER_PS_VERSION = ".*"/const BETTER_DOCKER_PS_VERSION = "'$(git describe --tags --abbrev=0 | sed "s/v//")'"/' "version.go"


================================================
FILE: docker/api.go
================================================
package docker

import (
	"better-docker-ps/cli"
	"better-docker-ps/consts"
	"better-docker-ps/pserr"
	"context"
	"encoding/json"
	"fmt"
	"io"
	"net"
	"net/http"
	"net/url"
	"os"
	"strconv"
	"strings"

	"github.com/joomcode/errorx"
)

func ListContainer(ctx *cli.PSContext) ([]byte, error) {
	if ctx.Opt.Input != nil {
		data, err := os.ReadFile(*ctx.Opt.Input)
		if err != nil {
			return nil, pserr.DirectOutput.Wrap(err, "Failed to read --input file")
		}
		return data, nil
	}

	socket := ctx.Opt.GetSocket()

	client := http.Client{
		Transport: &http.Transport{
			DialContext: func(_ context.Context, _, _ string) (net.Conn, error) {
				return net.Dial("unix", socket)
			},
		},
	}

	uri := fmt.Sprintf("%s?1=1", consts.DockerAPIContainerList)

	if ctx.Opt.All {
		uri += "&all=true"
	}

	if ctx.Opt.WithSize {
		uri += "&size=true"
	}

	if ctx.Opt.Limit != -1 {
		uri += "&limit=" + strconv.Itoa(ctx.Opt.Limit)
	}

	if ctx.Opt.Filter != nil {
		bin, err := json.Marshal(*ctx.Opt.Filter)
		if err != nil {
			return nil, errorx.InternalError.Wrap(err, "Failed to marshal filter")
		}

		uri += "&filters=" + url.PathEscape(string(bin))
	}

	response, err := client.Get(uri)
	if err != nil {
		if strings.Contains(err.Error(), "connect: permission denied") {
			return nil, pserr.DirectOutput.Wrap(err, "Call to unix socket failed (permission denied)")
		} else {
			return nil, pserr.DirectOutput.Wrap(err, "Call to unix socket failed")
		}
	}

	body, err := io.ReadAll(response.Body)
	if err != nil {
		return nil, errorx.InternalError.Wrap(err, "Failed to read unix socket response")
	}

	return body, nil
}


================================================
FILE: docker/schema.go
================================================
package docker

import (
	"git.blackforestbytes.com/BlackForestBytes/goext/langext"
	"net"
)

type ContainerSchema struct {
	ID              string                   `json:"Id"`
	Names           []string                 `json:"Names"`
	Image           string                   `json:"Image"`
	ImageID         string                   `json:"ImageID"`
	Command         string                   `json:"Command"`
	Created         int64                    `json:"Created"`
	Ports           []PortSchema             `json:"Ports"`
	Labels          map[string]string        `json:"Labels"`
	State           ContainerState           `json:"State"`
	Status          string                   `json:"Status"`
	HostConfig      ContainerHostConfig      `json:"HostConfig"`
	NetworkSettings ContainerNetworkSettings `json:"NetworkSettings"`
	Mounts          []ContainerMount         `json:"Mounts"`
	SizeRw          int64                    `json:"SizeRw"`
	SizeRootFs      int64                    `json:"SizeRootFs"`
}

func (s ContainerSchema) PortsSorted() []PortSchema {
	ports := langext.ArrCopy(s.Ports)

	langext.SortSliceStable(ports, func(p1, p2 PortSchema) bool {
		if p1.PublicPort != p2.PublicPort {
			return p1.PublicPort < p2.PublicPort
		}
		if p1.PrivatePort != p2.PrivatePort {
			return p1.PrivatePort < p2.PrivatePort
		}
		return false
	})

	return ports
}

type ContainerHostConfig struct {
	NetworkMode string `json:"NetworkMode"`
}

type ContainerNetworkSettings struct {
	Networks map[string]ContainerSingleNetworkSettings `json:"Networks"`
}
type ContainerSingleNetworkSettings struct {
	NetworkMode         string `json:"NetworkID"`
	EndpointID          string `json:"EndpointID"`
	Gateway             string `json:"Gateway"`
	IPAddress           string `json:"IPAddress"`
	IPPrefixLen         int    `json:"IPPrefixLen"`
	IPv6Gateway         string `json:"IPv6Gateway"`
	GlobalIPv6Address   string `json:"GlobalIPv6Address"`
	GlobalIPv6PrefixLen int    `json:"GlobalIPv6PrefixLen"`
	MacAddress          string `json:"MacAddress"`
}

type PortSchema struct {
	IP          string `json:"IP"`
	PrivatePort int    `json:"PrivatePort"`
	PublicPort  int    `json:"PublicPort"`
	Type        string `json:"Type"`
}

func (s PortSchema) IsLoopback() bool {
	ip := net.ParseIP(s.IP)
	return ip != nil && ip.IsLoopback()
}

type ContainerMount struct {
	Name        string `json:"Name"`
	Source      string `json:"Source"`
	Destination string `json:"Destination"`
	Driver      string `json:"Driver"`
	Mode        string `json:"Mode"`
	RW          bool   `json:"RW"`
	Propagation string `json:"Propagation"`
}

type ContainerState string

const (
	StateCreated    ContainerState = "created"
	StateRunning    ContainerState = "running"
	StateRestarting ContainerState = "restarting"
	StateExited     ContainerState = "exited"
	StatePaused     ContainerState = "paused"
	StateDead       ContainerState = "dead"
)

func (ct ContainerState) Num() int {
	switch ct {
	case StateCreated:
		return 0
	case StateRunning:
		return 1
	case StateRestarting:
		return 2
	case StateExited:
		return 3
	case StatePaused:
		return 4
	case StateDead:
		return 5
	default:
		return 999
	}
}


================================================
FILE: docker/util.go
================================================
package docker

import (
	"better-docker-ps/cli"
	"strings"
)

var registryPrefixList = []string{
	".com",
	".de",
	".net",
	".io",
	".org",
}

func SplitDockerImage(ctx *cli.PSContext, img string) (string, string, string) {

	resultRegistry := ""
	resultImage := ""
	resultTag := ""

	if v := strings.Split(img, ":"); len(v) > 1 {
		last := v[len(v)-1]
		if !strings.Contains(last, "/") {
			resultTag = last
			img = img[0 : len(img)-len(last)-1]
		}
	}

	if v := strings.Split(img, "/"); len(v) > 1 {
		first := v[0]
		if len(v) == 3 {
			resultRegistry = first
			img = img[len(resultRegistry)+1:]
		} else {
			for _, rpl := range registryPrefixList {
				if strings.HasSuffix(first, rpl) {
					resultRegistry = first
					img = img[len(resultRegistry)+1:]
				}
				break
			}
		}
	}

	resultImage = img

	if resultImage == "sha256" && len(resultTag) == 64 && ctx.Opt.Truncate {
		resultImage = "(sha256)"
		resultTag = resultTag[0:12] + "..."
	}

	return resultRegistry, resultImage, resultTag

}


================================================
FILE: go.mod
================================================
module better-docker-ps

go 1.24.2

require (
	git.blackforestbytes.com/BlackForestBytes/goext v0.0.572
	github.com/BurntSushi/toml v1.4.0
	github.com/joomcode/errorx v1.1.1
	github.com/kirsle/configdir v0.0.0-20170128060238-e45d2f54772f
	golang.org/x/term v0.31.0
)

require (
	github.com/bytedance/sonic v1.13.2 // indirect
	github.com/bytedance/sonic/loader v0.2.4 // indirect
	github.com/cloudwego/base64x v0.1.5 // indirect
	github.com/gabriel-vasile/mimetype v1.4.9 // indirect
	github.com/gin-contrib/sse v1.1.0 // indirect
	github.com/gin-gonic/gin v1.10.0 // indirect
	github.com/go-playground/locales v0.14.1 // indirect
	github.com/go-playground/universal-translator v0.18.1 // indirect
	github.com/go-playground/validator/v10 v10.26.0 // indirect
	github.com/goccy/go-json v0.10.5 // indirect
	github.com/json-iterator/go v1.1.12 // indirect
	github.com/klauspost/cpuid/v2 v2.2.10 // indirect
	github.com/leodido/go-urn v1.4.0 // indirect
	github.com/mattn/go-colorable v0.1.14 // indirect
	github.com/mattn/go-isatty v0.0.20 // indirect
	github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
	github.com/modern-go/reflect2 v1.0.2 // indirect
	github.com/pelletier/go-toml/v2 v2.2.4 // indirect
	github.com/rs/xid v1.6.0 // indirect
	github.com/rs/zerolog v1.34.0 // indirect
	github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
	github.com/ugorji/go/codec v1.2.12 // indirect
	go.mongodb.org/mongo-driver v1.17.3 // indirect
	golang.org/x/arch v0.16.0 // indirect
	golang.org/x/crypto v0.37.0 // indirect
	golang.org/x/net v0.39.0 // indirect
	golang.org/x/sync v0.13.0 // indirect
	golang.org/x/sys v0.32.0 // indirect
	golang.org/x/text v0.24.0 // indirect
	google.golang.org/protobuf v1.36.6 // indirect
	gopkg.in/yaml.v3 v3.0.1 // indirect
)


================================================
FILE: go.sum
================================================
git.blackforestbytes.com/BlackForestBytes/goext v0.0.572 h1:NALJ4KKkrRZcNJNsmGrUsjFdOclHSA/KyB6f94QV43k=
git.blackforestbytes.com/BlackForestBytes/goext v0.0.572/go.mod h1:C4mXq6MwC919jRHjN5IM++qGy6wmvzJZyz30nf285MU=
github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0=
github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/bytedance/sonic v1.13.2 h1:8/H1FempDZqC4VqjptGo14QQlJx8VdZJegxs6wwfqpQ=
github.com/bytedance/sonic v1.13.2/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4=
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
github.com/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY=
github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4=
github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY=
github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok=
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k=
github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs=
github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/joomcode/errorx v1.1.1 h1:/LFG/qSk1gUTuZjs+qlyOJEpcVjD9DXgBNFhdZkQrjY=
github.com/joomcode/errorx v1.1.1/go.mod h1:eQzdtdlNyN7etw6YCS4W4+lu442waxZYw5yvz0ULrRo=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/kirsle/configdir v0.0.0-20170128060238-e45d2f54772f h1:dKccXx7xA56UNqOcFIbuqFjAWPVtP688j5QMgmo6OHU=
github.com/kirsle/configdir v0.0.0-20170128060238-e45d2f54772f/go.mod h1:4rEELDSfUAlBSyUjPG0JnaNGjf13JySHFeRdD/3dLP0=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/montanaflynn/stats v0.7.1 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8eaE=
github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY=
github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4=
github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8=
github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI=
go.mongodb.org/mongo-driver v1.17.3 h1:TQyXhnsWfWtgAhMtOgtYHMTkZIfBTpMTsMnd9ZBeHxQ=
go.mongodb.org/mongo-driver v1.17.3/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ=
golang.org/x/arch v0.16.0 h1:foMtLTdyOmIniqWCHjY6+JxuC54XP1fDwx4N0ASyW+U=
golang.org/x/arch v0.16.0/go.mod h1:JmwW7aLIoRUKgaTzhkiEFxvcEiQGyOg9BMonBJUS7EE=
golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610=
golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o=
golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw=
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=


================================================
FILE: impl/columns.go
================================================
package impl

import (
	"better-docker-ps/cli"
	"better-docker-ps/docker"
	"better-docker-ps/printer"
	"bytes"
	"encoding/json"
	"fmt"
	"git.blackforestbytes.com/BlackForestBytes/goext/langext"
	"git.blackforestbytes.com/BlackForestBytes/goext/mathext"
	"git.blackforestbytes.com/BlackForestBytes/goext/rext"
	"git.blackforestbytes.com/BlackForestBytes/goext/termext"
	"git.blackforestbytes.com/BlackForestBytes/goext/timeext"
	"reflect"
	"regexp"
	"strconv"
	"strings"
	"text/template"
	"time"
)

var rexIP = rext.W(regexp.MustCompile("^(?P<b0>[0-9]{1,3})\\.(?P<b1>[0-9]{1,3})\\.(?P<b2>[0-9]{1,3})\\.(?P<b3>[0-9]{1,3})$"))

type ColSortFun = func(ctx *cli.PSContext, v1 *docker.ContainerSchema, v2 *docker.ContainerSchema) int

type ColumnDef struct {
	Reader printer.ColFun
	Sorter ColSortFun
}

var ColumnMap = map[string]ColumnDef{
	"ID":                  {ColContainerID, SortContainerID},
	"Image":               {ColFullImage, SortFullImage},
	"ImageName":           {ColImage, SortImage},
	"ImageTag":            {ColImageTag, SortImageTag},
	"Registry":            {ColRegistry, SortRegistry},
	"ImageRegistry":       {ColRegistry, SortRegistry},
	"Tag":                 {ColImageTag, SortImageTag},
	"Command":             {ColCommand, SortCommand},
	"ShortCommand":        {ColShortCommand, SortShortCommand},
	"CreatedAt":           {ColCreatedAt, SortCreatedAt},
	"RunningFor":          {ColRunningFor, SortRunningFor},
	"Ports":               {ColPortsPublished, SortPortsPublished},
	"PublishedPorts":      {ColPortsPublished, SortPortsPublished},
	"ShortPublishedPorts": {ColPortsPublishedShort, SortPortsPublishedShort},
	"LongPublishedPorts":  {ColPortsPublishedLong, SortPortsPublishedLong},
	"ExposedPorts":        {ColPortsExposed, SortPortsExposed},
	"NotPublishedPorts":   {ColPortsNotPublished, SortPortsNotPublished},
	"PublicPorts":         {ColPortsPublicPart, SortPortsPublicPart},
	"State":               {ColState, SortState},
	"Status":              {ColStatus, SortStatus},
	"Size":                {ColSize, SortSize},
	"Names":               {ColName, SortName},
	"Labels":              {ColLabels, SortLabels},
	"LabelKeys":           {ColLabelKeys, SortLabelKeys},
	"Mounts":              {ColMounts, SortMounts},
	"Networks":            {ColNetworks, SortNetworks},
	"IP":                  {ColIP, SortIP},
}

func ColContainerID(ctx *cli.PSContext, allData []docker.ContainerSchema, cont *docker.ContainerSchema) []string {
	if cont == nil {
		return []string{"CONTAINER ID"}
	}

	if ctx.Opt.Truncate {
		return []string{cont.ID[0:12]}
	} else {
		return []string{cont.ID}
	}
}

func ColFullImage(ctx *cli.PSContext, allData []docker.ContainerSchema, cont *docker.ContainerSchema) []string {
	if cont == nil {
		return []string{"IMAGE"}
	}

	return []string{cont.Image}
}

func ColRegistry(ctx *cli.PSContext, allData []docker.ContainerSchema, cont *docker.ContainerSchema) []string {
	if cont == nil {
		return []string{"REGISTRY"}
	}

	v, _, _ := docker.SplitDockerImage(ctx, cont.Image)

	return []string{v}
}

func ColImage(ctx *cli.PSContext, allData []docker.ContainerSchema, cont *docker.ContainerSchema) []string {
	if cont == nil {
		return []string{"IMAGE"}
	}

	_, v, _ := docker.SplitDockerImage(ctx, cont.Image)

	return []string{v}
}

func ColImageTag(ctx *cli.PSContext, allData []docker.ContainerSchema, cont *docker.ContainerSchema) []string {
	if cont == nil {
		return []string{"TAG"}
	}

	_, _, v := docker.SplitDockerImage(ctx, cont.Image)

	return []string{v}
}

func ColCommand(ctx *cli.PSContext, allData []docker.ContainerSchema, cont *docker.ContainerSchema) []string {
	if cont == nil {
		return []string{"COMMAND"}
	}

	cmd := cont.Command
	if ctx.Opt.Truncate && len(cmd) > 20 {
		cmd = cmd[:19] + "…"
	}

	cmd = "\"" + cmd + "\""

	return []string{cmd}
}

func ColShortCommand(ctx *cli.PSContext, allData []docker.ContainerSchema, cont *docker.ContainerSchema) []string {
	if cont == nil {
		return []string{"COMMAND"}
	}

	spl := strings.Split(cont.Command, " ")
	if len(spl) == 0 {
		return []string{""}
	} else {
		return []string{spl[0]}
	}

}

func ColRunningFor(ctx *cli.PSContext, allData []docker.ContainerSchema, cont *docker.ContainerSchema) []string {
	if cont == nil {
		return []string{"CREATED"}
	}

	ts := time.Unix(cont.Created, 0)
	diff := time.Now().Sub(ts)

	return []string{timeext.FormatNaturalDurationEnglish(diff)}
}

func ColCreatedAt(ctx *cli.PSContext, allData []docker.ContainerSchema, cont *docker.ContainerSchema) []string {
	if cont == nil {
		if ctx.Opt.TimeFormatHeader != "" {
			hdr := time.Now().In(ctx.Opt.TimeZone).Format(ctx.Opt.TimeFormatHeader)
			if hdr == "Z UTC" {
				hdr = "UTC"
			}
			return []string{"CREATED AT (" + hdr + ")"}
		} else {
			return []string{"CREATED AT"}
		}
	}

	ts := time.Unix(cont.Created, 0)

	return []string{ts.In(ctx.Opt.TimeZone).Format(ctx.Opt.TimeFormat)}
}

func ColState(ctx *cli.PSContext, allData []docker.ContainerSchema, cont *docker.ContainerSchema) []string {
	if cont == nil {
		return []string{"STATE"}
	}

	strstate := "[" + strings.ToUpper(string(cont.State)) + "]"

	if !ctx.Opt.OutputColor {
		return []string{strstate}
	}

	return []string{stateColor(cont.State, strstate)}
}

func ColStatus(ctx *cli.PSContext, allData []docker.ContainerSchema, cont *docker.ContainerSchema) []string {
	if cont == nil {
		return []string{"STATUS"}
	}

	if !ctx.Opt.OutputColor {
		return []string{cont.Status}
	}

	return []string{statusColor(cont.Status, cont.Status)}
}

func ColPortsExposed(ctx *cli.PSContext, allData []docker.ContainerSchema, cont *docker.ContainerSchema) []string {
	if cont == nil {
		return []string{"EXPOSED PORTS"}
	}

	m := make(map[string]bool)
	r := make([]string, 0)
	for _, port := range cont.PortsSorted() {
		p1 := langext.StrPadLeft(strconv.Itoa(port.PublicPort), " ", 5)
		p2 := langext.StrPadLeft(strconv.Itoa(port.PrivatePort), " ", 5)

		if port.PublicPort == 0 {
			str := fmt.Sprintf("         %s / %s", p2, port.Type)
			if _, ok := m[str]; !ok {
				m[str] = true
				r = append(r, str)
			}
		} else {
			str := fmt.Sprintf("%s -> %s / %s", p1, p2, port.Type)
			if _, ok := m[str]; !ok {
				m[str] = true
				r = append(r, str)
			}
		}
	}

	return r
}

func ColPortsPublicPart(ctx *cli.PSContext, allData []docker.ContainerSchema, cont *docker.ContainerSchema) []string {
	if cont == nil {
		return []string{"EXPOSED PORTS"}
	}

	m := make(map[string]bool)
	r := make([]string, 0)
	for _, port := range cont.PortsSorted() {
		if port.PublicPort != 0 {
			str := fmt.Sprintf("%d", port.PublicPort)
			if _, ok := m[str]; !ok {
				m[str] = true
				r = append(r, strconv.Itoa(port.PublicPort))
			}
		}
	}

	return r
}

func ColPortsPublished(ctx *cli.PSContext, allData []docker.ContainerSchema, cont *docker.ContainerSchema) []string {
	if cont == nil {
		return []string{"PUBLISHED PORTS"}
	}

	pubPortLenMax := ctx.GetIntFromCache("Printer::Ports::port_pub_length", func() int {
		ml := 0
		for _, v1 := range allData {
			for _, v2 := range v1.Ports {
				ml = mathext.Max(ml, len(strconv.Itoa(v2.PublicPort)))
			}
		}
		return ml
	})

	privPortLenMax := ctx.GetIntFromCache("Printer::Ports::port_pub_length", func() int {
		ml := 0
		for _, v1 := range allData {
			for _, v2 := range v1.Ports {
				ml = mathext.Max(ml, len(strconv.Itoa(v2.PrivatePort)))
			}
		}
		return ml
	})

	m := make(map[string]bool)
	r := make([]string, 0)
	for _, port := range cont.PortsSorted() {
		p1 := langext.StrPadLeft(strconv.Itoa(port.PublicPort), " ", pubPortLenMax)
		p2 := langext.StrPadLeft(strconv.Itoa(port.PrivatePort), " ", privPortLenMax)

		if port.PublicPort != 0 {
			str := fmt.Sprintf("%s -> %s / %s", p1, p2, port.Type)
			if port.IsLoopback() {
				str += " (loc)"
			}
			if _, ok := m[str]; !ok {
				m[str] = true
				r = append(r, str)
			}
		}
	}

	return r
}

func ColPortsPublishedShort(ctx *cli.PSContext, allData []docker.ContainerSchema, cont *docker.ContainerSchema) []string {
	if cont == nil {
		return []string{"PUBLISHED PORTS"}
	}

	pubPortLenMax := ctx.GetIntFromCache("Printer::Ports::port_pub_length", func() int {
		ml := 0
		for _, v1 := range allData {
			for _, v2 := range v1.Ports {
				ml = mathext.Max(ml, len(strconv.Itoa(v2.PublicPort)))
			}
		}
		return ml
	})

	privPortLenMax := ctx.GetIntFromCache("Printer::Ports::port_pub_length", func() int {
		ml := 0
		for _, v1 := range allData {
			for _, v2 := range v1.Ports {
				ml = mathext.Max(ml, len(strconv.Itoa(v2.PrivatePort)))
			}
		}
		return ml
	})

	m := make(map[string]bool)
	r := make([]string, 0)
	for _, port := range cont.PortsSorted() {
		p1 := langext.StrPadLeft(strconv.Itoa(port.PublicPort), " ", pubPortLenMax)
		p2 := langext.StrPadLeft(strconv.Itoa(port.PrivatePort), " ", privPortLenMax)

		if port.PublicPort != 0 {
			str := fmt.Sprintf("%s -> %s", p1, p2)
			if _, ok := m[str]; !ok {
				m[str] = true
				r = append(r, str)
			}
		}
	}

	return r
}

func ColPortsPublishedLong(ctx *cli.PSContext, allData []docker.ContainerSchema, cont *docker.ContainerSchema) []string {
	if cont == nil {
		return []string{"PUBLISHED PORTS"}
	}

	iplenMax := ctx.GetIntFromCache("Printer::Ports::ip_length", func() int {
		ml := 0
		for _, v1 := range allData {
			for _, v2 := range v1.Ports {
				ml = mathext.Max(ml, len(v2.IP))
			}
		}
		return ml
	})

	pubPortLenMax := ctx.GetIntFromCache("Printer::Ports::port_pub_length", func() int {
		ml := 0
		for _, v1 := range allData {
			for _, v2 := range v1.Ports {
				ml = mathext.Max(ml, len(strconv.Itoa(v2.PublicPort)))
			}
		}
		return ml
	})

	privPortLenMax := ctx.GetIntFromCache("Printer::Ports::port_pub_length", func() int {
		ml := 0
		for _, v1 := range allData {
			for _, v2 := range v1.Ports {
				ml = mathext.Max(ml, len(strconv.Itoa(v2.PrivatePort)))
			}
		}
		return ml
	})

	m := make(map[string]bool)
	r := make([]string, 0)
	for _, port := range cont.PortsSorted() {
		p0 := langext.StrPadLeft("["+port.IP+"]", " ", iplenMax+2)
		p1 := langext.StrPadLeft(strconv.Itoa(port.PublicPort), " ", pubPortLenMax)
		p2 := langext.StrPadLeft(strconv.Itoa(port.PrivatePort), " ", privPortLenMax)

		if port.PublicPort != 0 {
			str := fmt.Sprintf("%s:%s -> %s", p0, p1, p2)
			if _, ok := m[str]; !ok {
				m[str] = true
				r = append(r, str)
			}
		}
	}

	return r
}

func ColPortsNotPublished(ctx *cli.PSContext, allData []docker.ContainerSchema, cont *docker.ContainerSchema) []string {
	if cont == nil {
		return []string{"NOT PUBLISHED PORTS"}
	}

	m := make(map[string]bool)
	r := make([]string, 0)
	for _, port := range cont.PortsSorted() {
		p2 := langext.StrPadLeft(strconv.Itoa(port.PrivatePort), " ", 5)

		if port.PublicPort == 0 {
			str := fmt.Sprintf("%s / %s", p2, port.Type)
			if _, ok := m[str]; !ok {
				m[str] = true
				r = append(r, str)
			}
		}
	}

	return r
}

func ColName(ctx *cli.PSContext, allData []docker.ContainerSchema, cont *docker.ContainerSchema) []string {
	if cont == nil {
		return []string{"NAME"}
	}

	r := make([]string, 0, len(cont.Names))
	for _, n := range cont.Names {
		if len(n) > 0 && n[0] == '/' {
			n = n[1:]
		}
		r = append(r, n)
	}

	return r
}

func ColSize(ctx *cli.PSContext, allData []docker.ContainerSchema, cont *docker.ContainerSchema) []string {
	if cont == nil {
		return []string{"SIZE"}
	}

	if cont.SizeRw == 0 && cont.SizeRootFs == 0 {
		return []string{}
	}

	return []string{fmt.Sprintf("%v (virt %v)", langext.StrPadRight(langext.FormatBytes(cont.SizeRw), " ", 11), langext.FormatBytes(cont.SizeRootFs))}
}

func ColMounts(ctx *cli.PSContext, allData []docker.ContainerSchema, cont *docker.ContainerSchema) []string {
	if cont == nil {
		return []string{"MOUNTS"}
	}

	r := make([]string, 0, len(cont.Mounts))
	for _, mnt := range cont.Mounts {
		val := fmt.Sprintf("%s -> %s", mnt.Source, mnt.Destination)
		if !mnt.RW {
			val += " [ro]"
		}
		r = append(r, val)
	}

	return r
}

func ColIP(ctx *cli.PSContext, allData []docker.ContainerSchema, cont *docker.ContainerSchema) []string {
	if cont == nil {
		return []string{"IP"}
	}

	r := make([]string, 0, len(cont.NetworkSettings.Networks))
	for _, nw := range cont.NetworkSettings.Networks {
		if nw.IPAddress != "" {
			r = append(r, nw.IPAddress)
		}
	}

	return r
}

func ColLabels(ctx *cli.PSContext, allData []docker.ContainerSchema, cont *docker.ContainerSchema) []string {
	if cont == nil {
		return []string{"LABELS"}
	}

	r := make([]string, 0, len(cont.Mounts))
	for k, v := range cont.Labels {
		r = append(r, fmt.Sprintf("%s := %s", k, v))
	}

	return r
}

func ColLabelKeys(ctx *cli.PSContext, allData []docker.ContainerSchema, cont *docker.ContainerSchema) []string {
	if cont == nil {
		return []string{"LABELS"}
	}

	r := make([]string, 0, len(cont.Mounts))
	for k, _ := range cont.Labels {
		r = append(r, k)
	}

	return r
}

func ColNetworks(ctx *cli.PSContext, allData []docker.ContainerSchema, cont *docker.ContainerSchema) []string {
	if cont == nil {
		return []string{"NETWORKS"}
	}

	r := make([]string, 0, len(cont.Mounts))
	for k := range cont.NetworkSettings.Networks {
		r = append(r, k)
	}

	return r
}

func ColPlaintext(str string) printer.ColFun {
	return func(ctx *cli.PSContext, allData []docker.ContainerSchema, cont *docker.ContainerSchema) []string {
		return []string{str}
	}
}

// #####################################################################################################################

func SortContainerID(ctx *cli.PSContext, v1 *docker.ContainerSchema, v2 *docker.ContainerSchema) int {
	if ctx.Opt.Truncate {
		return langext.Compare(v1.ID[0:12], v2.ID[0:12])
	} else {
		return langext.Compare(v1.ID, v2.ID)
	}
}

func SortFullImage(ctx *cli.PSContext, v1 *docker.ContainerSchema, v2 *docker.ContainerSchema) int {
	return langext.Compare(v1.Image, v2.Image)
}

func SortRegistry(ctx *cli.PSContext, v1 *docker.ContainerSchema, v2 *docker.ContainerSchema) int {
	reg1, _, _ := docker.SplitDockerImage(ctx, v1.Image)
	reg2, _, _ := docker.SplitDockerImage(ctx, v2.Image)

	return langext.Compare(reg1, reg2)
}

func SortImage(ctx *cli.PSContext, v1 *docker.ContainerSchema, v2 *docker.ContainerSchema) int {
	_, img1, _ := docker.SplitDockerImage(ctx, v1.Image)
	_, img2, _ := docker.SplitDockerImage(ctx, v2.Image)

	return langext.Compare(img1, img2)
}

func SortImageTag(ctx *cli.PSContext, v1 *docker.ContainerSchema, v2 *docker.ContainerSchema) int {
	_, _, tag1 := docker.SplitDockerImage(ctx, v1.Image)
	_, _, tag2 := docker.SplitDockerImage(ctx, v2.Image)

	return langext.Compare(tag1, tag2)
}

func SortCommand(ctx *cli.PSContext, v1 *docker.ContainerSchema, v2 *docker.ContainerSchema) int {
	return langext.Compare(v1.Command, v2.Command)
}

func SortShortCommand(ctx *cli.PSContext, v1 *docker.ContainerSchema, v2 *docker.ContainerSchema) int {
	spl1 := strings.Split(v1.Command, " ")
	sc1 := ""
	if len(spl1) > 0 {
		sc1 = spl1[0]
	}

	spl2 := strings.Split(v2.Command, " ")
	sc2 := ""
	if len(spl2) > 0 {
		sc2 = spl2[0]
	}

	return langext.Compare(sc1, sc2)
}

func SortRunningFor(ctx *cli.PSContext, v1 *docker.ContainerSchema, v2 *docker.ContainerSchema) int {
	return langext.Compare(v1.Created, v2.Created) * -1 // runnign for is 'now - created', so we need to invert the sort order
}

func SortCreatedAt(ctx *cli.PSContext, v1 *docker.ContainerSchema, v2 *docker.ContainerSchema) int {
	return langext.Compare(v1.Created, v2.Created)
}

func SortState(ctx *cli.PSContext, v1 *docker.ContainerSchema, v2 *docker.ContainerSchema) int {
	return langext.Compare(v1.State.Num(), v2.State.Num())
}

func SortStatus(ctx *cli.PSContext, v1 *docker.ContainerSchema, v2 *docker.ContainerSchema) int {
	return langext.Compare(v1.Status, v2.Status)
}

func SortPortsExposed(ctx *cli.PSContext, v1 *docker.ContainerSchema, v2 *docker.ContainerSchema) int {
	parr1 := langext.ArrCopy(v1.Ports)
	parr2 := langext.ArrCopy(v2.Ports)

	pl1 := langext.ArrMap(parr1, func(v docker.PortSchema) string {
		return fmt.Sprintf("%s;%08d;%08d;%s", v.IP, v.PrivatePort, v.PublicPort, v.Type)
	})
	pl2 := langext.ArrMap(parr2, func(v docker.PortSchema) string {
		return fmt.Sprintf("%s;%08d;%08d;%s", v.IP, v.PrivatePort, v.PublicPort, v.Type)
	})

	langext.SortStable(pl1)
	langext.SortStable(pl2)

	pstr1 := strings.Join(pl1, "\n")
	pstr2 := strings.Join(pl2, "\n")

	return langext.Compare(pstr1, pstr2)
}

func SortPortsPublished(ctx *cli.PSContext, v1 *docker.ContainerSchema, v2 *docker.ContainerSchema) int {
	parr1 := langext.ArrCopy(v1.Ports)
	parr2 := langext.ArrCopy(v2.Ports)

	parr1 = langext.ArrFilter(parr1, func(v docker.PortSchema) bool { return v.PublicPort != 0 })
	parr2 = langext.ArrFilter(parr2, func(v docker.PortSchema) bool { return v.PublicPort != 0 })

	pl1 := langext.ArrMap(parr1, func(v docker.PortSchema) string {
		return fmt.Sprintf("%s;%08d;%08d;%s", v.IP, v.PrivatePort, v.PublicPort, v.Type)
	})
	pl2 := langext.ArrMap(parr2, func(v docker.PortSchema) string {
		return fmt.Sprintf("%s;%08d;%08d;%s", v.IP, v.PrivatePort, v.PublicPort, v.Type)
	})

	langext.SortStable(pl1)
	langext.SortStable(pl2)

	pstr1 := strings.Join(pl1, "\n")
	pstr2 := strings.Join(pl2, "\n")

	return langext.Compare(pstr1, pstr2)
}

func SortPortsPublishedShort(ctx *cli.PSContext, v1 *docker.ContainerSchema, v2 *docker.ContainerSchema) int {
	parr1 := langext.ArrCopy(v1.Ports)
	parr2 := langext.ArrCopy(v2.Ports)

	parr1 = langext.ArrFilter(parr1, func(v docker.PortSchema) bool { return v.PublicPort != 0 })
	parr2 = langext.ArrFilter(parr2, func(v docker.PortSchema) bool { return v.PublicPort != 0 })

	pl1 := langext.ArrMap(parr1, func(v docker.PortSchema) string {
		return fmt.Sprintf("%s;%08d;%08d;%s", v.IP, v.PrivatePort, v.PublicPort, v.Type)
	})
	pl2 := langext.ArrMap(parr2, func(v docker.PortSchema) string {
		return fmt.Sprintf("%s;%08d;%08d;%s", v.IP, v.PrivatePort, v.PublicPort, v.Type)
	})

	langext.SortStable(pl1)
	langext.SortStable(pl2)

	pstr1 := strings.Join(pl1, "\n")
	pstr2 := strings.Join(pl2, "\n")

	return langext.Compare(pstr1, pstr2)
}

func SortPortsPublishedLong(ctx *cli.PSContext, v1 *docker.ContainerSchema, v2 *docker.ContainerSchema) int {
	parr1 := langext.ArrCopy(v1.Ports)
	parr2 := langext.ArrCopy(v2.Ports)

	parr1 = langext.ArrFilter(parr1, func(v docker.PortSchema) bool { return v.PublicPort != 0 })
	parr2 = langext.ArrFilter(parr2, func(v docker.PortSchema) bool { return v.PublicPort != 0 })

	pl1 := langext.ArrMap(parr1, func(v docker.PortSchema) string {
		return fmt.Sprintf("%s;%08d;%08d;%s", v.IP, v.PrivatePort, v.PublicPort, v.Type)
	})
	pl2 := langext.ArrMap(parr2, func(v docker.PortSchema) string {
		return fmt.Sprintf("%s;%08d;%08d;%s", v.IP, v.PrivatePort, v.PublicPort, v.Type)
	})

	langext.SortStable(pl1)
	langext.SortStable(pl2)

	pstr1 := strings.Join(pl1, "\n")
	pstr2 := strings.Join(pl2, "\n")

	return langext.Compare(pstr1, pstr2)
}

func SortPortsNotPublished(ctx *cli.PSContext, v1 *docker.ContainerSchema, v2 *docker.ContainerSchema) int {
	parr1 := langext.ArrCopy(v1.Ports)
	parr2 := langext.ArrCopy(v2.Ports)

	parr1 = langext.ArrFilter(parr1, func(v docker.PortSchema) bool { return v.PublicPort == 0 })
	parr2 = langext.ArrFilter(parr2, func(v docker.PortSchema) bool { return v.PublicPort == 0 })

	pl1 := langext.ArrMap(parr1, func(v docker.PortSchema) string {
		return fmt.Sprintf("%s;%08d;%08d;%s", v.IP, v.PrivatePort, v.PublicPort, v.Type)
	})
	pl2 := langext.ArrMap(parr2, func(v docker.PortSchema) string {
		return fmt.Sprintf("%s;%08d;%08d;%s", v.IP, v.PrivatePort, v.PublicPort, v.Type)
	})

	langext.SortStable(pl1)
	langext.SortStable(pl2)

	pstr1 := strings.Join(pl1, "\n")
	pstr2 := strings.Join(pl2, "\n")

	return langext.Compare(pstr1, pstr2)
}

func SortPortsPublicPart(ctx *cli.PSContext, v1 *docker.ContainerSchema, v2 *docker.ContainerSchema) int {
	parr1 := langext.ArrCopy(v1.Ports)
	parr2 := langext.ArrCopy(v2.Ports)

	parr1 = langext.ArrFilter(parr1, func(v docker.PortSchema) bool { return v.PublicPort != 0 })
	parr2 = langext.ArrFilter(parr2, func(v docker.PortSchema) bool { return v.PublicPort != 0 })

	pl1 := langext.ArrMap(parr1, func(v docker.PortSchema) string {
		return fmt.Sprintf("%08d", v.PublicPort)
	})
	pl2 := langext.ArrMap(parr2, func(v docker.PortSchema) string {
		return fmt.Sprintf("%08d", v.PublicPort)
	})

	langext.SortStable(pl1)
	langext.SortStable(pl2)

	pstr1 := strings.Join(pl1, ":")
	pstr2 := strings.Join(pl2, ":")

	return langext.Compare(pstr1, pstr2)
}

func SortName(ctx *cli.PSContext, v1 *docker.ContainerSchema, v2 *docker.ContainerSchema) int {
	names1 := langext.ArrCopy(v1.Names)
	names2 := langext.ArrCopy(v2.Names)

	langext.SortStable(names1)
	langext.SortStable(names2)

	return langext.CompareArr(names1, names2)
}

func SortSize(ctx *cli.PSContext, v1 *docker.ContainerSchema, v2 *docker.ContainerSchema) int {
	return langext.CompareArr([]int64{v1.SizeRw, v1.SizeRootFs}, []int64{v2.SizeRw, v2.SizeRootFs})
}

func SortMounts(ctx *cli.PSContext, v1 *docker.ContainerSchema, v2 *docker.ContainerSchema) int {
	mounts1 := langext.ArrMap(v1.Mounts, func(v docker.ContainerMount) string {
		return fmt.Sprintf("%s\n%s", v.Source, v.Destination)
	})
	mounts2 := langext.ArrMap(v2.Mounts, func(v docker.ContainerMount) string {
		return fmt.Sprintf("%s\n%s", v.Source, v.Destination)
	})

	langext.SortStable(mounts1)
	langext.SortStable(mounts2)

	return langext.CompareArr(mounts1, mounts2)
}

func SortIP(ctx *cli.PSContext, v1 *docker.ContainerSchema, v2 *docker.ContainerSchema) int {
	ips1 := langext.ArrMap(langext.MapToArr(v1.NetworkSettings.Networks), func(v langext.MapEntry[string, docker.ContainerSingleNetworkSettings]) string {
		return v.Value.IPAddress
	})
	ips2 := langext.ArrMap(langext.MapToArr(v2.NetworkSettings.Networks), func(v langext.MapEntry[string, docker.ContainerSingleNetworkSettings]) string {
		return v.Value.IPAddress
	})

	ips1 = langext.ArrFilter(ips1, func(v string) bool {
		return v != ""
	})
	ips2 = langext.ArrFilter(ips2, func(v string) bool {
		return v != ""
	})

	ips1 = langext.ArrMap(ips1, func(v string) string {
		return ipExpand(v)
	})
	ips2 = langext.ArrMap(ips2, func(v string) string {
		return ipExpand(v)
	})

	langext.SortStable(ips1)
	langext.SortStable(ips2)

	return langext.CompareArr(ips1, ips2)
}

func SortLabels(ctx *cli.PSContext, v1 *docker.ContainerSchema, v2 *docker.ContainerSchema) int {
	lbls1 := langext.ArrMap(langext.MapToArr(v1.Labels), func(v langext.MapEntry[string, string]) string {
		return fmt.Sprintf("%s\n%s", v.Key, v.Value)
	})
	lbls2 := langext.ArrMap(langext.MapToArr(v2.Labels), func(v langext.MapEntry[string, string]) string {
		return fmt.Sprintf("%s\t%s", v.Key, v.Value)
	})

	langext.SortStable(lbls1)
	langext.SortStable(lbls2)

	return langext.CompareArr(lbls1, lbls2)
}

func SortLabelKeys(ctx *cli.PSContext, v1 *docker.ContainerSchema, v2 *docker.ContainerSchema) int {
	lbls1 := langext.ArrMap(langext.MapToArr(v1.Labels), func(v langext.MapEntry[string, string]) string {
		return v.Key
	})
	lbls2 := langext.ArrMap(langext.MapToArr(v2.Labels), func(v langext.MapEntry[string, string]) string {
		return v.Key
	})

	langext.SortStable(lbls1)
	langext.SortStable(lbls2)

	return langext.CompareArr(lbls1, lbls2)
}

func SortNetworks(ctx *cli.PSContext, v1 *docker.ContainerSchema, v2 *docker.ContainerSchema) int {
	ntwrk1 := langext.ArrMap(langext.MapToArr(v1.NetworkSettings.Networks), func(v langext.MapEntry[string, docker.ContainerSingleNetworkSettings]) string {
		return v.Key
	})
	ntwrk2 := langext.ArrMap(langext.MapToArr(v2.NetworkSettings.Networks), func(v langext.MapEntry[string, docker.ContainerSingleNetworkSettings]) string {
		return v.Key
	})

	langext.SortStable(ntwrk1)
	langext.SortStable(ntwrk2)

	return langext.CompareArr(ntwrk1, ntwrk2)
}

// #####################################################################################################################

func getColFun(colkey string) (printer.ColFun, bool) {

	// Fast branch, simple references to columns

	for k, v := range ColumnMap {
		if "{{."+k+"}}" == colkey {
			return v.Reader, true
		}
	}

	// Slow branch, fully-featured go templates

	if strings.HasPrefix(colkey, "{{") && strings.HasSuffix(colkey, "}}") {
		return templateColFun(colkey, ""), true
	}

	if splt := strings.SplitN(colkey, ":", 2); len(splt) == 2 && strings.HasPrefix(splt[1], "{{") && strings.HasSuffix(splt[1], "}}") {
		return templateColFun(splt[1], splt[0]), true
	}

	// Fallback, nothing

	return nil, false
}

func templateColFun(fmtstr string, header string) printer.ColFun {
	return func(ctx *cli.PSContext, allData []docker.ContainerSchema, cont *docker.ContainerSchema) (res []string) {
		defer func() {
			if r := recover(); r != nil {
				ctx.PrintErrorMessage(fmt.Sprintf("Panic in template evaluation of '%s':\n%v", fmtstr, r))
				res = []string{"@ERROR"}
			}
		}()

		if cont == nil {
			return []string{header}
		}

		funcs := template.FuncMap{
			"join": strings.Join,
			"array_last": func(v any) any {
				rval := reflect.ValueOf(v)
				alen := rval.Len()
				if alen == 0 {
					return nil
				}
				return rval.Index(alen - 1).Interface()
			},
			"array_slice": func(v any, start int, end int) any {
				rval := reflect.ValueOf(v)
				alen := rval.Len()

				start = max(0, min(alen, start))
				end = max(0, min(alen, end))

				return rval.Slice(start, end).Interface()
			},
			"in_array": func(compval any, arrval any) (resp bool) {
				defer func() {
					if rec := recover(); rec != nil {
						resp = false
					}
				}()
				v := reflect.ValueOf(arrval)
				for i := 0; i < v.Len(); i++ {
					if v.Index(i).Equal(reflect.ValueOf(compval)) {
						return true
					}
				}
				return false
			},
			"json": func(obj any) string {
				v, err := json.Marshal(obj)
				if err != nil {
					panic(err)
				}
				return string(v)
			},
			"json_indent": func(obj any) string {
				v, err := json.MarshalIndent(obj, "", "  ")
				if err != nil {
					panic(err)
				}
				return string(v)
			},
			"json_pretty": func(v string) string {
				buffer := &bytes.Buffer{}
				err := json.Indent(buffer, []byte(v), "", "  ")
				if err != nil {
					return v
				} else {
					return buffer.String()
				}
			},
			"coalesce": func(val any, def any) any {
				if langext.IsNil(val) {
					return def
				} else {
					return val
				}
			},
			"to_string": func(v any) string {
				return fmt.Sprintf("%v", v)
			},
			"deref": func(vInput any) any {
				val := reflect.ValueOf(vInput)
				if val.Kind() == reflect.Ptr {
					return val.Elem().Interface()
				}
				return ""
			},
			"now": func() time.Time {
				return time.Now()
			},
			"uniqid": func() string {
				return langext.MustRawHexUUID()
			},
		}

		templ, err := template.New("col").Funcs(funcs).Parse(fmtstr)
		if err != nil {
			ctx.PrintErrorMessage(fmt.Sprintf("Error in template parsing of '%s':\n%v", fmtstr, err.Error()))
			res = []string{"@ERROR"}
		}

		bfr := &bytes.Buffer{}
		err = templ.Execute(bfr, *cont)
		if err != nil {
			ctx.PrintErrorMessage(fmt.Sprintf("Error in template evaluation of '%s':\n%v", fmtstr, err.Error()))
			res = []string{"@ERROR"}
		}

		return strings.Split(bfr.String(), "\n")
	}
}

func getSortFun(colkey string) (ColSortFun, bool) {
	if cdef, ok := ColumnMap[colkey]; ok {
		return cdef.Sorter, true
	}
	return nil, false
}

func replaceSingleLineColumnData(ctx *cli.PSContext, allData []docker.ContainerSchema, data docker.ContainerSchema, format string) string {

	r := format

	for k, v := range ColumnMap {
		r = strings.ReplaceAll(r, "{{."+k+"}}", strings.Join(v.Reader(ctx, allData, &data), " "))
	}

	return r

}

func parseTableDef(fmt string) []printer.ColFun {
	split := regexp.MustCompile("(\\\\t|\\t)").Split(fmt[6:], -1)
	columns1 := make([]printer.ColFun, 0)
	for _, v := range split {
		if cf, ok := getColFun(v); ok {
			columns1 = append(columns1, cf)
		} else {
			columns1 = append(columns1, ColPlaintext(v))
		}
	}
	return columns1
}

func stateColor(state docker.ContainerState, value string) string {
	switch state {
	case docker.StateCreated:
		return termext.Yellow(value)
	case docker.StateRunning:
		return termext.Green(value)
	case docker.StateRestarting:
		return termext.Yellow(value)
	case docker.StateExited:
		return termext.Red(value)
	case docker.StatePaused:
		return termext.Yellow(value)
	case docker.StateDead:
		return termext.Red(value)
	}
	return value
}

func statusColor(status string, value string) string {
	if status == "Created" {
		return termext.Yellow(value)
	}

	if strings.HasPrefix(status, "Exited") {
		return termext.Red(value)
	}

	if strings.HasPrefix(status, "Up") {
		if strings.HasSuffix(status, "(unhealthy)") {
			return termext.Red(value)
		}
		if strings.HasSuffix(status, "(health: starting)") {
			return termext.Yellow(value)
		}

		return termext.Green(value)
	}

	return value
}

func ipExpand(ip string) string {
	if match, ok := rexIP.MatchFirst(ip); ok {
		return fmt.Sprintf("%03s.%03s.%03s.%03s",
			match.GroupByName("b0").Value(),
			match.GroupByName("b1").Value(),
			match.GroupByName("b2").Value(),
			match.GroupByName("b3").Value())
	}
	return ip
}


================================================
FILE: impl/impl.go
================================================
package impl

import (
	"better-docker-ps/cli"
	"better-docker-ps/docker"
	"better-docker-ps/printer"
	"better-docker-ps/pserr"
	"encoding/json"
	"git.blackforestbytes.com/BlackForestBytes/goext/langext"
	"git.blackforestbytes.com/BlackForestBytes/goext/mathext"
	"git.blackforestbytes.com/BlackForestBytes/goext/syncext"
	"golang.org/x/term"
	"os"
	"os/signal"
	"strings"
	"syscall"
	"time"
)

func Execute(ctx *cli.PSContext) error {
	return executeSingle(ctx, false)
}

func Watch(ctx *cli.PSContext, d time.Duration) error {

	sigTermChannel := make(chan os.Signal, 8)
	signal.Notify(sigTermChannel, os.Interrupt, syscall.SIGTERM)

	for {

		err := executeSingle(ctx, true)
		if err != nil {
			return err
		}

		_, isSig := syncext.ReadChannelWithTimeout(sigTermChannel, d)
		if isSig {
			ctx.PrintPrimaryOutput("")
			ctx.PrintPrimaryOutput("Watch canceled with Ctrl+C")
			return nil
		}

	}
}

func executeSingle(ctx *cli.PSContext, clear bool) error {
	for _, fmt := range ctx.Opt.Format {
		if strings.Contains(fmt, "{{.Size}}") {
			ctx.Opt.WithSize = true
		}
	}

	jsonraw, err := docker.ListContainer(ctx)
	if err != nil {
		return err
	}

	ctx.PrintVerboseKV("API response", langext.TryPrettyPrintJson(string(jsonraw)))

	var data []docker.ContainerSchema
	err = json.Unmarshal(jsonraw, &data)
	if err != nil {
		return pserr.DirectOutput.Wrap(err, "Failed to decode Docker API response")
	}

	if len(ctx.Opt.SortColumns) > 0 {
		data = doSort(ctx, data, ctx.Opt.SortColumns, ctx.Opt.SortDirection)
	}

	if ctx.Opt.Search != nil {
		data = doSearch(ctx, data, *ctx.Opt.Search)
	}

	for i, v := range ctx.Opt.Format {

		if clear {
			ctx.ClearTerminal()
		}

		ok, err := doOutput(ctx, data, v, i == len(ctx.Opt.Format)-1)
		if err != nil {
			return err
		}
		if ok {
			return nil
		}

	}

	return pserr.DirectOutput.New("Missing format specification for output")
}

func doSearch(ctx *cli.PSContext, data []docker.ContainerSchema, needle string) []docker.ContainerSchema {
	needle = strings.ToLower(needle)

	haystackFormat := ""
	for _, f := range ctx.Opt.Format {
		if strings.HasPrefix(f, "table ") {
			haystackFormat = f
			break
		}
	}

	result := make([]docker.ContainerSchema, 0, len(data))
	for _, cont := range data {
		hay := cont.ID + " " + strings.Join(cont.Names, " ") + " " + cont.Image + " " + cont.Command
		if haystackFormat != "" {
			for _, fn := range parseTableDef(haystackFormat) {
				hay += " " + strings.Join(fn(ctx, data, &cont), " ")
			}
		} else if len(ctx.Opt.Format) > 0 {
			hay += " " + replaceSingleLineColumnData(ctx, data, cont, ctx.Opt.Format[0])
		}
		if strings.Contains(strings.ToLower(hay), needle) {
			result = append(result, cont)
		}
	}
	return result
}

func doSort(ctx *cli.PSContext, data []docker.ContainerSchema, skeys []string, sdirs []cli.SortDirection) []docker.ContainerSchema {

	langext.SortSliceStable(data, func(v1, v2 docker.ContainerSchema) bool {

		// return true if v1 < v2

		for i := 0; i < len(skeys); i++ {

			sfn, ok := getSortFun(skeys[i])
			if !ok {
				continue
			}

			cmp := sfn(ctx, &v1, &v2)
			if sdirs[i] == "DESC" {
				cmp = cmp * -1
			}

			if cmp < 0 {
				return true
			} else if cmp > 0 {
				return false
			}
		}

		return false // equals
	})

	return data
}

func doOutput(ctx *cli.PSContext, data []docker.ContainerSchema, format string, force bool) (bool, error) {
	if format == "idlist" {

		for _, v := range data {
			if ctx.Opt.Truncate {
				ctx.PrintPrimaryOutput(v.ID[0:12])
			} else {
				ctx.PrintPrimaryOutput(v.ID)
			}
		}
		return true, nil

	} else if strings.HasPrefix(format, "table ") {

		columns := parseTableDef(format)
		outWidth := printer.Width(ctx, data, columns)

		if !force {
			termWidth, _, err := term.GetSize(int(os.Stdout.Fd()))
			if err == nil && 0 < termWidth && termWidth < outWidth {
				return false, nil
			}
		}

		printer.Print(ctx, data, columns)
		return true, nil

	} else {

		lines := make([]string, 0)
		outWidth := 0

		for _, v := range data {
			str := replaceSingleLineColumnData(ctx, data, v, format)
			lines = append(lines, str)
			outWidth = mathext.Max(outWidth, printer.RealStrLen(str))
		}

		if !force {
			termWidth, _, err := term.GetSize(int(os.Stdout.Fd()))
			if err == nil && 0 < termWidth && termWidth < outWidth {
				return false, nil
			}
		}

		for _, v := range lines {
			ctx.PrintPrimaryOutput(v)
		}
		return true, nil

	}
}


================================================
FILE: install.sh
================================================
#!/usr/bin/env bash
set -euo pipefail

# This script handles the installation of 'dops' by detecting the OS
# and architecture, downloading the appropriate binary, and configuring the shell PATH.

# Reset
Color_Off=''

# Regular Colors
Red=''
Green=''
Dim='' # White

# Bold
Bold_Green=''
Bold_White=''

if [[ -t 1 ]]; then
    # Reset
    Color_Off='\033[0m' # Text Reset

    # Regular Colors
    Red='\033[0;31m'   # Red
    Green='\033[0;32m' # Green
    Dim='\033[0;2m'    # White

    # Bold
    Bold_Green='\033[1;32m' # Bold Green
    Bold_White='\033[1m'    # Bold White
fi

error() {
    echo -e "${Red}error${Color_Off}:" "$@" >&2
    exit 1
}

info() {
    echo -e "${Dim}$@ ${Color_Off}"
}

success() {
    echo -e "${Green}$@ ${Color_Off}"
}

info_bold() {
    echo -e "${Bold_White}$@ ${Color_Off}"
}

# Check if we're on Arch Linux
is_arch_linux() {
    [[ -f /etc/arch-release ]] || command -v pacman >/dev/null 2>&1
}

# Detect available AUR helpers in order of preference
detect_aur_helper() {
    local aur_helpers=("yay" "paru" "pikaur" "pamac" "trizen" "yaourt")
    
    for helper in "${aur_helpers[@]}"; do
        if command -v "$helper" >/dev/null 2>&1; then
            echo "$helper"
            return 0
        fi
    done
    return 1
}

# Install via AUR
install_via_aur() {
    local aur_helper="$1"
    
    info "Installing 'dops' via AUR using ${aur_helper}..."
    
    case "$aur_helper" in
        yay|paru|pikaur)
            "$aur_helper" -S --noconfirm dops-bin ||
                error "Failed to install dops-bin via $aur_helper"
            ;;
        pamac)
            pamac install --no-confirm dops-bin ||
                error "Failed to install dops-bin via pamac"
            ;;
        trizen|yaourt)
            "$aur_helper" -S --noconfirm dops-bin ||
                error "Failed to install dops-bin via $aur_helper"
            ;;
        *)
            error "Unsupported AUR helper: $aur_helper"
            ;;
    esac
    
    success "dops was installed successfully via AUR using $aur_helper"
    echo "Run 'dops --help' to get started"
    
    echo
    info "To use 'dops' as a drop-in replacement for 'docker ps',"
    info "add the following function to your shell configuration file (e.g., ~/.zshrc, ~/.bashrc):"
    echo
    info_bold 'docker() {'
    info_bold '  case $1 in'
    info_bold '    ps)'
    info_bold '      shift'
    info_bold '      command dops "$@"'
    info_bold '      ;;'
    info_bold '    *)'
    info_bold '      command docker "$@";;'
    info_bold '  esac'
    info_bold '}'
    
    exit 0
}

# Try AUR installation first on Arch Linux
if is_arch_linux; then
    info "Detected Arch Linux, checking for AUR helpers..."
    
    if aur_helper=$(detect_aur_helper); then
        install_via_aur "$aur_helper"
    else
        info "No AUR helper found (yay, paru, pikaur, pamac, trizen, yaourt)"
        info "Falling back to binary installation..."
        echo
    fi
fi

# Check for curl
command -v curl >/dev/null ||
    error 'curl is required to install dops'

REPO="Mikescher/better-docker-ps"
BINARY_NAME=""

# Platform detection
OS=$(uname -s)
ARCH=$(uname -m)

info "Detecting platform: ${OS}/${ARCH}..."

case "$OS" in
    Linux)
        if [ "$ARCH" = "x86_64" ]; then
            BINARY_NAME="dops_linux-amd64-static"
        elif [ "$ARCH" = "aarch64" ] || [ "$ARCH" = "arm64" ]; then
            BINARY_NAME="dops_linux-arm64-static"
        fi
        ;;
    Darwin)
        if [ "$ARCH" = "arm64" ]; then
            BINARY_NAME="dops_macos-arm64"
        elif [ "$ARCH" = "x86_64" ]; then
            error "Intel-based Macs are not supported."
        fi
        ;;
esac

if [ -z "$BINARY_NAME" ]; then
    error "Unsupported OS or Architecture: ${OS}/${ARCH}"
fi

DOWNLOAD_URL="https://github.com/${REPO}/releases/latest/download/${BINARY_NAME}"

install_env=DOPS_INSTALL
bin_env=\$$install_env/bin

install_dir=${!install_env:-$HOME/.dops}
bin_dir=$install_dir/bin
exe=$bin_dir/dops

if [[ ! -d $bin_dir ]]; then
    mkdir -p "$bin_dir" ||
        error "Failed to create install directory \"$bin_dir\""
fi

info "Downloading 'dops' from ${DOWNLOAD_URL}..."

curl --fail --location --progress-bar --output "$exe" "$DOWNLOAD_URL" ||
    error "Failed to download dops from \"$DOWNLOAD_URL\""

chmod +x "$exe" ||
    error 'Failed to set permissions on dops executable'

tildify() {
    if [[ $1 = $HOME/* ]]; then
        local replacement=\~/
        echo "${1/$HOME\//$replacement}"
    else
        echo "$1"
    fi
}

success "dops was installed successfully to $Bold_Green$(tildify "$exe")"

if command -v dops >/dev/null; then
    echo "Run 'dops --help' to get started"
    exit
fi

refresh_command=''

tilde_bin_dir=$(tildify "$bin_dir")
quoted_install_dir=\"${install_dir//\"/\\\"}\"

if [[ $quoted_install_dir = \"$HOME/* ]]; then
    quoted_install_dir=${quoted_install_dir/$HOME\//\$HOME/}
fi

echo

case $(basename "$SHELL") in
fish)
    commands=(
        "set --export $install_env $quoted_install_dir"
        "set --export PATH $bin_env \$PATH"
    )

    fish_config=$HOME/.config/fish/config.fish
    tilde_fish_config=$(tildify "$fish_config")

    if [[ -w $fish_config ]]; then
        {
            echo -e '\n# dops'
            for command in "${commands[@]}"; do
                echo "$command"
            done
        } >>"$fish_config"
        info "Added \"$tilde_bin_dir\" to \$PATH in \"$tilde_fish_config\""
        refresh_command="source $tilde_fish_config"
    else
        echo "Manually add the directory to $tilde_fish_config (or similar):"
        for command in "${commands[@]}"; do
            info_bold "  $command"
        done
    fi
    ;;
zsh)
    commands=(
        "export $install_env=$quoted_install_dir"
        "export PATH=\"$bin_env:\$PATH\""
    )

    zsh_config=$HOME/.zshrc
    tilde_zsh_config=$(tildify "$zsh_config")

    if [[ -w $zsh_config ]]; then
        {
            echo -e '\n# dops'
            for command in "${commands[@]}"; do
                echo "$command"
            done
        } >>"$zsh_config"
        info "Added \"$tilde_bin_dir\" to \$PATH in \"$tilde_zsh_config\""
        refresh_command="exec $SHELL"
    else
        echo "Manually add the directory to $tilde_zsh_config (or similar):"
        for command in "${commands[@]}"; do
            info_bold "  $command"
        done
    fi
    ;;
bash)
    commands=(
        "export $install_env=$quoted_install_dir"
        "export PATH=\"$bin_env:\$PATH\""
    )

    bash_configs=(
        "$HOME/.bashrc"
        "$HOME/.bash_profile"
    )

    if [[ ${XDG_CONFIG_HOME:-} ]]; then
        bash_configs+=(
            "$XDG_CONFIG_HOME/.bash_profile"
            "$XDG_CONFIG_HOME/.bashrc"
            "$XDG_CONFIG_HOME/bash_profile"
            "$XDG_CONFIG_HOME/bashrc"
        )
    fi

    set_manually=true
    for bash_config in "${bash_configs[@]}"; do
        tilde_bash_config=$(tildify "$bash_config")

        if [[ -w $bash_config ]]; then
            {
                echo -e '\n# dops'
                for command in "${commands[@]}"; do
                    echo "$command"
                done
            } >>"$bash_config"
            info "Added \"$tilde_bin_dir\" to \$PATH in \"$tilde_bash_config\""
            refresh_command="source $bash_config"
            set_manually=false
            break
        fi
    done

    if [[ $set_manually = true ]]; then
        echo "Manually add the directory to your shell configuration file (or similar):"
        for command in "${commands[@]}"; do
            info_bold "  $command"
        done
    fi
    ;;
*)
    echo 'Manually add the directory to your shell configuration file (or similar):'
    info_bold "  export $install_env=$quoted_install_dir"
    info_bold "  export PATH=\"$bin_env:\$PATH\""
    ;;
esac

echo
info "To get started, run:"
echo

if [[ $refresh_command ]]; then
    info_bold "  $refresh_command"
fi

info_bold "  dops --help"

echo
info "To use 'dops' as a drop-in replacement for 'docker ps',"
info "add the following function to your shell configuration file (e.g., ~/.zshrc, ~/.bashrc):"
echo
info_bold 'docker() {'
info_bold '  case $1 in'
info_bold '    ps)'
info_bold '      shift'
info_bold '      command dops "$@"'
info_bold '      ;;'
info_bold '    *)'
info_bold '      command docker "$@";;'
info_bold '  esac'
info_bold '}'


================================================
FILE: printer/printer.go
================================================
package printer

import (
	"better-docker-ps/cli"
	"better-docker-ps/docker"
	"git.blackforestbytes.com/BlackForestBytes/goext/mathext"
	"git.blackforestbytes.com/BlackForestBytes/goext/termext"
	"strings"
)

type ColFun = func(ctx *cli.PSContext, allData []docker.ContainerSchema, cont *docker.ContainerSchema) []string

func Width(ctx *cli.PSContext, data []docker.ContainerSchema, cols []ColFun) int {
	var cells = make([][]string, 0)

	if ctx.Opt.PrintHeader {
		row := make([]string, 0)

		for _, fn := range cols {
			h := fn(ctx, data, nil)
			row = append(row, h[0])
		}

		cells = append(cells, row)
	}

	for _, dat := range data {
		extrow := make([][]string, 0)

		maxheight := 1
		for _, fn := range cols {
			h := fn(ctx, data, &dat)
			extrow = append(extrow, h)
			maxheight = mathext.Max(maxheight, len(h))
		}

		for yy := 0; yy < maxheight; yy++ {
			row := make([]string, len(cols))
			for xx := 0; xx < len(cols); xx++ {
				if yy < len(extrow[xx]) {
					row[xx] = extrow[xx][yy]
				}
			}
			cells = append(cells, row)
		}

	}

	if len(cells) == 0 {
		return 0
	}

	lens := make([]int, len(cells[0]))
	for _, row := range cells {
		for i, cell := range row {
			lens[i] = mathext.Max(lens[i], RealStrLen(cell))
		}
	}

	w := 0
	for _, v := range lens {
		w += v
	}

	return w + 4*(len(cols)-1)
}

func Print(ctx *cli.PSContext, data []docker.ContainerSchema, cols []ColFun) {

	var cells = make([][]string, 0)

	if ctx.Opt.PrintHeader {
		row := make([]string, 0)

		for _, fn := range cols {
			h := fn(ctx, data, nil)
			row = append(row, h[0])
		}

		cells = append(cells, row)
	}

	for _, dat := range data {
		extrow := make([][]string, 0)

		maxheight := 1
		for _, fn := range cols {
			h := fn(ctx, data, &dat)
			extrow = append(extrow, h)
			maxheight = mathext.Max(maxheight, len(h))
		}

		for yy := 0; yy < maxheight; yy++ {
			row := make([]string, len(cols))
			for xx := 0; xx < len(cols); xx++ {
				if yy < len(extrow[xx]) {
					row[xx] = extrow[xx][yy]
				}
			}
			cells = append(cells, row)
		}

	}

	if len(cells) == 0 {
		return
	}

	lens := make([]int, len(cells[0]))
	for _, row := range cells {
		for i, cell := range row {
			lens[i] = mathext.Max(lens[i], RealStrLen(cell))
		}
	}

	for rowidx, row := range cells {

		{
			rowstr := ""
			for colidx, cell := range row {
				if colidx > 0 {
					rowstr += "    "
				}
				if colidx == len(row)-1 {
					rowstr += cell // do not pad last
				} else {
					rowstr += TermStrPadRight(cell, " ", lens[colidx])
				}
			}
			ctx.PrintPrimaryOutput(rowstr)
		}

		if ctx.Opt.PrintHeader && ctx.Opt.PrintHeaderLines && rowidx == 0 {
			rowstr := ""
			for colidx := range row {
				if colidx > 0 {
					rowstr += "    "
				}
				rowstr += TermStrPadRight("", "-", lens[colidx])
			}
			ctx.PrintPrimaryOutput(rowstr)
		}
	}

}

func RealStrLen(cell string) int {
	return len([]rune(termext.CleanString(cell)))
}

func TermStrPadRight(str string, pad string, padlen int) string {
	if pad == "" {
		pad = " "
	}

	if RealStrLen(str) >= padlen {
		return str
	}

	return str + strings.Repeat(pad, padlen-RealStrLen(str))[0:(padlen-RealStrLen(str))]
}


================================================
FILE: pserr/err.go
================================================
package pserr

import "github.com/joomcode/errorx"

var (
	DopsErrors = errorx.NewNamespace("dops")
)

var (
	DirectOutput = DopsErrors.NewType("direct_out")
)

var (
	Exitcode = errorx.RegisterProperty("dops.exitcode")
)


================================================
FILE: pserr/util.go
================================================
package pserr

import (
	"fmt"
	"github.com/joomcode/errorx"
)

func GetDirectOutput(err error) *errorx.Error {

	sub := err
	for sub != nil {

		errx := errorx.Cast(sub)
		if errx == nil {
			break
		}

		if uw := errx.Unwrap(); uw != nil {
			sub = uw
			continue
		}

		if errx.Type() == DirectOutput {
			return errx
		}

		sub = errx.Cause()
	}

	return nil
}

func FormatError(err error, verbose bool) string {
	if errx := GetDirectOutput(err); errx != nil {
		if verbose {
			return fmt.Sprintf("%s\n\n%+v", errx.Message(), err)
		} else {
			return errx.Message()
		}
	}

	if verbose {
		return fmt.Sprintf("%+v", err)
	} else {
		return err.Error()
	}
}

func GetExitCode(err error, fallback int) int {
	if errx := GetDirectOutput(err); errx != nil {
		if ec, ok := errx.Property(Exitcode); ok {
			if eci, ok := ec.(int); ok {
				return eci
			}
		}
	}

	return fallback
}
Download .txt
gitextract_b7umzfrm/

├── .gitignore
├── .idea/
│   ├── .gitignore
│   ├── better-docker-ps.iml
│   ├── inspectionProfiles/
│   │   └── Project_Default.xml
│   ├── modules.xml
│   └── vcs.xml
├── LICENSE
├── Makefile
├── README.md
├── _data/
│   └── package-data/
│       ├── aur-bin/
│       │   ├── .gitignore
│       │   └── PKGBUILD
│       ├── aur-bin.sh
│       ├── aur-git/
│       │   ├── .gitignore
│       │   └── PKGBUILD
│       ├── aur-git.sh
│       ├── homebrew/
│       │   └── dops.rb
│       ├── homebrew.sh
│       └── sanitycheck.sh
├── cli/
│   ├── argTuple.go
│   ├── context.go
│   ├── options.go
│   └── parser.go
├── cmd/
│   └── dops/
│       └── main.go
├── consts/
│   ├── api.go
│   ├── exitcode.go
│   ├── version.go
│   └── version.sh
├── docker/
│   ├── api.go
│   ├── schema.go
│   └── util.go
├── go.mod
├── go.sum
├── impl/
│   ├── columns.go
│   └── impl.go
├── install.sh
├── printer/
│   └── printer.go
└── pserr/
    ├── err.go
    └── util.go
Download .txt
SYMBOL INDEX (138 symbols across 16 files)

FILE: _data/package-data/homebrew/dops.rb
  class Dops (line 1) | class Dops < Formula
    method install (line 8) | def install

FILE: cli/argTuple.go
  type ArgumentTuple (line 3) | type ArgumentTuple struct

FILE: cli/context.go
  type PSContext (line 16) | type PSContext struct
    method PrintPrimaryOutput (line 22) | func (c PSContext) PrintPrimaryOutput(msg string) {
    method PrintFatalMessage (line 30) | func (c PSContext) PrintFatalMessage(msg string) {
    method PrintFatalError (line 38) | func (c PSContext) PrintFatalError(e error) {
    method PrintErrorMessage (line 46) | func (c PSContext) PrintErrorMessage(msg string) {
    method PrintVerbose (line 54) | func (c PSContext) PrintVerbose(msg string) {
    method PrintVerboseHeader (line 62) | func (c PSContext) PrintVerboseHeader(msg string) {
    method PrintVerboseKV (line 74) | func (c PSContext) PrintVerboseKV(key string, vval any) {
    method ClearTerminal (line 106) | func (c PSContext) ClearTerminal() {
    method printPrimaryRaw (line 110) | func (c PSContext) printPrimaryRaw(msg string) {
    method printErrorRaw (line 118) | func (c PSContext) printErrorRaw(msg string) {
    method printVerboseRaw (line 130) | func (c PSContext) printVerboseRaw(msg string) {
    method Finish (line 172) | func (c PSContext) Finish() {
    method GetIntFromCache (line 176) | func (c *PSContext) GetIntFromCache(key string, calc func() int) int {
  function writeStdout (line 142) | func writeStdout(msg string) {
  function writeStderr (line 149) | func writeStderr(msg string) {
  function NewContext (line 156) | func NewContext(opt Options) (*PSContext, error) {
  function NewEarlyContext (line 164) | func NewEarlyContext() *PSContext {

FILE: cli/options.go
  type SortDirection (line 16) | type SortDirection
  constant SortASC (line 19) | SortASC  SortDirection = "ASC"
  constant SortDESC (line 20) | SortDESC SortDirection = "DESC"
  type Options (line 23) | type Options struct
    method GetSocket (line 101) | func (o Options) GetSocket() string {
  function DefaultCLIOptions (line 49) | func DefaultCLIOptions() Options {
  function getDefaultSocket (line 90) | func getDefaultSocket() string {
  type dockerContext (line 141) | type dockerContext struct
    method socket (line 152) | func (ctx dockerContext) socket() string {
  function p (line 156) | func p(v bool) *bool {

FILE: cli/parser.go
  function ParseCommandline (line 19) | func ParseCommandline(columnKeys []string) (Options, error) {
  function parseCommandlineInternal (line 27) | func parseCommandlineInternal(columnKeys []string) (Options, error) {

FILE: cmd/dops/main.go
  function main (line 17) | func main() {
  function printHelp (line 80) | func printHelp(ctx *cli.PSContext) {

FILE: consts/api.go
  constant DockerAPIContainerList (line 4) | DockerAPIContainerList = "http://localhost/v1.44/containers/json"

FILE: consts/exitcode.go
  constant ExitcodeError (line 4) | ExitcodeError                   = 60
  constant ExitcodePanic (line 5) | ExitcodePanic                   = 61
  constant ExitcodeNoArguments (line 6) | ExitcodeNoArguments             = 62
  constant ExitcodeCLIParse (line 7) | ExitcodeCLIParse                = 63
  constant ExitcodeNoLogin (line 8) | ExitcodeNoLogin                 = 64
  constant ExitcodeUnsupportedOutputFormat (line 9) | ExitcodeUnsupportedOutputFormat = 65
  constant ExitcodeRecordNotFound (line 10) | ExitcodeRecordNotFound          = 66
  constant ExitcodeInvalidSession (line 14) | ExitcodeInvalidSession            = 81
  constant ExitcodePasswordNotFound (line 15) | ExitcodePasswordNotFound          = 82
  constant ExitcodeParentNotAFolder (line 16) | ExitcodeParentNotAFolder          = 83
  constant ExitcodeInvalidPosition (line 17) | ExitcodeInvalidPosition           = 84
  constant ExitcodeBookmarkFieldNotSupported (line 18) | ExitcodeBookmarkFieldNotSupported = 85

FILE: consts/version.go
  constant BETTER_DOCKER_PS_VERSION (line 5) | BETTER_DOCKER_PS_VERSION = "1.17"

FILE: docker/api.go
  function ListContainer (line 21) | func ListContainer(ctx *cli.PSContext) ([]byte, error) {

FILE: docker/schema.go
  type ContainerSchema (line 8) | type ContainerSchema struct
    method PortsSorted (line 26) | func (s ContainerSchema) PortsSorted() []PortSchema {
  type ContainerHostConfig (line 42) | type ContainerHostConfig struct
  type ContainerNetworkSettings (line 46) | type ContainerNetworkSettings struct
  type ContainerSingleNetworkSettings (line 49) | type ContainerSingleNetworkSettings struct
  type PortSchema (line 61) | type PortSchema struct
    method IsLoopback (line 68) | func (s PortSchema) IsLoopback() bool {
  type ContainerMount (line 73) | type ContainerMount struct
  type ContainerState (line 83) | type ContainerState
    method Num (line 94) | func (ct ContainerState) Num() int {
  constant StateCreated (line 86) | StateCreated    ContainerState = "created"
  constant StateRunning (line 87) | StateRunning    ContainerState = "running"
  constant StateRestarting (line 88) | StateRestarting ContainerState = "restarting"
  constant StateExited (line 89) | StateExited     ContainerState = "exited"
  constant StatePaused (line 90) | StatePaused     ContainerState = "paused"
  constant StateDead (line 91) | StateDead       ContainerState = "dead"

FILE: docker/util.go
  function SplitDockerImage (line 16) | func SplitDockerImage(ctx *cli.PSContext, img string) (string, string, s...

FILE: impl/columns.go
  type ColumnDef (line 27) | type ColumnDef struct
  function ColContainerID (line 62) | func ColContainerID(ctx *cli.PSContext, allData []docker.ContainerSchema...
  function ColFullImage (line 74) | func ColFullImage(ctx *cli.PSContext, allData []docker.ContainerSchema, ...
  function ColRegistry (line 82) | func ColRegistry(ctx *cli.PSContext, allData []docker.ContainerSchema, c...
  function ColImage (line 92) | func ColImage(ctx *cli.PSContext, allData []docker.ContainerSchema, cont...
  function ColImageTag (line 102) | func ColImageTag(ctx *cli.PSContext, allData []docker.ContainerSchema, c...
  function ColCommand (line 112) | func ColCommand(ctx *cli.PSContext, allData []docker.ContainerSchema, co...
  function ColShortCommand (line 127) | func ColShortCommand(ctx *cli.PSContext, allData []docker.ContainerSchem...
  function ColRunningFor (line 141) | func ColRunningFor(ctx *cli.PSContext, allData []docker.ContainerSchema,...
  function ColCreatedAt (line 152) | func ColCreatedAt(ctx *cli.PSContext, allData []docker.ContainerSchema, ...
  function ColState (line 170) | func ColState(ctx *cli.PSContext, allData []docker.ContainerSchema, cont...
  function ColStatus (line 184) | func ColStatus(ctx *cli.PSContext, allData []docker.ContainerSchema, con...
  function ColPortsExposed (line 196) | func ColPortsExposed(ctx *cli.PSContext, allData []docker.ContainerSchem...
  function ColPortsPublicPart (line 225) | func ColPortsPublicPart(ctx *cli.PSContext, allData []docker.ContainerSc...
  function ColPortsPublished (line 245) | func ColPortsPublished(ctx *cli.PSContext, allData []docker.ContainerSch...
  function ColPortsPublishedShort (line 291) | func ColPortsPublishedShort(ctx *cli.PSContext, allData []docker.Contain...
  function ColPortsPublishedLong (line 334) | func ColPortsPublishedLong(ctx *cli.PSContext, allData []docker.Containe...
  function ColPortsNotPublished (line 388) | func ColPortsNotPublished(ctx *cli.PSContext, allData []docker.Container...
  function ColName (line 410) | func ColName(ctx *cli.PSContext, allData []docker.ContainerSchema, cont ...
  function ColSize (line 426) | func ColSize(ctx *cli.PSContext, allData []docker.ContainerSchema, cont ...
  function ColMounts (line 438) | func ColMounts(ctx *cli.PSContext, allData []docker.ContainerSchema, con...
  function ColIP (line 455) | func ColIP(ctx *cli.PSContext, allData []docker.ContainerSchema, cont *d...
  function ColLabels (line 470) | func ColLabels(ctx *cli.PSContext, allData []docker.ContainerSchema, con...
  function ColLabelKeys (line 483) | func ColLabelKeys(ctx *cli.PSContext, allData []docker.ContainerSchema, ...
  function ColNetworks (line 496) | func ColNetworks(ctx *cli.PSContext, allData []docker.ContainerSchema, c...
  function ColPlaintext (line 509) | func ColPlaintext(str string) printer.ColFun {
  function SortContainerID (line 517) | func SortContainerID(ctx *cli.PSContext, v1 *docker.ContainerSchema, v2 ...
  function SortFullImage (line 525) | func SortFullImage(ctx *cli.PSContext, v1 *docker.ContainerSchema, v2 *d...
  function SortRegistry (line 529) | func SortRegistry(ctx *cli.PSContext, v1 *docker.ContainerSchema, v2 *do...
  function SortImage (line 536) | func SortImage(ctx *cli.PSContext, v1 *docker.ContainerSchema, v2 *docke...
  function SortImageTag (line 543) | func SortImageTag(ctx *cli.PSContext, v1 *docker.ContainerSchema, v2 *do...
  function SortCommand (line 550) | func SortCommand(ctx *cli.PSContext, v1 *docker.ContainerSchema, v2 *doc...
  function SortShortCommand (line 554) | func SortShortCommand(ctx *cli.PSContext, v1 *docker.ContainerSchema, v2...
  function SortRunningFor (line 570) | func SortRunningFor(ctx *cli.PSContext, v1 *docker.ContainerSchema, v2 *...
  function SortCreatedAt (line 574) | func SortCreatedAt(ctx *cli.PSContext, v1 *docker.ContainerSchema, v2 *d...
  function SortState (line 578) | func SortState(ctx *cli.PSContext, v1 *docker.ContainerSchema, v2 *docke...
  function SortStatus (line 582) | func SortStatus(ctx *cli.PSContext, v1 *docker.ContainerSchema, v2 *dock...
  function SortPortsExposed (line 586) | func SortPortsExposed(ctx *cli.PSContext, v1 *docker.ContainerSchema, v2...
  function SortPortsPublished (line 606) | func SortPortsPublished(ctx *cli.PSContext, v1 *docker.ContainerSchema, ...
  function SortPortsPublishedShort (line 629) | func SortPortsPublishedShort(ctx *cli.PSContext, v1 *docker.ContainerSch...
  function SortPortsPublishedLong (line 652) | func SortPortsPublishedLong(ctx *cli.PSContext, v1 *docker.ContainerSche...
  function SortPortsNotPublished (line 675) | func SortPortsNotPublished(ctx *cli.PSContext, v1 *docker.ContainerSchem...
  function SortPortsPublicPart (line 698) | func SortPortsPublicPart(ctx *cli.PSContext, v1 *docker.ContainerSchema,...
  function SortName (line 721) | func SortName(ctx *cli.PSContext, v1 *docker.ContainerSchema, v2 *docker...
  function SortSize (line 731) | func SortSize(ctx *cli.PSContext, v1 *docker.ContainerSchema, v2 *docker...
  function SortMounts (line 735) | func SortMounts(ctx *cli.PSContext, v1 *docker.ContainerSchema, v2 *dock...
  function SortIP (line 749) | func SortIP(ctx *cli.PSContext, v1 *docker.ContainerSchema, v2 *docker.C...
  function SortLabels (line 777) | func SortLabels(ctx *cli.PSContext, v1 *docker.ContainerSchema, v2 *dock...
  function SortLabelKeys (line 791) | func SortLabelKeys(ctx *cli.PSContext, v1 *docker.ContainerSchema, v2 *d...
  function SortNetworks (line 805) | func SortNetworks(ctx *cli.PSContext, v1 *docker.ContainerSchema, v2 *do...
  function getColFun (line 821) | func getColFun(colkey string) (printer.ColFun, bool) {
  function templateColFun (line 846) | func templateColFun(fmtstr string, header string) printer.ColFun {
  function getSortFun (line 957) | func getSortFun(colkey string) (ColSortFun, bool) {
  function replaceSingleLineColumnData (line 964) | func replaceSingleLineColumnData(ctx *cli.PSContext, allData []docker.Co...
  function parseTableDef (line 976) | func parseTableDef(fmt string) []printer.ColFun {
  function stateColor (line 989) | func stateColor(state docker.ContainerState, value string) string {
  function statusColor (line 1007) | func statusColor(status string, value string) string {
  function ipExpand (line 1030) | func ipExpand(ip string) string {

FILE: impl/impl.go
  function Execute (line 20) | func Execute(ctx *cli.PSContext) error {
  function Watch (line 24) | func Watch(ctx *cli.PSContext, d time.Duration) error {
  function executeSingle (line 46) | func executeSingle(ctx *cli.PSContext, clear bool) error {
  function doSearch (line 93) | func doSearch(ctx *cli.PSContext, data []docker.ContainerSchema, needle ...
  function doSort (line 121) | func doSort(ctx *cli.PSContext, data []docker.ContainerSchema, skeys []s...
  function doOutput (line 152) | func doOutput(ctx *cli.PSContext, data []docker.ContainerSchema, format ...

FILE: printer/printer.go
  function Width (line 13) | func Width(ctx *cli.PSContext, data []docker.ContainerSchema, cols []Col...
  function Print (line 68) | func Print(ctx *cli.PSContext, data []docker.ContainerSchema, cols []Col...
  function RealStrLen (line 147) | func RealStrLen(cell string) int {
  function TermStrPadRight (line 151) | func TermStrPadRight(str string, pad string, padlen int) string {

FILE: pserr/util.go
  function GetDirectOutput (line 8) | func GetDirectOutput(err error) *errorx.Error {
  function FormatError (line 33) | func FormatError(err error, verbose bool) string {
  function GetExitCode (line 49) | func GetExitCode(err error, fallback int) int {
Condensed preview — 38 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (129K chars).
[
  {
    "path": ".gitignore",
    "chars": 779,
    "preview": "\n########## GOLAND ##########\n\n.idea/**/workspace.xml\n.idea/**/tasks.xml\n.idea/**/usage.statistics.xml\n.idea/**/dictiona"
  },
  {
    "path": ".idea/.gitignore",
    "chars": 176,
    "preview": "# Default ignored files\n/shelf/\n/workspace.xml\n# Editor-based HTTP Client requests\n/httpRequests/\n# Datasource local sto"
  },
  {
    "path": ".idea/better-docker-ps.iml",
    "chars": 322,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<module type=\"WEB_MODULE\" version=\"4\">\n  <component name=\"Go\" enabled=\"true\" />\n "
  },
  {
    "path": ".idea/inspectionProfiles/Project_Default.xml",
    "chars": 562,
    "preview": "<component name=\"InspectionProjectProfileManager\">\n  <profile version=\"1.0\">\n    <option name=\"myName\" value=\"Project De"
  },
  {
    "path": ".idea/modules.xml",
    "chars": 284,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project version=\"4\">\n  <component name=\"ProjectModuleManager\">\n    <modules>\n   "
  },
  {
    "path": ".idea/vcs.xml",
    "chars": 180,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project version=\"4\">\n  <component name=\"VcsDirectoryMappings\">\n    <mapping dire"
  },
  {
    "path": "LICENSE",
    "chars": 1070,
    "preview": "MIT License\n\nCopyright (c) 2022 Mike Schwörer\n\nPermission is hereby granted, free of charge, to any person obtaining a c"
  },
  {
    "path": "Makefile",
    "chars": 2790,
    "preview": "build:\n\tgo generate ./...\n\tCGO_ENABLED=0 go build -o _out/dops ./cmd/dops\n\nrun: build\n\t./_out/dops\n\nclean:\n\tgo clean\n\trm"
  },
  {
    "path": "README.md",
    "chars": 10576,
    "preview": "# ./dops - better `docker ps` \nA replacement for the default docker-ps that tries really hard to fit within your termina"
  },
  {
    "path": "_data/package-data/aur-bin/.gitignore",
    "chars": 54,
    "preview": "pkg/\nsrc/\ndops/\ndops-bin/\ndops-git/\n.SRCINFO\n*.tar.zst"
  },
  {
    "path": "_data/package-data/aur-bin/PKGBUILD",
    "chars": 664,
    "preview": "# Maintainer: Mikescher <aur@mikescher.com>\n# Repo:       https://github.com/Mikescher/better-docker-ps\n\npkgname=dops-bi"
  },
  {
    "path": "_data/package-data/aur-bin.sh",
    "chars": 1438,
    "preview": "#!/bin/bash\n\nset -o nounset   # disallow usage of unset vars  ( set -u )\nset -o errexit   # Exit immediately if a pipeli"
  },
  {
    "path": "_data/package-data/aur-git/.gitignore",
    "chars": 54,
    "preview": "pkg/\nsrc/\ndops/\ndops-bin/\ndops-git/\n.SRCINFO\n*.tar.zst"
  },
  {
    "path": "_data/package-data/aur-git/PKGBUILD",
    "chars": 604,
    "preview": "# Maintainer: Mikescher <aur@mikescher.com>\n# Repo:       https://github.com/Mikescher/better-docker-ps\n\npkgname=dops-gi"
  },
  {
    "path": "_data/package-data/aur-git.sh",
    "chars": 1210,
    "preview": "#!/bin/bash\n\nset -o nounset   # disallow usage of unset vars  ( set -u )\nset -o errexit   # Exit immediately if a pipeli"
  },
  {
    "path": "_data/package-data/homebrew/dops.rb",
    "chars": 435,
    "preview": "class Dops < Formula\n\n  desc     \" A replacement for the default docker-ps that tries really hard to fit into the width "
  },
  {
    "path": "_data/package-data/homebrew.sh",
    "chars": 1269,
    "preview": "#!/bin/bash\n\nset -o nounset   # disallow usage of unset vars  ( set -u )\nset -o errexit   # Exit immediately if a pipeli"
  },
  {
    "path": "_data/package-data/sanitycheck.sh",
    "chars": 517,
    "preview": "#!/bin/bash\n\ncd \"$(dirname \"$0\")\"\n\nversion_tag=\"$(cd ../../ && git tag --sort=-v:refname | grep -P 'v[0-9\\.]' | head -1 "
  },
  {
    "path": "cli/argTuple.go",
    "chars": 72,
    "preview": "package cli\n\ntype ArgumentTuple struct {\n\tKey   string\n\tValue *string\n}\n"
  },
  {
    "path": "cli/context.go",
    "chars": 3299,
    "preview": "package cli\n\nimport (\n\t\"better-docker-ps/pserr\"\n\t\"context\"\n\t\"encoding/hex\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"time\"\n\n\t\"git.blackf"
  },
  {
    "path": "cli/options.go",
    "chars": 4499,
    "preview": "package cli\n\nimport (\n\t\"encoding/json\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"runtime\"\n\t\"strings\"\n\t\"time\"\n\n\t\"git.blackforest"
  },
  {
    "path": "cli/parser.go",
    "chars": 13285,
    "preview": "package cli\n\nimport (\n\t\"better-docker-ps/pserr\"\n\t\"fmt\"\n\t\"github.com/BurntSushi/toml\"\n\t\"github.com/kirsle/configdir\"\n\t\"gi"
  },
  {
    "path": "cmd/dops/main.go",
    "chars": 7169,
    "preview": "package main\n\nimport (\n\t\"better-docker-ps/cli\"\n\t\"better-docker-ps/consts\"\n\t\"better-docker-ps/impl\"\n\t\"better-docker-ps/ps"
  },
  {
    "path": "consts/api.go",
    "chars": 201,
    "preview": "package consts\n\n// DockerAPIContainerList -> see https://docs.docker.com/engine/api/v1.41/#tag/Container/operation/Conta"
  },
  {
    "path": "consts/exitcode.go",
    "chars": 503,
    "preview": "package consts\n\nconst (\n\tExitcodeError                   = 60\n\tExitcodePanic                   = 61\n\tExitcodeNoArguments"
  },
  {
    "path": "consts/version.go",
    "chars": 92,
    "preview": "package consts\n\n//go:generate /bin/bash version.sh\n\nconst BETTER_DOCKER_PS_VERSION = \"1.17\"\n"
  },
  {
    "path": "consts/version.sh",
    "chars": 161,
    "preview": "#!/bin/bash\n\nsed -i 's/const BETTER_DOCKER_PS_VERSION = \".*\"/const BETTER_DOCKER_PS_VERSION = \"'$(git describe --tags --"
  },
  {
    "path": "docker/api.go",
    "chars": 1619,
    "preview": "package docker\n\nimport (\n\t\"better-docker-ps/cli\"\n\t\"better-docker-ps/consts\"\n\t\"better-docker-ps/pserr\"\n\t\"context\"\n\t\"encod"
  },
  {
    "path": "docker/schema.go",
    "chars": 3178,
    "preview": "package docker\n\nimport (\n\t\"git.blackforestbytes.com/BlackForestBytes/goext/langext\"\n\t\"net\"\n)\n\ntype ContainerSchema struc"
  },
  {
    "path": "docker/util.go",
    "chars": 1005,
    "preview": "package docker\n\nimport (\n\t\"better-docker-ps/cli\"\n\t\"strings\"\n)\n\nvar registryPrefixList = []string{\n\t\".com\",\n\t\".de\",\n\t\".ne"
  },
  {
    "path": "go.mod",
    "chars": 1790,
    "preview": "module better-docker-ps\n\ngo 1.24.2\n\nrequire (\n\tgit.blackforestbytes.com/BlackForestBytes/goext v0.0.572\n\tgithub.com/Burn"
  },
  {
    "path": "go.sum",
    "chars": 11156,
    "preview": "git.blackforestbytes.com/BlackForestBytes/goext v0.0.572 h1:NALJ4KKkrRZcNJNsmGrUsjFdOclHSA/KyB6f94QV43k=\ngit.blackforest"
  },
  {
    "path": "impl/columns.go",
    "chars": 29142,
    "preview": "package impl\n\nimport (\n\t\"better-docker-ps/cli\"\n\t\"better-docker-ps/docker\"\n\t\"better-docker-ps/printer\"\n\t\"bytes\"\n\t\"encodin"
  },
  {
    "path": "impl/impl.go",
    "chars": 4412,
    "preview": "package impl\n\nimport (\n\t\"better-docker-ps/cli\"\n\t\"better-docker-ps/docker\"\n\t\"better-docker-ps/printer\"\n\t\"better-docker-ps"
  },
  {
    "path": "install.sh",
    "chars": 8390,
    "preview": "#!/usr/bin/env bash\nset -euo pipefail\n\n# This script handles the installation of 'dops' by detecting the OS\n# and archit"
  },
  {
    "path": "printer/printer.go",
    "chars": 3147,
    "preview": "package printer\n\nimport (\n\t\"better-docker-ps/cli\"\n\t\"better-docker-ps/docker\"\n\t\"git.blackforestbytes.com/BlackForestBytes"
  },
  {
    "path": "pserr/err.go",
    "chars": 222,
    "preview": "package pserr\n\nimport \"github.com/joomcode/errorx\"\n\nvar (\n\tDopsErrors = errorx.NewNamespace(\"dops\")\n)\n\nvar (\n\tDirectOutp"
  },
  {
    "path": "pserr/util.go",
    "chars": 884,
    "preview": "package pserr\n\nimport (\n\t\"fmt\"\n\t\"github.com/joomcode/errorx\"\n)\n\nfunc GetDirectOutput(err error) *errorx.Error {\n\n\tsub :="
  }
]

About this extraction

This page contains the full source code of the Mikescher/better-docker-ps GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 38 files (114.5 KB), approximately 37.3k tokens, and a symbol index with 138 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!