Implemented support for 'Apple Container' as a runtime environment

Apple Container is a Docker-like system for managing containers in macOS
26+ Tahoe. Not all Docker features are supported but enough support
exists to have Lazydocker function as a frontend.
This commit is contained in:
mazdak 2025-07-22 22:36:01 -04:00
parent 4e2f26b21e
commit 5bf6870d0f
No known key found for this signature in database
131 changed files with 4119 additions and 2657 deletions

1
.gitignore vendored
View file

@ -3,3 +3,4 @@ TODO.md
Lazydocker.code-workspace Lazydocker.code-workspace
.vscode .vscode
.idea .idea
coverage.txt

View file

@ -40,7 +40,7 @@
<img src="https://user-images.githubusercontent.com/8456633/59972109-8e9c8480-95cc-11e9-8350-38f7f86ba76d.png"> <img src="https://user-images.githubusercontent.com/8456633/59972109-8e9c8480-95cc-11e9-8350-38f7f86ba76d.png">
</p> </p>
A simple terminal UI for both docker and docker-compose, written in Go with the [gocui](https://github.com/jroimartin/gocui 'gocui') library. A simple terminal UI for both docker and docker-compose, written in Go with the [gocui](https://github.com/jroimartin/gocui 'gocui') library. Now with experimental Apple Container support!
![CI](https://github.com/jesseduffield/lazygit/workflows/Continuous%20Integration/badge.svg) ![CI](https://github.com/jesseduffield/lazygit/workflows/Continuous%20Integration/badge.svg)
[![Go Report Card](https://goreportcard.com/badge/github.com/jesseduffield/lazydocker)](https://goreportcard.com/report/github.com/jesseduffield/lazydocker) [![Go Report Card](https://goreportcard.com/badge/github.com/jesseduffield/lazydocker)](https://goreportcard.com/report/github.com/jesseduffield/lazydocker)
@ -86,9 +86,15 @@ Memorising docker commands is hard. Memorising aliases is slightly less hard. Ke
## Requirements ## Requirements
### Docker Runtime (Default)
- Docker >= **1.13** (API >= **1.25**) - Docker >= **1.13** (API >= **1.25**)
- Docker-Compose >= **1.23.2** (optional) - Docker-Compose >= **1.23.2** (optional)
### Apple Container Runtime (Experimental)
- macOS 26 beta or later
- Apple Silicon Mac
- Apple Container CLI (`container` command available in PATH)
## Installation ## Installation
### Homebrew ### Homebrew
@ -255,14 +261,39 @@ You can also use `go run main.go` to compile and run in one go (pun definitely i
## Usage ## Usage
### Basic Usage
Call `lazydocker` in your terminal. I personally use this a lot so I've made an alias for it like so: Call `lazydocker` in your terminal. I personally use this a lot so I've made an alias for it like so:
``` ```bash
echo "alias lzd='lazydocker'" >> ~/.zshrc echo "alias lzd='lazydocker'" >> ~/.zshrc
``` ```
(you can substitute .zshrc for whatever rc file you're using) (you can substitute .zshrc for whatever rc file you're using)
### Container Runtime Selection
By default, lazydocker uses Docker. You can specify a different container runtime using the `--runtime` flag:
```bash
# Use Docker (default)
lazydocker
lazydocker --runtime docker
# Use Apple Container (requires macOS 26 beta and Apple Silicon)
lazydocker --runtime apple
```
### Available Flags
```bash
lazydocker --help
-c --config Print the current default config
-d --debug Enable debug mode
-f --file Specify alternate compose files
-r --runtime Container runtime to use (docker, apple)
```
- Basic video tutorial [here](https://youtu.be/NICqQPxwJWw). - Basic video tutorial [here](https://youtu.be/NICqQPxwJWw).
- List of keybindings - List of keybindings
[here](/docs/keybindings). [here](/docs/keybindings).

File diff suppressed because it is too large Load diff

View file

@ -133,6 +133,20 @@ You may use the following go templates (such as `{{ .Container.ID }}` above) in
- [`{{ .Container }}`](https://pkg.go.dev/github.com/jesseduffield/lazydocker@v0.20.0/pkg/commands#Container) and its fields. For example: `{{ .Container.Container.ImageID }}` - [`{{ .Container }}`](https://pkg.go.dev/github.com/jesseduffield/lazydocker@v0.20.0/pkg/commands#Container) and its fields. For example: `{{ .Container.Container.ImageID }}`
- [`{{ .Service }}`](https://pkg.go.dev/github.com/jesseduffield/lazydocker@v0.20.0/pkg/commands#Service) and its fields. For example: `{{ .Service.Name }}` - [`{{ .Service }}`](https://pkg.go.dev/github.com/jesseduffield/lazydocker@v0.20.0/pkg/commands#Service) and its fields. For example: `{{ .Service.Name }}`
## Container Runtime
Lazydocker supports multiple container runtimes. By default it uses Docker, but you can switch to Apple Container (on macOS 26+ with Apple Silicon) using the `--runtime` flag:
```bash
# Use Docker (default)
lazydocker
# Use Apple Container
lazydocker --runtime apple
```
Note: The runtime cannot be changed from within the application. You must restart lazydocker with the appropriate flag.
## Replacements ## Replacements
You can add replacements like so: You can add replacements like so:

View file

@ -29,6 +29,7 @@ var (
configFlag = false configFlag = false
debuggingFlag = false debuggingFlag = false
composeFiles []string composeFiles []string
runtimeFlag = "docker"
) )
func main() { func main() {
@ -51,6 +52,7 @@ func main() {
flaggy.Bool(&configFlag, "c", "config", "Print the current default config") flaggy.Bool(&configFlag, "c", "config", "Print the current default config")
flaggy.Bool(&debuggingFlag, "d", "debug", "a boolean") flaggy.Bool(&debuggingFlag, "d", "debug", "a boolean")
flaggy.StringSlice(&composeFiles, "f", "file", "Specify alternate compose files") flaggy.StringSlice(&composeFiles, "f", "file", "Specify alternate compose files")
flaggy.String(&runtimeFlag, "r", "runtime", "Container runtime to use (docker, apple)")
flaggy.SetVersion(info) flaggy.SetVersion(info)
flaggy.Parse() flaggy.Parse()
@ -71,7 +73,7 @@ func main() {
log.Fatal(err.Error()) log.Fatal(err.Error())
} }
appConfig, err := config.NewAppConfig("lazydocker", version, commit, date, buildSource, debuggingFlag, composeFiles, projectDir) appConfig, err := config.NewAppConfig("lazydocker", version, commit, date, buildSource, debuggingFlag, composeFiles, projectDir, runtimeFlag)
if err != nil { if err != nil {
log.Fatal(err.Error()) log.Fatal(err.Error())
} }

View file

@ -17,13 +17,15 @@ import (
type App struct { type App struct {
closers []io.Closer closers []io.Closer
Config *config.AppConfig Config *config.AppConfig
Log *logrus.Entry Log *logrus.Entry
OSCommand *commands.OSCommand OSCommand *commands.OSCommand
DockerCommand *commands.DockerCommand DockerCommand *commands.DockerCommand
Gui *gui.Gui AppleContainerCommand *commands.AppleContainerCommand
Tr *i18n.TranslationSet ContainerRuntime *commands.ContainerRuntimeAdapter
ErrorChan chan error Gui *gui.Gui
Tr *i18n.TranslationSet
ErrorChan chan error
} }
// NewApp bootstrap a new application // NewApp bootstrap a new application
@ -41,14 +43,29 @@ func NewApp(config *config.AppConfig) (*App, error) {
} }
app.OSCommand = commands.NewOSCommand(app.Log, config) app.OSCommand = commands.NewOSCommand(app.Log, config)
// here is the place to make use of the docker-compose.yml file in the current directory // Initialize the appropriate container runtime based on config
switch config.Runtime {
app.DockerCommand, err = commands.NewDockerCommand(app.Log, app.OSCommand, app.Tr, app.Config, app.ErrorChan) case "docker":
if err != nil { // here is the place to make use of the docker-compose.yml file in the current directory
return app, err app.DockerCommand, err = commands.NewDockerCommand(app.Log, app.OSCommand, app.Tr, app.Config, app.ErrorChan)
if err != nil {
return app, err
}
app.closers = append(app.closers, app.DockerCommand)
app.ContainerRuntime = commands.NewContainerRuntimeAdapter(app.DockerCommand, nil, "docker")
containerCommand := commands.NewGuiContainerCommand(app.ContainerRuntime, app.DockerCommand, app.Config)
app.Gui, err = gui.NewGui(app.Log, app.DockerCommand, containerCommand, app.OSCommand, app.Tr, config, app.ErrorChan)
case "apple":
app.AppleContainerCommand, err = commands.NewAppleContainerCommand(app.Log, app.OSCommand, app.Tr, app.Config, app.ErrorChan)
if err != nil {
return app, err
}
app.ContainerRuntime = commands.NewContainerRuntimeAdapter(nil, app.AppleContainerCommand, "apple")
containerCommand := commands.NewGuiContainerCommand(app.ContainerRuntime, nil, app.Config)
app.Gui, err = gui.NewGui(app.Log, nil, containerCommand, app.OSCommand, app.Tr, config, app.ErrorChan)
default:
return app, err // This should be caught by config validation, but just in case
} }
app.closers = append(app.closers, app.DockerCommand)
app.Gui, err = gui.NewGui(app.Log, app.DockerCommand, app.OSCommand, app.Tr, config, app.ErrorChan)
if err != nil { if err != nil {
return app, err return app, err
} }
@ -77,6 +94,18 @@ func (app *App) KnownError(err error) (string, bool) {
originalError: "Got permission denied while trying to connect to the Docker daemon socket", originalError: "Got permission denied while trying to connect to the Docker daemon socket",
newError: app.Tr.CannotAccessDockerSocketError, newError: app.Tr.CannotAccessDockerSocketError,
}, },
{
originalError: "Apple Container CLI not found",
newError: "Apple Container CLI not found. Please ensure the 'container' command is installed and available in your PATH.",
},
{
originalError: "failed to get containers",
newError: "Failed to retrieve containers. Please check if the container runtime is running.",
},
{
originalError: "failed to get images",
newError: "Failed to retrieve images. Please check if the container runtime is running.",
},
} }
for _, mapping := range mappings { for _, mapping := range mappings {

214
pkg/app/app_test.go Normal file
View file

@ -0,0 +1,214 @@
package app
import (
"os/exec"
"testing"
"github.com/jesseduffield/lazydocker/pkg/config"
"github.com/stretchr/testify/assert"
)
func TestNewAppRuntimeSelection(t *testing.T) {
tests := []struct {
name string
runtime string
expectError bool
errorContains string
expectDocker bool
expectApple bool
}{
{
name: "docker runtime",
runtime: "docker",
expectError: false,
expectDocker: true,
expectApple: false,
},
{
name: "apple runtime",
runtime: "apple",
expectError: !appleAvailable(),
errorContains: "Apple Container CLI not found",
expectDocker: false,
expectApple: appleAvailable(),
},
{
name: "invalid runtime",
runtime: "invalid",
expectError: true,
errorContains: "unsupported runtime 'invalid'",
expectDocker: false,
expectApple: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Create app config with the test runtime
appConfig, err := config.NewAppConfig(
"lazydocker",
"test-version",
"test-commit",
"test-date",
"test-build-source",
false, // debug
[]string{}, // compose files
"/tmp", // project dir
tt.runtime,
)
if tt.runtime == "invalid" {
// Should fail at config creation for invalid runtime
assert.NotNil(t, err)
assert.Contains(t, err.Error(), tt.errorContains)
return
}
assert.Nil(t, err)
assert.Equal(t, tt.runtime, appConfig.Runtime)
// Try to create the app
app, err := NewApp(appConfig)
if tt.expectError {
assert.NotNil(t, err)
if tt.errorContains != "" {
assert.Contains(t, err.Error(), tt.errorContains)
}
} else {
assert.Nil(t, err)
assert.NotNil(t, app)
assert.Equal(t, tt.runtime, app.Config.Runtime)
// Check that the correct command was initialized
if tt.expectDocker {
assert.NotNil(t, app.DockerCommand, "DockerCommand should be initialized for docker runtime")
assert.Nil(t, app.AppleContainerCommand, "AppleContainerCommand should be nil for docker runtime")
assert.NotNil(t, app.ContainerRuntime, "ContainerRuntime should be initialized")
assert.Equal(t, "docker", app.ContainerRuntime.GetRuntimeName())
}
if tt.expectApple {
assert.Nil(t, app.DockerCommand, "DockerCommand should be nil for apple runtime")
assert.NotNil(t, app.AppleContainerCommand, "AppleContainerCommand should be initialized for apple runtime")
assert.NotNil(t, app.ContainerRuntime, "ContainerRuntime should be initialized")
assert.Equal(t, "apple", app.ContainerRuntime.GetRuntimeName())
}
}
})
}
}
func appleAvailable() bool {
_, err := exec.LookPath("container")
return err == nil
}
func TestAppRuntimeFieldsInitialization(t *testing.T) {
// Test that app properly initializes with docker runtime
appConfig, err := config.NewAppConfig(
"lazydocker",
"test-version",
"test-commit",
"test-date",
"test-build-source",
false,
[]string{},
"/tmp",
"docker",
)
assert.Nil(t, err)
app, err := NewApp(appConfig)
assert.Nil(t, err)
assert.NotNil(t, app)
// Check that all required fields are initialized
assert.NotNil(t, app.Config)
assert.NotNil(t, app.Log)
assert.NotNil(t, app.OSCommand)
assert.NotNil(t, app.Tr)
assert.NotNil(t, app.ErrorChan)
// For docker runtime
assert.NotNil(t, app.DockerCommand)
assert.Nil(t, app.AppleContainerCommand)
assert.NotNil(t, app.ContainerRuntime)
assert.Equal(t, "docker", app.ContainerRuntime.GetRuntimeName())
assert.NotNil(t, app.Gui)
}
func TestAppKnownErrorHandling(t *testing.T) {
// Create a basic app for testing error handling
appConfig, err := config.NewAppConfig(
"lazydocker",
"test-version",
"test-commit",
"test-date",
"test-build-source",
false,
[]string{},
"/tmp",
"docker",
)
assert.Nil(t, err)
app, err := NewApp(appConfig)
assert.Nil(t, err)
tests := []struct {
name string
errorMessage string
expectKnown bool
expectedText string
}{
{
name: "docker permission error",
errorMessage: "Got permission denied while trying to connect to the Docker daemon socket",
expectKnown: true,
expectedText: app.Tr.CannotAccessDockerSocketError,
},
{
name: "apple container not found",
errorMessage: "Apple Container CLI not found",
expectKnown: true,
expectedText: "Apple Container CLI not found. Please ensure the 'container' command is installed and available in your PATH.",
},
{
name: "failed to get containers",
errorMessage: "failed to get containers from runtime",
expectKnown: true,
expectedText: "Failed to retrieve containers. Please check if the container runtime is running.",
},
{
name: "unknown error",
errorMessage: "some unknown error message",
expectKnown: false,
expectedText: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Create a mock error with the test message
mockError := &mockError{message: tt.errorMessage}
text, known := app.KnownError(mockError)
assert.Equal(t, tt.expectKnown, known)
if tt.expectKnown {
assert.Equal(t, tt.expectedText, text)
} else {
assert.Empty(t, text)
}
})
}
}
// mockError is a simple error implementation for testing
type mockError struct {
message string
}
func (e *mockError) Error() string {
return e.message
}

View file

@ -33,7 +33,7 @@ func Generate() {
} }
func generateAtDir(dir string) { func generateAtDir(dir string) {
mConfig, err := config.NewAppConfig("lazydocker", "", "", "", "", true, nil, "") mConfig, err := config.NewAppConfig("lazydocker", "", "", "", "", true, nil, "", "docker")
if err != nil { if err != nil {
panic(err) panic(err)
} }

View file

@ -0,0 +1,838 @@
package commands
import (
"encoding/json"
"fmt"
"os/exec"
"strings"
"github.com/distribution/reference"
"github.com/jesseduffield/lazydocker/pkg/config"
"github.com/jesseduffield/lazydocker/pkg/i18n"
"github.com/sirupsen/logrus"
)
// AppleContainerCommand handles interactions with Apple's container CLI
type AppleContainerCommand struct {
Log *logrus.Entry
OSCommand *OSCommand
Tr *i18n.TranslationSet
Config *config.AppConfig
ErrorChan chan error
features map[Feature]bool
}
// internal logging helpers to tolerate nil Log in tests
func (c *AppleContainerCommand) logInfo(args ...interface{}) {
if c != nil && c.Log != nil {
c.Log.Info(args...)
}
}
func (c *AppleContainerCommand) logInfof(format string, args ...interface{}) {
if c != nil && c.Log != nil {
c.Log.Infof(format, args...)
}
}
func (c *AppleContainerCommand) logError(args ...interface{}) {
if c != nil && c.Log != nil {
c.Log.Error(args...)
}
}
func (c *AppleContainerCommand) logDebugf(format string, args ...interface{}) {
if c != nil && c.Log != nil {
c.Log.Debugf(format, args...)
}
}
// NewAppleContainerCommand creates a new Apple Container command handler
func NewAppleContainerCommand(log *logrus.Entry, osCommand *OSCommand, tr *i18n.TranslationSet, config *config.AppConfig, errorChan chan error) (*AppleContainerCommand, error) {
// Check if Apple Container CLI is available
if !isAppleContainerAvailable() {
return nil, fmt.Errorf("apple container CLI not found; ensure 'container' is on PATH")
}
cmd := &AppleContainerCommand{
Log: log,
OSCommand: osCommand,
Tr: tr,
Config: config,
ErrorChan: errorChan,
features: map[Feature]bool{},
}
cmd.detectCapabilities()
return cmd, nil
}
// isAppleContainerAvailable checks if the Apple Container CLI is available
func isAppleContainerAvailable() bool {
_, err := exec.LookPath("container")
return err == nil
}
// Supports reports if a capability is supported by the Apple container CLI
func (c *AppleContainerCommand) Supports(f Feature) bool {
if c == nil {
return false
}
return c.features[f]
}
// detectCapabilities runs lightweight CLI introspection to populate features.
// It errs on the conservative side (feature=false) on any unexpected error.
func (c *AppleContainerCommand) detectCapabilities() {
feats := map[Feature]bool{}
help := func(args ...string) string {
cmd := c.OSCommand.NewCmd("container", append(args, "--help")...)
out, err := c.OSCommand.RunExecutableWithOutput(cmd)
if err != nil {
return ""
}
return out
}
// Toplevel commands
root := help()
contains := func(haystack, needle string) bool { return strings.Contains(haystack, needle) }
feats[FeatureContainerExec] = contains(root, " exec ") || contains(root, "\nexec ")
feats[FeatureContainerAttach] = contains(root, " attach ") || contains(root, "\nattach ")
feats[FeatureContainerTop] = contains(root, " top ") || contains(root, "\ntop ")
feats[FeatureEventsStream] = contains(root, " events ") || contains(root, "\nevents ")
feats[FeatureStats] = contains(root, " stats ") || contains(root, "\nstats ")
// Images namespace
imagesHelp := help("images")
feats[FeatureImageHistory] = contains(imagesHelp, " history ") || contains(imagesHelp, "\nhistory ")
feats[FeatureImagePrune] = contains(imagesHelp, " prune ") || contains(imagesHelp, "\nprune ")
feats[FeatureImageRemove] = contains(imagesHelp, " rm ") || contains(imagesHelp, " remove ")
// Volume/Network prune
volumeHelp := help("volume")
feats[FeatureVolumePrune] = contains(volumeHelp, " prune ")
feats[FeatureVolumeCreate] = contains(volumeHelp, " create ") || contains(volumeHelp, "\ncreate ")
networkHelp := help("network")
feats[FeatureNetworkPrune] = contains(networkHelp, " prune ")
// Containers prune (if any)
containersHelp := help("container")
feats[FeatureContainerPrune] = contains(containersHelp, " prune ")
// Services/compose not supported by Apple CLI
feats[FeatureServices] = false
// Build/run platform flags
buildHelp := help("build")
feats[FeatureBuildPlatform] = contains(buildHelp, " --platform ") || contains(buildHelp, " --os ") || contains(buildHelp, " --arch ")
runHelp := help("run")
feats[FeatureRunPlatform] = contains(runHelp, " --platform ") || contains(runHelp, " --os ") || contains(runHelp, " --arch ")
// SSH agent forward for exec/run
execHelp := help("exec")
feats[FeatureSSHAgentForward] = contains(execHelp, " --ssh ") || contains(runHelp, " --ssh ")
c.features = feats
}
// GetContainers retrieves all containers from Apple Container
func (c *AppleContainerCommand) GetContainers() ([]*Container, error) {
c.logInfo("Getting containers from Apple Container")
// Execute: container ls --format json
cmd := c.OSCommand.NewCmd("container", "ls", "--format", "json")
output, err := c.OSCommand.RunExecutableWithOutput(cmd)
if err != nil {
c.logError("Failed to get containers from Apple Container: ", err)
return nil, fmt.Errorf("failed to get containers: %w", err)
}
c.logDebugf("Raw container ls output: %s", output)
// Parse the JSON output
containers, err := c.parseContainerList(output)
if err != nil {
c.logError("Failed to parse container list: ", err)
return nil, fmt.Errorf("failed to parse container list: %w", err)
}
c.logInfof("Found %d containers", len(containers))
return containers, nil
}
// parseContainerList parses the JSON output from Apple Container's ls command
func (c *AppleContainerCommand) parseContainerList(output string) ([]*Container, error) {
if strings.TrimSpace(output) == "" {
return []*Container{}, nil
}
// Apple Container outputs a JSON array of container objects
var containerArray []map[string]interface{}
if err := json.Unmarshal([]byte(output), &containerArray); err != nil {
c.logError("Failed to parse container JSON array: ", err)
return nil, fmt.Errorf("failed to parse container JSON: %w", err)
}
containers := make([]*Container, 0, len(containerArray))
for _, containerData := range containerArray {
container := c.jsonToContainer(containerData)
if container != nil {
containers = append(containers, container)
}
}
return containers, nil
}
// jsonToContainer converts JSON data to a Container struct
func (c *AppleContainerCommand) jsonToContainer(data map[string]interface{}) *Container {
// Extract container configuration
config, ok := data["configuration"].(map[string]interface{})
if !ok {
c.logError("Container missing configuration field")
return nil
}
// Extract basic container information
id, _ := config["id"].(string)
if id == "" {
c.logError("Container missing ID field")
return nil
}
// Extract status
status, _ := data["status"].(string)
// Extract address if available (schema-tolerant)
var addr string
if s, ok := data["addr"].(string); ok {
addr = s
}
if addr == "" {
if m, ok := data["network"].(map[string]interface{}); ok {
if s, ok := m["addr"].(string); ok {
addr = s
}
}
}
if addr == "" {
if s, ok := data["ip"].(string); ok {
addr = s
}
}
// Extract image information
var imageName string
if imageInfo, ok := config["image"].(map[string]interface{}); ok {
imageName, _ = imageInfo["reference"].(string)
}
// Create container with Apple Container specific fields
container := &Container{
ID: id,
Name: id, // Apple Container uses ID as the name
OSCommand: c.OSCommand,
Log: c.Log,
Tr: c.Tr,
// Note: Client is nil for Apple containers - we don't use Docker client
Client: nil,
// Set a reference to the Apple command for container operations
DockerCommand: c,
Addr: addr,
}
// Set up container.Container with basic state information
container.Container.State = status
container.Container.Status = status
// Map Apple Container states to Docker-like states for consistency
switch status {
case "stopped":
container.Container.State = "exited"
case "running":
container.Container.State = "running"
}
// Set image name
if imageName != "" {
container.Container.Image = imageName
}
c.logDebugf("Parsed container: ID=%s, Name=%s, Image=%s, Status=%s", id, id, imageName, status)
return container
}
// BuildImage builds a container image using Apple Container
func (c *AppleContainerCommand) BuildImage(tag, dockerfile string) error {
c.logInfof("Building image with tag %s using dockerfile %s", tag, dockerfile)
args := []string{"build", "--tag", tag, "--file", dockerfile, "."}
if c.Config != nil && c.Config.UserConfig != nil && c.Config.UserConfig.Apple != nil && c.Supports(FeatureBuildPlatform) {
if v := c.Config.UserConfig.Apple.BuildPlatform; v != "" {
args = append([]string{"build", "--tag", tag, "--file", dockerfile}, "--platform", v, ".")
} else {
if v := c.Config.UserConfig.Apple.BuildOS; v != "" {
args = append(args[:len(args)-1], "--os", v, ".")
}
if v := c.Config.UserConfig.Apple.BuildArch; v != "" {
args = append(args[:len(args)-1], "--arch", v, ".")
}
}
}
execCmd := c.OSCommand.NewCmd("container", args...)
return c.OSCommand.RunExecutable(execCmd)
}
// RunContainer runs a new container using Apple Container
func (c *AppleContainerCommand) RunContainer(name, image string, detached bool) error {
c.logInfof("Running container %s from image %s", name, image)
args := []string{"run", "--name", name}
if detached {
args = append(args, "--detach")
}
// platform flags
if c.Config != nil && c.Config.UserConfig != nil && c.Config.UserConfig.Apple != nil && c.Supports(FeatureRunPlatform) {
if v := c.Config.UserConfig.Apple.RunPlatform; v != "" {
args = append(args, "--platform", v)
} else {
if v := c.Config.UserConfig.Apple.RunOS; v != "" {
args = append(args, "--os", v)
}
if v := c.Config.UserConfig.Apple.RunArch; v != "" {
args = append(args, "--arch", v)
}
}
// ssh agent forward
if c.Config.UserConfig.Apple.ForwardSSHAgent && c.Supports(FeatureSSHAgentForward) {
args = append(args, "--ssh")
}
}
args = append(args, image)
execCmd := c.OSCommand.NewCmd("container", args...)
return c.OSCommand.RunExecutable(execCmd)
}
// StopContainer stops a running container
func (c *AppleContainerCommand) StopContainer(nameOrID string) error {
c.logInfof("Stopping container %s", nameOrID)
execCmd := c.OSCommand.NewCmd("container", "stop", nameOrID)
return c.OSCommand.RunExecutable(execCmd)
}
// StartContainer starts a stopped container
func (c *AppleContainerCommand) StartContainer(nameOrID string) error {
c.logInfof("Starting container %s", nameOrID)
execCmd := c.OSCommand.NewCmd("container", "start", nameOrID)
return c.OSCommand.RunExecutable(execCmd)
}
// RemoveContainer removes a container
func (c *AppleContainerCommand) RemoveContainer(nameOrID string, force bool) error {
c.logInfof("Removing container %s (force: %v)", nameOrID, force)
args := []string{"rm"}
if force {
args = append(args, "--force")
}
args = append(args, nameOrID)
execCmd := c.OSCommand.NewCmd("container", args...)
return c.OSCommand.RunExecutable(execCmd)
}
// ExecCommand executes a command in a running container
func (c *AppleContainerCommand) ExecCommand(nameOrID, command string) error {
c.logInfof("Executing command in container %s: %s", nameOrID, command)
// Keep string form to preserve exact command; quoting handled by ExecutableFromString
cmdStr := fmt.Sprintf("container exec %s %s", nameOrID, command)
execCmd := c.OSCommand.ExecutableFromString(cmdStr)
return c.OSCommand.RunExecutable(execCmd)
}
// GetImages retrieves all images from Apple Container
func (c *AppleContainerCommand) GetImages() ([]*Image, error) {
c.logInfo("Getting images from Apple Container")
// Execute: container images list --format json
cmd := c.OSCommand.NewCmd("container", "images", "list", "--format", "json")
output, err := c.OSCommand.RunExecutableWithOutput(cmd)
if err != nil {
c.logError("Failed to get images from Apple Container: ", err)
return nil, fmt.Errorf("failed to get images: %w", err)
}
// Parse the JSON output (implementation similar to containers)
images, err := c.parseImageList(output)
if err != nil {
c.logError("Failed to parse image list: ", err)
return nil, fmt.Errorf("failed to parse image list: %w", err)
}
c.logInfof("Found %d images", len(images))
return images, nil
}
// parseImageList parses the JSON output from Apple Container's images list command
func (c *AppleContainerCommand) parseImageList(output string) ([]*Image, error) {
if strings.TrimSpace(output) == "" {
return []*Image{}, nil
}
// Apple Container outputs a JSON array of image objects
var imageArray []map[string]interface{}
if err := json.Unmarshal([]byte(output), &imageArray); err != nil {
c.logError("Failed to parse image JSON array: ", err)
return nil, fmt.Errorf("failed to parse image JSON: %w", err)
}
images := make([]*Image, 0, len(imageArray))
for _, imageData := range imageArray {
image := c.jsonToImage(imageData)
if image != nil {
images = append(images, image)
}
}
return images, nil
}
// jsonToImage converts JSON data to an Image struct
func (c *AppleContainerCommand) jsonToImage(data map[string]interface{}) *Image {
// Extract reference (image name)
refStr, _ := data["reference"].(string)
if refStr == "" {
c.logError("Image missing reference field")
return nil
}
// Extract descriptor information
var digest string
var size float64
if descriptor, ok := data["descriptor"].(map[string]interface{}); ok {
digest, _ = descriptor["digest"].(string)
size, _ = descriptor["size"].(float64)
}
// Parse reference robustly (handles registries with ports and digests)
var repository, tag string
if named, err := reference.ParseNormalizedNamed(refStr); err == nil {
repository = reference.FamiliarName(named)
if t, ok := named.(reference.Tagged); ok {
tag = t.Tag()
} else {
tag = "latest"
}
if digest == "" {
if d, ok := named.(reference.Digested); ok {
digest = d.Digest().String()
}
}
} else {
// Fallback
parts := strings.Split(refStr, ":")
repository = parts[0]
tag = "latest"
if len(parts) > 1 {
tag = parts[1]
}
}
// Use digest as ID if available, otherwise use reference
id := digest
if id == "" {
id = refStr
}
image := &Image{
ID: id,
Name: repository,
Tag: tag,
OSCommand: c.OSCommand,
Log: c.Log,
}
// Store additional info in the Image.Image field
image.Image.RepoTags = []string{refStr}
if size > 0 {
image.Image.Size = int64(size)
}
c.logDebugf("Parsed image: ID=%s, Name=%s, Tag=%s, Reference=%s", id, repository, tag, refStr)
return image
}
// SystemStart starts the Apple Container system services
func (c *AppleContainerCommand) SystemStart() error {
c.logInfo("Starting Apple Container system services")
return c.OSCommand.RunExecutable(c.OSCommand.NewCmd("container", "system", "start"))
}
// SystemStop stops the Apple Container system services
func (c *AppleContainerCommand) SystemStop() error {
c.logInfo("Stopping Apple Container system services")
return c.OSCommand.RunExecutable(c.OSCommand.NewCmd("container", "system", "stop"))
}
// SystemStatus gets the status of Apple Container system services
func (c *AppleContainerCommand) SystemStatus() (map[string]interface{}, error) {
c.logInfo("Getting Apple Container system status")
cmd := c.OSCommand.NewCmd("container", "system", "status", "--format", "json")
output, err := c.OSCommand.RunExecutableWithOutput(cmd)
if err != nil {
return nil, fmt.Errorf("failed to get system status: %w", err)
}
var status map[string]interface{}
if err := json.Unmarshal([]byte(output), &status); err != nil {
return nil, fmt.Errorf("failed to parse system status: %w", err)
}
return status, nil
}
// NewCommandObject creates a new command object for Apple Container (implements LimitedDockerCommand)
func (c *AppleContainerCommand) NewCommandObject(obj CommandObject) CommandObject {
// For Apple Container, we don't need to modify the command object
// as we don't use docker-compose
return obj
}
// GetContainerLogs returns a command to get logs for a container
func (c *AppleContainerCommand) GetContainerLogs(containerID string, follow bool, tail string) *exec.Cmd {
args := []string{"logs"}
if follow {
args = append(args, "--follow")
}
if tail != "" && tail != "all" {
args = append(args, "-n", tail)
}
args = append(args, containerID)
return c.OSCommand.NewCmd("container", args...)
}
// GetNetworks retrieves all networks from Apple Container
func (c *AppleContainerCommand) GetNetworks() ([]*Network, error) {
c.logInfo("Getting networks from Apple Container")
// Execute: container network list --format json
cmd := c.OSCommand.NewCmd("container", "network", "list", "--format", "json")
output, err := c.OSCommand.RunExecutableWithOutput(cmd)
if err != nil {
c.logError("Failed to get networks from Apple Container: ", err)
return nil, fmt.Errorf("failed to get networks: %w", err)
}
// Parse the JSON output
networks, err := c.parseNetworkList(output)
if err != nil {
c.logError("Failed to parse network list: ", err)
return nil, fmt.Errorf("failed to parse network list: %w", err)
}
c.logInfof("Found %d networks", len(networks))
return networks, nil
}
// parseNetworkList parses the JSON output from Apple Container's network list command
func (c *AppleContainerCommand) parseNetworkList(output string) ([]*Network, error) {
if strings.TrimSpace(output) == "" {
return []*Network{}, nil
}
// Apple Container outputs a JSON array of network objects
var networkArray []map[string]interface{}
if err := json.Unmarshal([]byte(output), &networkArray); err != nil {
c.logError("Failed to parse network JSON array: ", err)
return nil, fmt.Errorf("failed to parse network JSON: %w", err)
}
networks := make([]*Network, 0, len(networkArray))
for _, networkData := range networkArray {
network := c.jsonToNetwork(networkData)
if network != nil {
networks = append(networks, network)
}
}
return networks, nil
}
// jsonToNetwork converts JSON data to a Network struct
func (c *AppleContainerCommand) jsonToNetwork(data map[string]interface{}) *Network {
// Extract network ID
id, _ := data["id"].(string)
if id == "" {
c.logError("Network missing ID field")
return nil
}
// Extract state
state, _ := data["state"].(string)
// Extract network config
var driver string
if config, ok := data["config"].(map[string]interface{}); ok {
mode, _ := config["mode"].(string)
driver = mode // Use mode as driver for display
}
// Create network struct
network := &Network{
Name: id,
OSCommand: c.OSCommand,
Log: c.Log,
}
// Set network details
network.Network.ID = id
network.Network.Name = id
network.Network.Driver = driver
c.logDebugf("Parsed network: ID=%s, Name=%s, Driver=%s, State=%s", id, id, driver, state)
return network
}
// GetContainerMounts retrieves mount information for a container
func (c *AppleContainerCommand) GetContainerMounts(containerID string) (string, error) {
// Execute container inspect to get full details
cmd := c.OSCommand.NewCmd("container", "inspect", containerID)
output, err := c.OSCommand.RunExecutableWithOutput(cmd)
if err != nil {
return "", fmt.Errorf("failed to inspect container: %w", err)
}
// Parse the JSON output
var containers []map[string]interface{}
if err := json.Unmarshal([]byte(output), &containers); err != nil {
return "", fmt.Errorf("failed to parse container inspect JSON: %w", err)
}
if len(containers) == 0 {
return "No container found", nil
}
containerData := containers[0]
config, ok := containerData["configuration"].(map[string]interface{})
if !ok {
return "No configuration found", nil
}
mounts, ok := config["mounts"].([]interface{})
if !ok || len(mounts) == 0 {
return "No mounts configured for this container", nil
}
// Format mount information
var result strings.Builder
result.WriteString("Volume Mounts:\n")
result.WriteString("==============\n\n")
for i, mount := range mounts {
mountMap, ok := mount.(map[string]interface{})
if !ok {
continue
}
source, _ := mountMap["source"].(string)
destination, _ := mountMap["destination"].(string)
options, _ := mountMap["options"].([]interface{})
// Get mount type
mountType := "unknown"
if typeInfo, ok := mountMap["type"].(map[string]interface{}); ok {
if _, ok := typeInfo["virtiofs"]; ok {
mountType = "virtiofs"
} else if _, ok := typeInfo["tmpfs"]; ok {
mountType = "tmpfs"
}
}
result.WriteString(fmt.Sprintf("Mount %d:\n", i+1))
result.WriteString(fmt.Sprintf(" Type: %s\n", mountType))
result.WriteString(fmt.Sprintf(" Source: %s\n", source))
result.WriteString(fmt.Sprintf(" Destination: %s\n", destination))
if len(options) > 0 {
optStrings := make([]string, len(options))
for j, opt := range options {
optStrings[j] = fmt.Sprintf("%v", opt)
}
result.WriteString(fmt.Sprintf(" Options: %s\n", strings.Join(optStrings, ", ")))
}
result.WriteString("\n")
}
return result.String(), nil
}
// RefreshVolumes gets the volumes and stores them for Apple Container
func (c *AppleContainerCommand) RefreshVolumes() ([]*Volume, error) {
c.logInfo("Getting volumes from Apple Container")
// Execute: container volume list --format json
cmd := c.OSCommand.NewCmd("container", "volume", "list", "--format", "json")
output, err := c.OSCommand.RunExecutableWithOutput(cmd)
if err != nil {
c.logError("Failed to get volumes from Apple Container: ", err)
return nil, fmt.Errorf("failed to get volumes: %w", err)
}
c.logDebugf("Raw volume ls output: %s", output)
// Parse the JSON output
volumes, err := c.parseVolumeList(output)
if err != nil {
c.logError("Failed to parse volume list: ", err)
return nil, fmt.Errorf("failed to parse volume list: %w", err)
}
c.logInfof("Found %d volumes", len(volumes))
return volumes, nil
}
// parseVolumeList parses the JSON output from Apple Container's volume list command
func (c *AppleContainerCommand) parseVolumeList(output string) ([]*Volume, error) {
if strings.TrimSpace(output) == "" {
return []*Volume{}, nil
}
// Apple Container outputs a JSON array of volume objects
var volumeArray []map[string]interface{}
if err := json.Unmarshal([]byte(output), &volumeArray); err != nil {
c.logError("Failed to parse volume JSON array: ", err)
return nil, fmt.Errorf("failed to parse volume JSON: %w", err)
}
volumes := make([]*Volume, 0, len(volumeArray))
for _, volumeData := range volumeArray {
volume := c.jsonToVolume(volumeData)
if volume != nil {
volumes = append(volumes, volume)
}
}
return volumes, nil
}
// jsonToVolume converts JSON data to a Volume struct
func (c *AppleContainerCommand) jsonToVolume(data map[string]interface{}) *Volume {
// Extract volume name/ID
name, _ := data["name"].(string)
if name == "" {
// Try alternative field names
name, _ = data["id"].(string)
if name == "" {
c.logError("Volume missing name/id field")
return nil
}
}
// Extract driver (if available)
driver, _ := data["driver"].(string)
if driver == "" {
driver = "local" // Default driver
}
// Extract mountpoint (if available)
mountpoint, _ := data["mountpoint"].(string)
// Create volume with Apple Container specific fields
volume := &Volume{
Name: name,
OSCommand: c.OSCommand,
Log: c.Log,
// Note: Client is nil for Apple containers - we don't use Docker client
Client: nil,
// Set a reference to the Apple command for volume operations
DockerCommand: c,
}
// Set up volume.Volume with basic information
volume.Volume.Driver = driver
volume.Volume.Mountpoint = mountpoint
volume.Volume.Name = name
volume.Volume.Scope = "local" // Apple Container volumes are typically local
// Extract labels if available
if labelsData, ok := data["labels"].(map[string]interface{}); ok {
labels := make(map[string]string)
for k, v := range labelsData {
if vStr, ok := v.(string); ok {
labels[k] = vStr
}
}
volume.Volume.Labels = labels
}
// Extract options if available
if optionsData, ok := data["options"].(map[string]interface{}); ok {
options := make(map[string]string)
for k, v := range optionsData {
if vStr, ok := v.(string); ok {
options[k] = vStr
}
}
volume.Volume.Options = options
}
c.logDebugf("Parsed volume: Name=%s, Driver=%s, Mountpoint=%s", name, driver, mountpoint)
return volume
}
// PruneVolumes prunes volumes for Apple Container
func (c *AppleContainerCommand) PruneVolumes() error {
c.logInfo("Pruning volumes from Apple Container")
// Execute: container volume prune --force
err := c.OSCommand.RunExecutable(c.OSCommand.NewCmd("container", "volume", "prune", "--force"))
if err != nil {
c.logError("Failed to prune volumes from Apple Container: ", err)
return fmt.Errorf("failed to prune volumes: %w", err)
}
c.logInfo("Successfully pruned volumes")
return nil
}
// RemoveVolume removes a volume for Apple Container
func (c *AppleContainerCommand) RemoveVolume(name string, force bool) error {
c.logInfof("Removing volume %s (force: %v)", name, force)
args := []string{"volume", "rm"}
if force {
args = append(args, "--force")
}
args = append(args, name)
return c.OSCommand.RunExecutable(c.OSCommand.NewCmd("container", args...))
}
// CreateVolume creates a named volume with optional key=value options
func (c *AppleContainerCommand) CreateVolume(name string, opts map[string]string) error {
if name == "" {
return fmt.Errorf("volume name required")
}
if !c.Supports(FeatureVolumeCreate) {
return fmt.Errorf("volume create not supported by this runtime")
}
args := []string{"volume", "create", "--name", name}
for k, v := range opts {
if k == "" {
continue
}
if v != "" {
args = append(args, "--opt", fmt.Sprintf("%s=%s", k, v))
} else {
args = append(args, "--opt", k)
}
}
return c.OSCommand.RunExecutable(c.OSCommand.NewCmd("container", args...))
}

View file

@ -0,0 +1,334 @@
package commands
import (
"testing"
"github.com/jesseduffield/lazydocker/pkg/config"
"github.com/jesseduffield/lazydocker/pkg/i18n"
"github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert"
)
func TestAppleContainerCommandCreation(t *testing.T) {
log := logrus.NewEntry(logrus.New())
// Create a basic config
appConfig := &config.AppConfig{
Runtime: "apple",
}
// Create a basic OS command (real, but used only for help/introspection)
osCommand := NewOSCommand(log, appConfig)
// Create translation set
tr := &i18n.TranslationSet{}
errorChan := make(chan error, 1)
_, err := NewAppleContainerCommand(log, osCommand, tr, appConfig, errorChan)
if isAppleContainerAvailable() {
// On machines where Apple CLI is installed, expect success
assert.Nil(t, err)
} else {
assert.NotNil(t, err)
assert.Contains(t, err.Error(), "Apple Container CLI not found")
}
}
func TestIsAppleContainerAvailable(t *testing.T) {
// Simply call the function; do not assert a fixed value because
// the test host may or may not have the CLI installed.
_ = isAppleContainerAvailable()
}
func TestParseContainerList(t *testing.T) {
log := logrus.NewEntry(logrus.New())
appConfig := &config.AppConfig{Runtime: "apple"}
osCommand := &OSCommand{}
tr := &i18n.TranslationSet{}
errorChan := make(chan error, 1)
// Create command instance for testing parsing methods
cmd := &AppleContainerCommand{
Log: log,
OSCommand: osCommand,
Tr: tr,
Config: appConfig,
ErrorChan: errorChan,
}
tests := []struct {
name string
input string
expected int
hasError bool
}{
{
name: "empty output",
input: "",
expected: 0,
hasError: false,
},
{
name: "single container",
input: `[{"configuration":{"id":"abc123","image":{"reference":"nginx:latest"}},"status":"running"}]`,
expected: 1,
hasError: false,
},
{
name: "multiple containers",
input: `[{"configuration":{"id":"abc123","image":{"reference":"nginx:latest"}},"status":"running"},
{"configuration":{"id":"def456","image":{"reference":"redis:6"}},"status":"stopped"}]`,
expected: 2,
hasError: false,
},
{
name: "invalid json",
input: `{"invalid json}`,
expected: 0,
hasError: true,
},
{
name: "missing required fields",
input: `[{"status":"running"}]`,
expected: 0,
hasError: false, // parse succeeds but no valid entries
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
containers, err := cmd.parseContainerList(tt.input)
if tt.hasError {
assert.NotNil(t, err)
} else {
assert.Nil(t, err)
assert.Equal(t, tt.expected, len(containers))
}
})
}
}
func TestParseImageList(t *testing.T) {
log := logrus.NewEntry(logrus.New())
appConfig := &config.AppConfig{Runtime: "apple"}
osCommand := &OSCommand{}
tr := &i18n.TranslationSet{}
errorChan := make(chan error, 1)
cmd := &AppleContainerCommand{
Log: log,
OSCommand: osCommand,
Tr: tr,
Config: appConfig,
ErrorChan: errorChan,
}
tests := []struct {
name string
input string
expected int
hasError bool
}{
{
name: "empty output",
input: "",
expected: 0,
hasError: false,
},
{
name: "single image",
input: `[{"reference":"nginx:latest","descriptor":{"digest":"sha256:abc","size":1234}}]`,
expected: 1,
hasError: false,
},
{
name: "multiple images",
input: `[{"reference":"nginx:latest","descriptor":{"digest":"sha256:abc","size":1234}},
{"reference":"redis:6-alpine","descriptor":{"digest":"sha256:def","size":5678}}]`,
expected: 2,
hasError: false,
},
{
name: "missing required fields",
input: `[{"descriptor":{"digest":"sha256:abc"}}]`,
expected: 0,
hasError: false, // Should skip images with missing ID
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
images, err := cmd.parseImageList(tt.input)
if tt.hasError {
assert.NotNil(t, err)
} else {
assert.Nil(t, err)
assert.Equal(t, tt.expected, len(images))
}
})
}
}
func TestJsonToContainer(t *testing.T) {
log := logrus.NewEntry(logrus.New())
appConfig := &config.AppConfig{Runtime: "apple"}
osCommand := &OSCommand{}
tr := &i18n.TranslationSet{}
errorChan := make(chan error, 1)
cmd := &AppleContainerCommand{
Log: log,
OSCommand: osCommand,
Tr: tr,
Config: appConfig,
ErrorChan: errorChan,
}
tests := []struct {
name string
input map[string]interface{}
expected *Container
}{
{
name: "valid container data",
input: map[string]interface{}{
"configuration": map[string]interface{}{
"id": "abc123",
"image": map[string]interface{}{
"reference": "nginx:latest",
},
},
"status": "running",
},
expected: &Container{
ID: "abc123",
Name: "abc123",
},
},
{
name: "missing id",
input: map[string]interface{}{
"configuration": map[string]interface{}{
"image": map[string]interface{}{"reference": "nginx:latest"},
},
"status": "running",
},
expected: nil,
},
// missing name is fine; name defaults to ID in implementation
{
name: "missing name",
input: map[string]interface{}{
"configuration": map[string]interface{}{
"id": "abc123",
"image": map[string]interface{}{"reference": "nginx:latest"},
},
"status": "running",
},
expected: &Container{ID: "abc123", Name: "abc123"},
},
{
name: "state mapping",
input: map[string]interface{}{
"configuration": map[string]interface{}{
"id": "abc123",
"image": map[string]interface{}{"reference": "nginx:latest"},
},
"status": "stopped", // Should map to exited
},
expected: &Container{
ID: "abc123",
Name: "abc123",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
container := cmd.jsonToContainer(tt.input)
if tt.expected == nil {
assert.Nil(t, container)
} else {
assert.NotNil(t, container)
assert.Equal(t, tt.expected.ID, container.ID)
assert.Equal(t, tt.expected.Name, container.Name)
}
})
}
}
func TestJsonToImage(t *testing.T) {
log := logrus.NewEntry(logrus.New())
appConfig := &config.AppConfig{Runtime: "apple"}
osCommand := &OSCommand{}
tr := &i18n.TranslationSet{}
errorChan := make(chan error, 1)
cmd := &AppleContainerCommand{
Log: log,
OSCommand: osCommand,
Tr: tr,
Config: appConfig,
ErrorChan: errorChan,
}
tests := []struct {
name string
input map[string]interface{}
expected *Image
}{
{
name: "valid image data",
input: map[string]interface{}{
"reference": "nginx:latest",
"descriptor": map[string]interface{}{
"digest": "sha256:abc",
"size": 1234,
},
},
expected: &Image{
ID: "sha256:abc",
Name: "nginx",
Tag: "latest",
},
},
// Missing digest is fine; implementation uses reference as ID
{
name: "missing id",
input: map[string]interface{}{
"reference": "nginx:latest",
},
expected: &Image{ID: "nginx:latest", Name: "nginx", Tag: "latest"},
},
{
name: "partial data",
input: map[string]interface{}{
"reference": "nginx",
},
expected: &Image{
ID: "nginx",
Name: "nginx",
Tag: "latest",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
image := cmd.jsonToImage(tt.input)
if tt.expected == nil {
assert.Nil(t, image)
} else {
assert.NotNil(t, image)
assert.Equal(t, tt.expected.ID, image.ID)
assert.Equal(t, tt.expected.Name, image.Name)
assert.Equal(t, tt.expected.Tag, image.Tag)
}
})
}
}

View file

@ -40,11 +40,23 @@ type Container struct {
Tr *i18n.TranslationSet Tr *i18n.TranslationSet
StatsMutex deadlock.Mutex StatsMutex deadlock.Mutex
// Addr is a runtime-specific address string (e.g., IP or hostname) used by
// alternative runtimes like Apple's container CLI. It may be empty.
Addr string
} }
// Remove removes the container // Remove removes the container
func (c *Container) Remove(options container.RemoveOptions) error { func (c *Container) Remove(options container.RemoveOptions) error {
c.Log.Warn(fmt.Sprintf("removing container %s", c.Name)) c.Log.Warn(fmt.Sprintf("removing container %s", c.Name))
// For Apple containers, use the AppleContainerCommand
if c.Client == nil && c.DockerCommand != nil {
if appleCmd, ok := c.DockerCommand.(*AppleContainerCommand); ok {
return appleCmd.RemoveContainer(c.ID, options.Force)
}
}
if err := c.Client.ContainerRemove(context.Background(), c.ID, options); err != nil { if err := c.Client.ContainerRemove(context.Background(), c.ID, options); err != nil {
if strings.Contains(err.Error(), "Stop the container before attempting removal or force remove") { if strings.Contains(err.Error(), "Stop the container before attempting removal or force remove") {
return ComplexError{ return ComplexError{
@ -62,6 +74,14 @@ func (c *Container) Remove(options container.RemoveOptions) error {
// Stop stops the container // Stop stops the container
func (c *Container) Stop() error { func (c *Container) Stop() error {
c.Log.Warn(fmt.Sprintf("stopping container %s", c.Name)) c.Log.Warn(fmt.Sprintf("stopping container %s", c.Name))
// For Apple containers, use the AppleContainerCommand
if c.Client == nil && c.DockerCommand != nil {
if appleCmd, ok := c.DockerCommand.(*AppleContainerCommand); ok {
return appleCmd.StopContainer(c.ID)
}
}
return c.Client.ContainerStop(context.Background(), c.ID, container.StopOptions{}) return c.Client.ContainerStop(context.Background(), c.ID, container.StopOptions{})
} }
@ -80,6 +100,17 @@ func (c *Container) Unpause() error {
// Restart restarts the container // Restart restarts the container
func (c *Container) Restart() error { func (c *Container) Restart() error {
c.Log.Warn(fmt.Sprintf("restarting container %s", c.Name)) c.Log.Warn(fmt.Sprintf("restarting container %s", c.Name))
// For Apple containers, stop and start
if c.Client == nil && c.DockerCommand != nil {
if appleCmd, ok := c.DockerCommand.(*AppleContainerCommand); ok {
if err := appleCmd.StopContainer(c.ID); err != nil {
return err
}
return appleCmd.StartContainer(c.ID)
}
}
return c.Client.ContainerRestart(context.Background(), c.ID, container.StopOptions{}) return c.Client.ContainerRestart(context.Background(), c.ID, container.StopOptions{})
} }
@ -98,6 +129,11 @@ func (c *Container) Attach() (*exec.Cmd, error) {
return nil, errors.New(c.Tr.CannotAttachStoppedContainerError) return nil, errors.New(c.Tr.CannotAttachStoppedContainerError)
} }
// Apple Container runtime does not support docker attach
if c.Client == nil {
return nil, errors.New("attach not available for Apple Container runtime")
}
c.Log.Warn(fmt.Sprintf("attaching to container %s", c.Name)) c.Log.Warn(fmt.Sprintf("attaching to container %s", c.Name))
// TODO: use SDK // TODO: use SDK
cmd := c.OSCommand.NewCmd("docker", "attach", "--sig-proxy=false", c.ID) cmd := c.OSCommand.NewCmd("docker", "attach", "--sig-proxy=false", c.ID)
@ -106,6 +142,16 @@ func (c *Container) Attach() (*exec.Cmd, error) {
// Top returns process information // Top returns process information
func (c *Container) Top(ctx context.Context) (container.ContainerTopOKBody, error) { func (c *Container) Top(ctx context.Context) (container.ContainerTopOKBody, error) {
// For Apple containers, this feature is not available
if c.Client == nil {
return container.ContainerTopOKBody{
Titles: []string{"PID", "USER", "TIME", "COMMAND"},
Processes: [][]string{
{"N/A", "N/A", "N/A", "Process information not available for Apple Container runtime"},
},
}, nil
}
detail, err := c.Inspect() detail, err := c.Inspect()
if err != nil { if err != nil {
return container.ContainerTopOKBody{}, err return container.ContainerTopOKBody{}, err
@ -127,6 +173,23 @@ func (c *DockerCommand) PruneContainers() error {
// Inspect returns details about the container // Inspect returns details about the container
func (c *Container) Inspect() (dockerTypes.ContainerJSON, error) { func (c *Container) Inspect() (dockerTypes.ContainerJSON, error) {
// For Apple containers, we don't have a Docker client
if c.Client == nil {
// Return a minimal ContainerJSON with just the state information we have
return dockerTypes.ContainerJSON{
ContainerJSONBase: &dockerTypes.ContainerJSONBase{
ID: c.ID,
Name: c.Name,
State: &dockerTypes.ContainerState{
Status: c.Container.State,
Running: c.Container.State == "running",
},
},
Config: &container.Config{
Image: c.Container.Image,
},
}, nil
}
return c.Client.ContainerInspect(context.Background(), c.ID) return c.Client.ContainerInspect(context.Background(), c.ID)
} }

View file

@ -0,0 +1,432 @@
package commands
import (
"fmt"
"io"
"os/exec"
)
// ContainerRuntime defines the interface that all container runtimes must implement
// This allows lazydocker to work with different container systems (Docker, Apple Container, etc.)
type ContainerRuntime interface {
// Container operations
GetContainers() ([]*Container, error)
RefreshContainersAndServices(currentServices []*Service, currentContainers []*Container) ([]*Container, []*Service, error)
RefreshContainerDetails(containers []*Container) error
PruneContainers() error
// Image operations
RefreshImages() ([]*Image, error)
PruneImages() error
// Volume operations
RefreshVolumes() ([]*Volume, error)
PruneVolumes() error
// Network operations
RefreshNetworks() ([]*Network, error)
PruneNetworks() error
// Volume create (optional capability)
CreateVolume(name string, opts map[string]string) error
// Service operations (for docker-compose/equivalent)
GetServices() ([]*Service, error)
InDockerComposeProject() bool
// System operations
ViewAllLogs() (cmd Cmd, err error)
DockerComposeConfig() string
SystemStatus() (map[string]interface{}, error)
// Runtime information
GetRuntimeName() string
GetRuntimeVersion() string
// Close resources
io.Closer
}
// Cmd represents a command that can be executed
type Cmd interface {
Start() error
Wait() error
Kill() error
}
// ContainerRuntimeAdapter provides a unified interface for accessing container operations
// It wraps either a DockerCommand or AppleContainerCommand and exposes them through
// the ContainerRuntime interface
type ContainerRuntimeAdapter struct {
dockerCommand *DockerCommand
appleContainerCommand *AppleContainerCommand
runtimeType string
}
// operationNotSupported returns an error indicating the operation is not supported by the current runtime
func (c *ContainerRuntimeAdapter) operationNotSupported(operation string) error {
return fmt.Errorf("%s not supported by %s runtime", operation, c.runtimeType)
}
// commandNotAvailable returns an error indicating the runtime command is not available
func (c *ContainerRuntimeAdapter) commandNotAvailable() error {
return fmt.Errorf("%s command not available", c.runtimeType)
}
// unsupportedRuntime returns an error indicating the runtime type is not supported
func (c *ContainerRuntimeAdapter) unsupportedRuntime() error {
return fmt.Errorf("unsupported runtime '%s'", c.runtimeType)
}
// NewContainerRuntimeAdapter creates a new adapter based on the provided commands
func NewContainerRuntimeAdapter(docker *DockerCommand, apple *AppleContainerCommand, runtimeType string) *ContainerRuntimeAdapter {
return &ContainerRuntimeAdapter{
dockerCommand: docker,
appleContainerCommand: apple,
runtimeType: runtimeType,
}
}
// Supports returns whether a given feature is supported by the active runtime
func (c *ContainerRuntimeAdapter) Supports(f Feature) bool {
switch c.runtimeType {
case "docker":
// Docker runtime supports all features used by LazyDocker
return true
case "apple":
if c.appleContainerCommand == nil {
return false
}
return c.appleContainerCommand.Supports(f)
default:
return false
}
}
// GetContainers returns containers from the active runtime
func (c *ContainerRuntimeAdapter) GetContainers() ([]*Container, error) {
switch c.runtimeType {
case "docker":
if c.dockerCommand == nil {
return nil, c.commandNotAvailable()
}
return c.dockerCommand.GetContainers(nil)
case "apple":
if c.appleContainerCommand == nil {
return nil, c.commandNotAvailable()
}
return c.appleContainerCommand.GetContainers()
default:
return nil, c.unsupportedRuntime()
}
}
// RefreshContainersAndServices refreshes both containers and services
func (c *ContainerRuntimeAdapter) RefreshContainersAndServices(currentServices []*Service, currentContainers []*Container) ([]*Container, []*Service, error) {
switch c.runtimeType {
case "docker":
if c.dockerCommand == nil {
return nil, nil, c.commandNotAvailable()
}
return c.dockerCommand.RefreshContainersAndServices(currentServices, currentContainers)
case "apple":
if c.appleContainerCommand == nil {
return nil, nil, c.commandNotAvailable()
}
// Apple Container doesn't have services concept, so we just return containers
containers, err := c.appleContainerCommand.GetContainers()
return containers, []*Service{}, err
default:
return nil, nil, c.unsupportedRuntime()
}
}
// RefreshContainerDetails updates container details
func (c *ContainerRuntimeAdapter) RefreshContainerDetails(containers []*Container) error {
switch c.runtimeType {
case "docker":
if c.dockerCommand == nil {
return c.commandNotAvailable()
}
return c.dockerCommand.RefreshContainerDetails(containers)
case "apple":
// Apple Container doesn't need explicit refresh - details are fetched with containers
return nil
default:
return c.unsupportedRuntime()
}
}
// PruneContainers removes unused containers
func (c *ContainerRuntimeAdapter) PruneContainers() error {
switch c.runtimeType {
case "docker":
if c.dockerCommand == nil {
return c.commandNotAvailable()
}
return c.dockerCommand.PruneContainers()
case "apple":
// Apple Container might not have prune functionality
return c.operationNotSupported("container pruning")
default:
return c.unsupportedRuntime()
}
}
// RefreshImages returns images from the active runtime
func (c *ContainerRuntimeAdapter) RefreshImages() ([]*Image, error) {
switch c.runtimeType {
case "docker":
if c.dockerCommand == nil {
return nil, c.commandNotAvailable()
}
return c.dockerCommand.RefreshImages()
case "apple":
if c.appleContainerCommand == nil {
return nil, c.commandNotAvailable()
}
return c.appleContainerCommand.GetImages()
default:
return nil, c.unsupportedRuntime()
}
}
// PruneImages removes unused images
func (c *ContainerRuntimeAdapter) PruneImages() error {
switch c.runtimeType {
case "docker":
if c.dockerCommand == nil {
return c.commandNotAvailable()
}
return c.dockerCommand.PruneImages()
case "apple":
return c.operationNotSupported("image pruning")
default:
return c.unsupportedRuntime()
}
}
// RefreshVolumes returns volumes from the active runtime
func (c *ContainerRuntimeAdapter) RefreshVolumes() ([]*Volume, error) {
switch c.runtimeType {
case "docker":
if c.dockerCommand == nil {
return nil, c.commandNotAvailable()
}
return c.dockerCommand.RefreshVolumes()
case "apple":
if c.appleContainerCommand == nil {
return nil, c.commandNotAvailable()
}
if c.appleContainerCommand.OSCommand == nil { // avoid invoking external CLI in tests
return []*Volume{}, nil
}
return c.appleContainerCommand.RefreshVolumes()
default:
return nil, c.unsupportedRuntime()
}
}
// PruneVolumes removes unused volumes
func (c *ContainerRuntimeAdapter) PruneVolumes() error {
switch c.runtimeType {
case "docker":
if c.dockerCommand == nil {
return c.commandNotAvailable()
}
return c.dockerCommand.PruneVolumes()
case "apple":
return c.operationNotSupported("volume pruning")
default:
return c.unsupportedRuntime()
}
}
// RefreshNetworks returns networks from the active runtime
func (c *ContainerRuntimeAdapter) RefreshNetworks() ([]*Network, error) {
switch c.runtimeType {
case "docker":
if c.dockerCommand == nil {
return nil, c.commandNotAvailable()
}
return c.dockerCommand.RefreshNetworks()
case "apple":
if c.appleContainerCommand == nil {
return nil, c.commandNotAvailable()
}
if c.appleContainerCommand.OSCommand == nil { // avoid invoking external CLI in tests
return []*Network{}, nil
}
return c.appleContainerCommand.GetNetworks()
default:
return nil, c.unsupportedRuntime()
}
}
// PruneNetworks removes unused networks
func (c *ContainerRuntimeAdapter) PruneNetworks() error {
switch c.runtimeType {
case "docker":
if c.dockerCommand == nil {
return c.commandNotAvailable()
}
return c.dockerCommand.PruneNetworks()
case "apple":
return c.operationNotSupported("network pruning")
default:
return c.unsupportedRuntime()
}
}
// GetServices returns services from the active runtime
func (c *ContainerRuntimeAdapter) GetServices() ([]*Service, error) {
switch c.runtimeType {
case "docker":
if c.dockerCommand == nil {
return nil, c.commandNotAvailable()
}
return c.dockerCommand.GetServices()
case "apple":
// Apple Container doesn't have services concept
return []*Service{}, nil
default:
return nil, c.unsupportedRuntime()
}
}
// InDockerComposeProject returns whether we're in a docker-compose project
func (c *ContainerRuntimeAdapter) InDockerComposeProject() bool {
switch c.runtimeType {
case "docker":
if c.dockerCommand == nil {
return false
}
return c.dockerCommand.InDockerComposeProject
case "apple":
// Apple Container doesn't have compose concept
return false
default:
return false
}
}
// ViewAllLogs returns a command to view all logs
func (c *ContainerRuntimeAdapter) ViewAllLogs() (cmd Cmd, err error) {
switch c.runtimeType {
case "docker":
if c.dockerCommand == nil {
return nil, c.commandNotAvailable()
}
execCmd, err := c.dockerCommand.ViewAllLogs()
if err != nil {
return nil, err
}
return &cmdWrapper{cmd: execCmd}, nil
case "apple":
return nil, c.operationNotSupported("viewing all logs")
default:
return nil, c.unsupportedRuntime()
}
}
// DockerComposeConfig returns the docker-compose config
func (c *ContainerRuntimeAdapter) DockerComposeConfig() string {
switch c.runtimeType {
case "docker":
if c.dockerCommand == nil {
return ""
}
return c.dockerCommand.DockerComposeConfig()
case "apple":
return ""
default:
return ""
}
}
// CreateVolume creates a volume (runtime-dependent)
func (c *ContainerRuntimeAdapter) CreateVolume(name string, opts map[string]string) error {
switch c.runtimeType {
case "docker":
return c.operationNotSupported("volume create")
case "apple":
if c.appleContainerCommand == nil {
return c.commandNotAvailable()
}
if !c.appleContainerCommand.Supports(FeatureVolumeCreate) {
return c.operationNotSupported("volume create")
}
return c.appleContainerCommand.CreateVolume(name, opts)
default:
return c.unsupportedRuntime()
}
}
// SystemStatus returns a runtime-specific system status map
func (c *ContainerRuntimeAdapter) SystemStatus() (map[string]interface{}, error) {
switch c.runtimeType {
case "docker":
return map[string]interface{}{"runtime": "docker"}, nil
case "apple":
if c.appleContainerCommand == nil {
return nil, c.commandNotAvailable()
}
if c.appleContainerCommand.OSCommand == nil {
return map[string]interface{}{"runtime": "apple"}, nil
}
return c.appleContainerCommand.SystemStatus()
default:
return nil, c.unsupportedRuntime()
}
}
// GetRuntimeName returns the name of the runtime
func (c *ContainerRuntimeAdapter) GetRuntimeName() string {
return c.runtimeType
}
// GetRuntimeVersion returns the version of the runtime
func (c *ContainerRuntimeAdapter) GetRuntimeVersion() string {
switch c.runtimeType {
case "docker":
return "Docker Runtime"
case "apple":
return "Apple Container Runtime"
default:
return "Unknown Runtime"
}
}
// Close closes the underlying runtime resources
func (c *ContainerRuntimeAdapter) Close() error {
switch c.runtimeType {
case "docker":
if c.dockerCommand != nil {
return c.dockerCommand.Close()
}
case "apple":
// Apple Container command doesn't implement io.Closer currently
// This is fine since it doesn't hold persistent connections
}
return nil
}
// cmdWrapper wraps an exec.Cmd to implement our Cmd interface
type cmdWrapper struct {
cmd *exec.Cmd
}
func (c *cmdWrapper) Start() error {
return c.cmd.Start()
}
func (c *cmdWrapper) Wait() error {
return c.cmd.Wait()
}
func (c *cmdWrapper) Kill() error {
if c.cmd.Process != nil {
return c.cmd.Process.Kill()
}
return nil
}

View file

@ -0,0 +1,194 @@
package commands
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestContainerRuntimeAdapter(t *testing.T) {
tests := []struct {
name string
runtimeType string
hasDocker bool
hasApple bool
}{
{
name: "docker runtime",
runtimeType: "docker",
hasDocker: true,
hasApple: false,
},
{
name: "apple runtime",
runtimeType: "apple",
hasDocker: false,
hasApple: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var dockerCmd *DockerCommand
var appleCmd *AppleContainerCommand
if tt.hasDocker {
// Create a mock docker command (we can't easily create a real one in tests)
dockerCmd = &DockerCommand{}
}
if tt.hasApple {
// Create a mock apple container command
appleCmd = &AppleContainerCommand{}
}
adapter := NewContainerRuntimeAdapter(dockerCmd, appleCmd, tt.runtimeType)
// Test basic properties
assert.Equal(t, tt.runtimeType, adapter.GetRuntimeName())
assert.NotEmpty(t, adapter.GetRuntimeVersion())
// Test that the adapter correctly identifies its runtime type
switch tt.runtimeType {
case "docker":
assert.Equal(t, dockerCmd, adapter.dockerCommand)
assert.Nil(t, adapter.appleContainerCommand)
case "apple":
assert.Nil(t, adapter.dockerCommand)
assert.Equal(t, appleCmd, adapter.appleContainerCommand)
}
})
}
}
func TestContainerRuntimeAdapterErrorHandling(t *testing.T) {
tests := []struct {
name string
runtimeType string
dockerCmd *DockerCommand
appleCmd *AppleContainerCommand
expectError bool
errorMessage string
}{
{
name: "docker runtime with nil command",
runtimeType: "docker",
dockerCmd: nil,
appleCmd: nil,
expectError: true,
errorMessage: "docker command not available",
},
{
name: "apple runtime with nil command",
runtimeType: "apple",
dockerCmd: nil,
appleCmd: nil,
expectError: true,
errorMessage: "apple command not available",
},
{
name: "unsupported runtime",
runtimeType: "unsupported",
dockerCmd: nil,
appleCmd: nil,
expectError: true,
errorMessage: "unsupported runtime 'unsupported'",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
adapter := NewContainerRuntimeAdapter(tt.dockerCmd, tt.appleCmd, tt.runtimeType)
// Test GetContainers error handling
containers, err := adapter.GetContainers()
if tt.expectError {
assert.Error(t, err)
assert.Contains(t, err.Error(), tt.errorMessage)
assert.Nil(t, containers)
}
// Test RefreshImages error handling
images, err := adapter.RefreshImages()
if tt.expectError {
assert.Error(t, err)
assert.Contains(t, err.Error(), tt.errorMessage)
assert.Nil(t, images)
}
// Test PruneContainers error handling
err = adapter.PruneContainers()
if tt.expectError {
assert.Error(t, err)
// Apple runtime returns operation not supported error
if tt.runtimeType == "apple" {
assert.Contains(t, err.Error(), "container pruning not supported by apple runtime")
} else {
assert.Contains(t, err.Error(), tt.errorMessage)
}
}
})
}
}
func TestContainerRuntimeAdapterAppleSpecificBehavior(t *testing.T) {
adapter := NewContainerRuntimeAdapter(nil, &AppleContainerCommand{}, "apple")
// Test that Apple Container returns empty services
services, err := adapter.GetServices()
assert.Nil(t, err)
assert.Empty(t, services)
// Test that Apple Container doesn't support compose projects
assert.False(t, adapter.InDockerComposeProject())
// Test that Apple Container returns empty volumes
volumes, err := adapter.RefreshVolumes()
assert.Nil(t, err)
assert.Empty(t, volumes)
// Test that Apple Container returns empty networks
networks, err := adapter.RefreshNetworks()
assert.Nil(t, err)
assert.Empty(t, networks)
// Test that Apple Container doesn't support certain operations
err = adapter.PruneContainers()
assert.Error(t, err)
assert.Contains(t, err.Error(), "container pruning not supported by apple runtime")
err = adapter.PruneImages()
assert.Error(t, err)
assert.Contains(t, err.Error(), "image pruning not supported by apple runtime")
err = adapter.PruneVolumes()
assert.Error(t, err)
assert.Contains(t, err.Error(), "volume pruning not supported by apple runtime")
err = adapter.PruneNetworks()
assert.Error(t, err)
assert.Contains(t, err.Error(), "network pruning not supported by apple runtime")
// Test ViewAllLogs returns error for Apple Container
cmd, err := adapter.ViewAllLogs()
assert.Error(t, err)
assert.Contains(t, err.Error(), "viewing all logs not supported by apple runtime")
assert.Nil(t, cmd)
// Test DockerComposeConfig returns empty for Apple Container
config := adapter.DockerComposeConfig()
assert.Empty(t, config)
}
func TestContainerRuntimeAdapterClose(t *testing.T) {
// Test closing with docker runtime
dockerAdapter := NewContainerRuntimeAdapter(&DockerCommand{}, nil, "docker")
err := dockerAdapter.Close()
// We expect no error because our mock DockerCommand doesn't implement Close properly
// In a real scenario, this would call the actual Close method
assert.Nil(t, err)
// Test closing with apple runtime
appleAdapter := NewContainerRuntimeAdapter(nil, &AppleContainerCommand{}, "apple")
err = appleAdapter.Close()
assert.Nil(t, err) // Apple Container doesn't implement io.Closer, so this should always be nil
}

View file

@ -1,9 +1,10 @@
package commands package commands
import ( import (
"errors"
"fmt" "fmt"
"github.com/go-errors/errors" goErrors "github.com/go-errors/errors"
"golang.org/x/xerrors" "golang.org/x/xerrors"
) )
@ -20,7 +21,7 @@ func WrapError(err error) error {
return err return err
} }
return errors.Wrap(err, 0) return goErrors.Wrap(err, 0)
} }
// ComplexError an error which carries a code so that calling code has an easier job to do // ComplexError an error which carries a code so that calling code has an easier job to do
@ -55,3 +56,11 @@ func HasErrorCode(err error, code int) bool {
} }
return false return false
} }
// Runtime-specific errors
var (
ErrDockerCommandNotAvailable = errors.New("docker command not available")
ErrAppleContainerCommandNotAvailable = errors.New("apple container command not available")
ErrUnsupportedRuntime = errors.New("unsupported runtime")
ErrOperationNotSupported = errors.New("operation not supported by this runtime")
)

35
pkg/commands/features.go Normal file
View file

@ -0,0 +1,35 @@
package commands
// Feature represents a runtime capability that may or may not be supported
// by a given container runtime implementation.
type Feature int
const (
// Image features
FeatureImageHistory Feature = iota
FeatureImageRemove
FeatureImagePrune
// Container features
FeatureContainerAttach
FeatureContainerExec
FeatureContainerTop
FeatureContainerPrune
// Volume/Network
FeatureVolumePrune
FeatureNetworkPrune
// Services / compose
FeatureServices
// Telemetry/streaming
FeatureEventsStream
FeatureStats
// Newer Apple CLI capabilities
FeatureVolumeCreate
FeatureBuildPlatform
FeatureRunPlatform
FeatureSSHAgentForward
)

160
pkg/commands/gui_adapter.go Normal file
View file

@ -0,0 +1,160 @@
package commands
import (
"fmt"
"os/exec"
"github.com/jesseduffield/lazydocker/pkg/config"
)
// GuiContainerCommand provides an adapter that makes ContainerRuntime work with the GUI
// It implements the subset of DockerCommand methods that the GUI actually uses
type GuiContainerCommand struct {
runtime *ContainerRuntimeAdapter
dockerCommand *DockerCommand // For docker-specific operations
config *config.AppConfig
}
// NewGuiContainerCommand creates a new GUI adapter for the container runtime
func NewGuiContainerCommand(runtime *ContainerRuntimeAdapter, dockerCommand *DockerCommand, config *config.AppConfig) *GuiContainerCommand {
return &GuiContainerCommand{
runtime: runtime,
dockerCommand: dockerCommand,
config: config,
}
}
// Supports exposes runtime capability checks to the GUI layer
func (g *GuiContainerCommand) Supports(f Feature) bool {
if g.runtime == nil {
return false
}
return g.runtime.Supports(f)
}
// GetContainers returns containers
func (g *GuiContainerCommand) GetContainers(existingContainers []*Container) ([]*Container, error) {
return g.runtime.GetContainers()
}
// RefreshContainersAndServices refreshes both containers and services
func (g *GuiContainerCommand) RefreshContainersAndServices(currentServices []*Service, currentContainers []*Container) ([]*Container, []*Service, error) {
return g.runtime.RefreshContainersAndServices(currentServices, currentContainers)
}
// RefreshContainer updates details for a specific container
func (g *GuiContainerCommand) RefreshContainer(container *Container) error {
containers := []*Container{container}
return g.runtime.RefreshContainerDetails(containers)
}
// RefreshImages returns the list of images
func (g *GuiContainerCommand) RefreshImages() ([]*Image, error) {
return g.runtime.RefreshImages()
}
// RefreshVolumes returns the list of volumes
func (g *GuiContainerCommand) RefreshVolumes() ([]*Volume, error) {
return g.runtime.RefreshVolumes()
}
// RefreshNetworks returns the list of networks
func (g *GuiContainerCommand) RefreshNetworks() ([]*Network, error) {
return g.runtime.RefreshNetworks()
}
// GetServices returns the list of services
func (g *GuiContainerCommand) GetServices() ([]*Service, error) {
return g.runtime.GetServices()
}
// InDockerComposeProject checks if we're in a docker-compose project
func (g *GuiContainerCommand) InDockerComposeProject() bool {
return g.runtime.InDockerComposeProject()
}
// DockerComposeConfig returns the docker-compose configuration
func (g *GuiContainerCommand) DockerComposeConfig() string {
return g.runtime.DockerComposeConfig()
}
// GetRuntimeName returns the name of the active runtime
func (g *GuiContainerCommand) GetRuntimeName() string {
return g.runtime.GetRuntimeName()
}
// GetRuntimeVersion returns a friendly runtime version/info string
func (g *GuiContainerCommand) GetRuntimeVersion() string {
if g.runtime == nil {
return ""
}
return g.runtime.GetRuntimeVersion()
}
// CreateVolume creates a volume via the active runtime
func (g *GuiContainerCommand) CreateVolume(name string, opts map[string]string) error {
return g.runtime.CreateVolume(name, opts)
}
// SystemStatus returns a runtime-specific system status map
func (g *GuiContainerCommand) SystemStatus() (map[string]interface{}, error) {
return g.runtime.SystemStatus()
}
// ViewAllLogs returns a command to view all logs
func (g *GuiContainerCommand) ViewAllLogs() (*exec.Cmd, error) {
cmd, err := g.runtime.ViewAllLogs()
if err != nil {
return nil, err
}
// Convert from our Cmd interface to exec.Cmd
// This is a bit of a hack but maintains compatibility
if cmdWrapper, ok := cmd.(*cmdWrapper); ok {
return cmdWrapper.cmd, nil
}
return nil, fmt.Errorf("viewing all logs not supported by %s runtime", g.runtime.GetRuntimeName())
}
// PruneContainers removes unused containers
func (g *GuiContainerCommand) PruneContainers() error {
return g.runtime.PruneContainers()
}
// PruneImages removes unused images
func (g *GuiContainerCommand) PruneImages() error {
return g.runtime.PruneImages()
}
// PruneVolumes removes unused volumes
func (g *GuiContainerCommand) PruneVolumes() error {
return g.runtime.PruneVolumes()
}
// PruneNetworks removes unused networks
func (g *GuiContainerCommand) PruneNetworks() error {
return g.runtime.PruneNetworks()
}
// NewCommandObject creates a new command object with defaults
func (g *GuiContainerCommand) NewCommandObject(obj CommandObject) CommandObject {
if g.dockerCommand != nil {
return g.dockerCommand.NewCommandObject(obj)
}
// For Apple Container, just return the object as-is since it doesn't use docker-compose
return obj
}
// GetClient returns the Docker client if available (only for Docker runtime)
func (g *GuiContainerCommand) GetClient() interface{} {
if g.dockerCommand != nil {
return g.dockerCommand.Client
}
return nil
}
// CreateClientStatMonitor creates a stat monitor for a container (Docker-specific)
func (g *GuiContainerCommand) CreateClientStatMonitor(container *Container) {
if g.dockerCommand != nil {
g.dockerCommand.CreateClientStatMonitor(container)
}
}

View file

@ -2,6 +2,7 @@ package commands
import ( import (
"context" "context"
"fmt"
"strings" "strings"
"github.com/docker/docker/api/types/filters" "github.com/docker/docker/api/types/filters"
@ -73,9 +74,32 @@ func getHistoryResponseItemDisplayStrings(layer image.HistoryResponseItem) []str
} }
} }
// safeImageHistory wraps the Docker client's ImageHistory call to guard
// against panics originating from the Docker SDK when certain runtimes
// or daemons do not support the endpoint as expected. In the event of a
// panic, we convert it to an error so the TUI does not crash.
func (i *Image) safeImageHistory(ctx context.Context, imageID string) (items []image.HistoryResponseItem, err error) {
defer func() {
if r := recover(); r != nil {
// Avoid crashing the whole app when the Docker SDK panics.
if i != nil && i.Log != nil {
i.Log.Errorf("panic in ImageHistory: %v", r)
}
err = fmt.Errorf("failed to fetch image history: %v", r)
}
}()
// Guard against a nil client just in case.
if i == nil || i.Client == nil {
return nil, fmt.Errorf("docker client is not initialized")
}
return i.Client.ImageHistory(ctx, imageID)
}
// RenderHistory renders the history of the image // RenderHistory renders the history of the image
func (i *Image) RenderHistory() (string, error) { func (i *Image) RenderHistory() (string, error) {
history, err := i.Client.ImageHistory(context.Background(), i.ID) history, err := i.safeImageHistory(context.Background(), i.ID)
if err != nil { if err != nil {
return "", err return "", err
} }

View file

@ -2,6 +2,7 @@ package commands
import ( import (
"context" "context"
"fmt"
"os/exec" "os/exec"
"github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/container"
@ -26,6 +27,7 @@ func (s *Service) Remove(options container.RemoveOptions) error {
// Stop stops the service's containers // Stop stops the service's containers
func (s *Service) Stop() error { func (s *Service) Stop() error {
s.Log.Warn(fmt.Sprintf("stopping service %s", s.Name))
return s.runCommand(s.OSCommand.Config.UserConfig.CommandTemplates.StopService) return s.runCommand(s.OSCommand.Config.UserConfig.CommandTemplates.StopService)
} }
@ -49,7 +51,12 @@ func (s *Service) runCommand(templateCmdStr string) error {
templateCmdStr, templateCmdStr,
s.DockerCommand.NewCommandObject(CommandObject{Service: s}), s.DockerCommand.NewCommandObject(CommandObject{Service: s}),
) )
return s.OSCommand.RunCommand(command) s.Log.Warn(fmt.Sprintf("executing command: %s", command))
err := s.OSCommand.RunCommand(command)
if err != nil {
s.Log.Error(fmt.Sprintf("command failed: %s, error: %v", command, err))
}
return err
} }
// Attach attaches to the service // Attach attaches to the service

View file

@ -52,5 +52,12 @@ func (c *DockerCommand) PruneVolumes() error {
// Remove removes the volume // Remove removes the volume
func (v *Volume) Remove(force bool) error { func (v *Volume) Remove(force bool) error {
// For Apple containers, use the AppleContainerCommand
if v.Client == nil && v.DockerCommand != nil {
if appleCmd, ok := v.DockerCommand.(*AppleContainerCommand); ok {
return appleCmd.RemoveVolume(v.Name, force)
}
}
return v.Client.VolumeRemove(context.Background(), v.Name, force) return v.Client.VolumeRemove(context.Background(), v.Name, force)
} }

View file

@ -13,6 +13,7 @@
package config package config
import ( import (
"fmt"
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
@ -64,6 +65,23 @@ type UserConfig struct {
// will be filtered out and not displayed. // will be filtered out and not displayed.
// Not documented because it's subject to change // Not documented because it's subject to change
Ignore []string `yaml:"ignore,omitempty"` Ignore []string `yaml:"ignore,omitempty"`
// Apple contains Apple runtime specific configuration
Apple *AppleRuntimeConfig `yaml:"apple,omitempty"`
}
// AppleRuntimeConfig holds Apple container runtime options
type AppleRuntimeConfig struct {
// Build flags
BuildPlatform string `yaml:"buildPlatform,omitempty"`
BuildOS string `yaml:"buildOS,omitempty"`
BuildArch string `yaml:"buildArch,omitempty"`
// Run flags
RunPlatform string `yaml:"runPlatform,omitempty"`
RunOS string `yaml:"runOS,omitempty"`
RunArch string `yaml:"runArch,omitempty"`
// Exec/Run SSH agent forward
ForwardSSHAgent bool `yaml:"forwardSSHAgent,omitempty"`
} }
// ThemeConfig is for setting the colors of panels and some text. // ThemeConfig is for setting the colors of panels and some text.
@ -474,6 +492,7 @@ func GetDefaultConfig() UserConfig {
Replacements: Replacements{ Replacements: Replacements{
ImageNamePrefixes: map[string]string{}, ImageNamePrefixes: map[string]string{},
}, },
Apple: &AppleRuntimeConfig{},
} }
} }
@ -485,13 +504,14 @@ type AppConfig struct {
BuildDate string `long:"build-date" env:"BUILD_DATE"` BuildDate string `long:"build-date" env:"BUILD_DATE"`
Name string `long:"name" env:"NAME" default:"lazydocker"` Name string `long:"name" env:"NAME" default:"lazydocker"`
BuildSource string `long:"build-source" env:"BUILD_SOURCE" default:""` BuildSource string `long:"build-source" env:"BUILD_SOURCE" default:""`
Runtime string `long:"runtime" env:"RUNTIME" default:"docker"`
UserConfig *UserConfig UserConfig *UserConfig
ConfigDir string ConfigDir string
ProjectDir string ProjectDir string
} }
// NewAppConfig makes a new app config // NewAppConfig makes a new app config
func NewAppConfig(name, version, commit, date string, buildSource string, debuggingFlag bool, composeFiles []string, projectDir string) (*AppConfig, error) { func NewAppConfig(name, version, commit, date string, buildSource string, debuggingFlag bool, composeFiles []string, projectDir string, runtime string) (*AppConfig, error) {
configDir, err := findOrCreateConfigDir(name) configDir, err := findOrCreateConfigDir(name)
if err != nil { if err != nil {
return nil, err return nil, err
@ -507,6 +527,11 @@ func NewAppConfig(name, version, commit, date string, buildSource string, debugg
userConfig.CommandTemplates.DockerCompose += " -f " + strings.Join(composeFiles, " -f ") userConfig.CommandTemplates.DockerCompose += " -f " + strings.Join(composeFiles, " -f ")
} }
// Validate runtime parameter
if runtime != "docker" && runtime != "apple" {
return nil, fmt.Errorf("unsupported runtime '%s'. Supported runtimes: docker, apple", runtime)
}
appConfig := &AppConfig{ appConfig := &AppConfig{
Name: name, Name: name,
Version: version, Version: version,
@ -514,6 +539,7 @@ func NewAppConfig(name, version, commit, date string, buildSource string, debugg
BuildDate: date, BuildDate: date,
Debug: debuggingFlag || os.Getenv("DEBUG") == "TRUE", Debug: debuggingFlag || os.Getenv("DEBUG") == "TRUE",
BuildSource: buildSource, BuildSource: buildSource,
Runtime: runtime,
UserConfig: userConfig, UserConfig: userConfig,
ConfigDir: configDir, ConfigDir: configDir,
ProjectDir: projectDir, ProjectDir: projectDir,

View file

@ -9,7 +9,7 @@ import (
func TestDockerComposeCommandNoFiles(t *testing.T) { func TestDockerComposeCommandNoFiles(t *testing.T) {
composeFiles := []string{} composeFiles := []string{}
conf, err := NewAppConfig("name", "version", "commit", "date", "buildSource", false, composeFiles, "projectDir") conf, err := NewAppConfig("name", "version", "commit", "date", "buildSource", false, composeFiles, "projectDir", "docker")
if err != nil { if err != nil {
t.Fatalf("Unexpected error: %s", err) t.Fatalf("Unexpected error: %s", err)
} }
@ -23,7 +23,7 @@ func TestDockerComposeCommandNoFiles(t *testing.T) {
func TestDockerComposeCommandSingleFile(t *testing.T) { func TestDockerComposeCommandSingleFile(t *testing.T) {
composeFiles := []string{"one.yml"} composeFiles := []string{"one.yml"}
conf, err := NewAppConfig("name", "version", "commit", "date", "buildSource", false, composeFiles, "projectDir") conf, err := NewAppConfig("name", "version", "commit", "date", "buildSource", false, composeFiles, "projectDir", "docker")
if err != nil { if err != nil {
t.Fatalf("Unexpected error: %s", err) t.Fatalf("Unexpected error: %s", err)
} }
@ -37,7 +37,7 @@ func TestDockerComposeCommandSingleFile(t *testing.T) {
func TestDockerComposeCommandMultipleFiles(t *testing.T) { func TestDockerComposeCommandMultipleFiles(t *testing.T) {
composeFiles := []string{"one.yml", "two.yml", "three.yml"} composeFiles := []string{"one.yml", "two.yml", "three.yml"}
conf, err := NewAppConfig("name", "version", "commit", "date", "buildSource", false, composeFiles, "projectDir") conf, err := NewAppConfig("name", "version", "commit", "date", "buildSource", false, composeFiles, "projectDir", "docker")
if err != nil { if err != nil {
t.Fatalf("Unexpected error: %s", err) t.Fatalf("Unexpected error: %s", err)
} }
@ -52,7 +52,7 @@ func TestDockerComposeCommandMultipleFiles(t *testing.T) {
func TestWritingToConfigFile(t *testing.T) { func TestWritingToConfigFile(t *testing.T) {
// init the AppConfig // init the AppConfig
emptyComposeFiles := []string{} emptyComposeFiles := []string{}
conf, err := NewAppConfig("name", "version", "commit", "date", "buildSource", false, emptyComposeFiles, "projectDir") conf, err := NewAppConfig("name", "version", "commit", "date", "buildSource", false, emptyComposeFiles, "projectDir", "docker")
if err != nil { if err != nil {
t.Fatalf("Unexpected error: %s", err) t.Fatalf("Unexpected error: %s", err)
} }
@ -96,3 +96,76 @@ func TestWritingToConfigFile(t *testing.T) {
// modifying an existing file that already has 'ConfirmOnQuit' // modifying an existing file that already has 'ConfirmOnQuit'
testFn(t, conf, false) testFn(t, conf, false)
} }
// Test runtime parameter validation and configuration
func TestRuntimeValidation(t *testing.T) {
composeFiles := []string{}
// Test valid docker runtime
conf, err := NewAppConfig("name", "version", "commit", "date", "buildSource", false, composeFiles, "projectDir", "docker")
if err != nil {
t.Fatalf("Unexpected error for docker runtime: %s", err)
}
if conf.Runtime != "docker" {
t.Fatalf("Expected runtime 'docker' but got '%s'", conf.Runtime)
}
// Test valid apple runtime
conf, err = NewAppConfig("name", "version", "commit", "date", "buildSource", false, composeFiles, "projectDir", "apple")
if err != nil {
t.Fatalf("Unexpected error for apple runtime: %s", err)
}
if conf.Runtime != "apple" {
t.Fatalf("Expected runtime 'apple' but got '%s'", conf.Runtime)
}
// Test invalid runtime
_, err = NewAppConfig("name", "version", "commit", "date", "buildSource", false, composeFiles, "projectDir", "invalid")
if err == nil {
t.Fatalf("Expected error for invalid runtime but got none")
}
expectedError := "unsupported runtime 'invalid'. Supported runtimes: docker, apple"
if err.Error() != expectedError {
t.Fatalf("Expected error '%s' but got '%s'", expectedError, err.Error())
}
}
func TestRuntimeConfigurationDefaults(t *testing.T) {
composeFiles := []string{}
// Test docker runtime gets correct defaults
conf, err := NewAppConfig("name", "version", "commit", "date", "buildSource", false, composeFiles, "projectDir", "docker")
if err != nil {
t.Fatalf("Unexpected error: %s", err)
}
// Docker runtime should have normal docker-compose command
expected := "docker compose"
actual := conf.UserConfig.CommandTemplates.DockerCompose
if actual != expected {
t.Fatalf("Expected DockerCompose command '%s' but got '%s'", expected, actual)
}
}
func TestRuntimeFieldInAppConfig(t *testing.T) {
composeFiles := []string{}
testCases := []struct {
runtime string
expected string
}{
{"docker", "docker"},
{"apple", "apple"},
}
for _, tc := range testCases {
conf, err := NewAppConfig("name", "version", "commit", "date", "buildSource", false, composeFiles, "projectDir", tc.runtime)
if err != nil {
t.Fatalf("Unexpected error for runtime '%s': %s", tc.runtime, err)
}
if conf.Runtime != tc.expected {
t.Fatalf("Expected Runtime field '%s' but got '%s'", tc.expected, conf.Runtime)
}
}
}

View file

@ -9,6 +9,7 @@ import (
"time" "time"
"github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/container"
"github.com/docker/docker/client"
"github.com/docker/docker/pkg/stdcopy" "github.com/docker/docker/pkg/stdcopy"
"github.com/fatih/color" "github.com/fatih/color"
"github.com/jesseduffield/lazydocker/pkg/commands" "github.com/jesseduffield/lazydocker/pkg/commands"
@ -105,7 +106,16 @@ func (gui *Gui) promptToReturn() {
} }
func (gui *Gui) writeContainerLogs(ctr *commands.Container, ctx context.Context, writer io.Writer) error { func (gui *Gui) writeContainerLogs(ctr *commands.Container, ctx context.Context, writer io.Writer) error {
readCloser, err := gui.DockerCommand.Client.ContainerLogs(ctx, ctr.ID, container.LogsOptions{ clientInterface := gui.ContainerCommand.GetClient()
if clientInterface == nil {
// Handle Apple Container logs
if gui.ContainerCommand.GetRuntimeName() == "apple" {
return gui.writeAppleContainerLogs(ctr, ctx, writer)
}
return fmt.Errorf("container logs not supported for %s runtime", gui.ContainerCommand.GetRuntimeName())
}
dockerClient := clientInterface.(*client.Client)
readCloser, err := dockerClient.ContainerLogs(ctx, ctr.ID, container.LogsOptions{
ShowStdout: true, ShowStdout: true,
ShowStderr: true, ShowStderr: true,
Timestamps: gui.Config.UserConfig.Logs.Timestamps, Timestamps: gui.Config.UserConfig.Logs.Timestamps,
@ -150,3 +160,40 @@ func (gui *Gui) writeContainerLogs(ctr *commands.Container, ctx context.Context,
return nil return nil
} }
func (gui *Gui) writeAppleContainerLogs(ctr *commands.Container, ctx context.Context, writer io.Writer) error {
// Get the AppleContainerCommand from the DockerCommand interface
appleCmd, ok := ctr.DockerCommand.(*commands.AppleContainerCommand)
if !ok {
return fmt.Errorf("invalid container command type for Apple runtime")
}
// Get the logs command
cmd := appleCmd.GetContainerLogs(ctr.ID, true, gui.Config.UserConfig.Logs.Tail)
// Set up the command with proper context
cmd.Stdout = writer
cmd.Stderr = writer
// Start the command
if err := cmd.Start(); err != nil {
return err
}
// Wait for context cancellation or command completion
done := make(chan error, 1)
go func() {
done <- cmd.Wait()
}()
select {
case <-ctx.Done():
// Context cancelled, kill the process
if cmd.Process != nil {
cmd.Process.Kill()
}
return ctx.Err()
case err := <-done:
return err
}
}

View file

@ -34,33 +34,55 @@ func (gui *Gui) getContainersPanel() *panels.SideListPanel[*commands.Container]
return &panels.SideListPanel[*commands.Container]{ return &panels.SideListPanel[*commands.Container]{
ContextState: &panels.ContextState[*commands.Container]{ ContextState: &panels.ContextState[*commands.Container]{
GetMainTabs: func() []panels.MainTab[*commands.Container] { GetMainTabs: func() []panels.MainTab[*commands.Container] {
return []panels.MainTab[*commands.Container]{ tabs := []panels.MainTab[*commands.Container]{
{ {
Key: "logs", Key: "logs",
Title: gui.Tr.LogsTitle, Title: gui.Tr.LogsTitle,
Render: gui.renderContainerLogsToMain, Render: gui.renderContainerLogsToMain,
}, },
{ }
// Add runtime-specific tabs
if !gui.ContainerCommand.Supports(commands.FeatureStats) {
// When stats are not supported (e.g. Apple), show a Volumes tab instead
tabs = append(tabs, panels.MainTab[*commands.Container]{
Key: "volumes",
Title: "Volumes",
Render: gui.renderContainerVolumesToMain,
})
} else {
// Runtime supports live stats (Docker)
tabs = append(tabs, panels.MainTab[*commands.Container]{
Key: "stats", Key: "stats",
Title: gui.Tr.StatsTitle, Title: gui.Tr.StatsTitle,
Render: gui.renderContainerStats, Render: gui.renderContainerStats,
}, })
{ }
// Common tabs for all runtimes
tabs = append(tabs,
panels.MainTab[*commands.Container]{
Key: "env", Key: "env",
Title: gui.Tr.EnvTitle, Title: gui.Tr.EnvTitle,
Render: gui.renderContainerEnv, Render: gui.renderContainerEnv,
}, },
{ panels.MainTab[*commands.Container]{
Key: "config", Key: "config",
Title: gui.Tr.ConfigTitle, Title: gui.Tr.ConfigTitle,
Render: gui.renderContainerConfig, Render: gui.renderContainerConfig,
}, },
{ )
// Only add Top tab if supported by runtime
if gui.ContainerCommand.Supports(commands.FeatureContainerTop) {
tabs = append(tabs, panels.MainTab[*commands.Container]{
Key: "top", Key: "top",
Title: gui.Tr.TopTitle, Title: gui.Tr.TopTitle,
Render: gui.renderContainerTop, Render: gui.renderContainerTop,
}, })
} }
return tabs
}, },
GetItemContextCacheKey: func(container *commands.Container) string { GetItemContextCacheKey: func(container *commands.Container) string {
// Including the container state in the cache key so that if the container // Including the container state in the cache key so that if the container
@ -97,7 +119,17 @@ func (gui *Gui) getContainersPanel() *panels.SideListPanel[*commands.Container]
return true return true
}, },
GetTableCells: func(container *commands.Container) []string { GetTableCells: func(container *commands.Container) []string {
return presentation.GetContainerDisplayStrings(&gui.Config.UserConfig.Gui, container) runtime := gui.ContainerCommand.GetRuntimeName()
return presentation.GetContainerDisplayStrings(&gui.Config.UserConfig.Gui, container, runtime)
},
GetTableHeaders: func() []string {
runtime := gui.ContainerCommand.GetRuntimeName()
// Columns: STATUS, SUB, NAME, CPU, ADDR (apple only), PORTS, IMAGE
addr := ""
if runtime == "apple" {
addr = "ADDR"
}
return []string{"STATUS", "", "NAME", "CPU", addr, "PORTS", "IMAGE"}
}, },
} }
} }
@ -255,7 +287,7 @@ func (gui *Gui) refreshContainersAndServices() error {
originalSelectedLineIdx := gui.Panels.Services.SelectedIdx originalSelectedLineIdx := gui.Panels.Services.SelectedIdx
selectedService, isServiceSelected := gui.Panels.Services.List.TryGet(originalSelectedLineIdx) selectedService, isServiceSelected := gui.Panels.Services.List.TryGet(originalSelectedLineIdx)
containers, services, err := gui.DockerCommand.RefreshContainersAndServices( containers, services, err := gui.ContainerCommand.RefreshContainersAndServices(
gui.Panels.Services.List.GetAllItems(), gui.Panels.Services.List.GetAllItems(),
gui.Panels.Containers.List.GetAllItems(), gui.Panels.Containers.List.GetAllItems(),
) )
@ -283,7 +315,7 @@ func (gui *Gui) refreshContainersAndServices() error {
} }
func (gui *Gui) renderContainersAndServices() error { func (gui *Gui) renderContainersAndServices() error {
if gui.DockerCommand.InDockerComposeProject { if gui.ContainerCommand.InDockerComposeProject() {
if err := gui.Panels.Services.RerenderList(); err != nil { if err := gui.Panels.Services.RerenderList(); err != nil {
return err return err
} }
@ -325,13 +357,21 @@ func (gui *Gui) handleContainersRemoveMenu(g *gocui.Gui, v *gocui.View) error {
}) })
} }
runtimeName := gui.ContainerCommand.GetRuntimeName()
rmCmd := "docker rm " + ctr.ID[1:10]
rmVolCmd := "docker rm --volumes " + ctr.ID[1:10]
if runtimeName == "apple" {
rmCmd = "container rm " + ctr.ID
rmVolCmd = "container rm --force " + ctr.ID // Apple CLI may not support --volumes; show force
}
menuItems := []*types.MenuItem{ menuItems := []*types.MenuItem{
{ {
LabelColumns: []string{gui.Tr.Remove, "docker rm " + ctr.ID[1:10]}, LabelColumns: []string{gui.Tr.Remove, rmCmd},
OnPress: func() error { return handleMenuPress(container.RemoveOptions{}) }, OnPress: func() error { return handleMenuPress(container.RemoveOptions{}) },
}, },
{ {
LabelColumns: []string{gui.Tr.RemoveWithVolumes, "docker rm --volumes " + ctr.ID[1:10]}, LabelColumns: []string{gui.Tr.RemoveWithVolumes, rmVolCmd},
OnPress: func() error { return handleMenuPress(container.RemoveOptions{RemoveVolumes: true}) }, OnPress: func() error { return handleMenuPress(container.RemoveOptions{RemoveVolumes: true}) },
}, },
} }
@ -414,9 +454,12 @@ func (gui *Gui) handleContainerAttach(g *gocui.Gui, v *gocui.View) error {
} }
func (gui *Gui) handlePruneContainers() error { func (gui *Gui) handlePruneContainers() error {
if gui.ContainerCommand != nil && !gui.ContainerCommand.Supports(commands.FeatureContainerPrune) {
return gui.createErrorPanel("Container pruning is not supported by the current container runtime.")
}
return gui.createConfirmationPanel(gui.Tr.Confirm, gui.Tr.ConfirmPruneContainers, func(g *gocui.Gui, v *gocui.View) error { return gui.createConfirmationPanel(gui.Tr.Confirm, gui.Tr.ConfirmPruneContainers, func(g *gocui.Gui, v *gocui.View) error {
return gui.WithWaitingStatus(gui.Tr.PruningStatus, func() error { return gui.WithWaitingStatus(gui.Tr.PruningStatus, func() error {
err := gui.DockerCommand.PruneContainers() err := gui.ContainerCommand.PruneContainers()
if err != nil { if err != nil {
return gui.createErrorPanel(err.Error()) return gui.createErrorPanel(err.Error())
} }
@ -446,13 +489,24 @@ func (gui *Gui) handleContainersExecShell(g *gocui.Gui, v *gocui.View) error {
} }
func (gui *Gui) containerExecShell(container *commands.Container) error { func (gui *Gui) containerExecShell(container *commands.Container) error {
commandObject := gui.DockerCommand.NewCommandObject(commands.CommandObject{ // Use appropriate exec based on runtime capability
if gui.ContainerCommand.Supports(commands.FeatureContainerExec) && gui.ContainerCommand.GetRuntimeName() == "apple" {
// Prefer a simple interactive shell for Apple Container
resolved := fmt.Sprintf("container exec -it %s /bin/sh", container.ID)
// Optionally forward SSH agent when supported and enabled
if gui.Config.UserConfig.Apple != nil && gui.Config.UserConfig.Apple.ForwardSSHAgent && gui.ContainerCommand.Supports(commands.FeatureSSHAgentForward) {
resolved = fmt.Sprintf("container exec --ssh -it %s /bin/sh", container.ID)
}
cmd := gui.OSCommand.ExecutableFromString(resolved)
return gui.runSubprocess(cmd)
}
commandObject := gui.ContainerCommand.NewCommandObject(commands.CommandObject{
Container: container, Container: container,
}) })
// TODO: use SDK // Docker runtime
resolvedCommand := utils.ApplyTemplate("docker exec -it {{ .Container.ID }} /bin/sh -c 'eval $(grep ^$(id -un): /etc/passwd | cut -d : -f 7-)'", commandObject) resolvedCommand := utils.ApplyTemplate("docker exec -it {{ .Container.ID }} /bin/sh -c 'eval $(grep ^$(id -un): /etc/passwd | cut -d : -f 7-)'", commandObject)
// attach and return the subprocess error
cmd := gui.OSCommand.ExecutableFromString(resolvedCommand) cmd := gui.OSCommand.ExecutableFromString(resolvedCommand)
return gui.runSubprocess(cmd) return gui.runSubprocess(cmd)
} }
@ -463,7 +517,7 @@ func (gui *Gui) handleContainersCustomCommand(g *gocui.Gui, v *gocui.View) error
return nil return nil
} }
commandObject := gui.DockerCommand.NewCommandObject(commands.CommandObject{ commandObject := gui.ContainerCommand.NewCommandObject(commands.CommandObject{
Container: ctr, Container: ctr,
}) })
@ -502,22 +556,18 @@ func (gui *Gui) handleRemoveContainers() error {
func (gui *Gui) handleContainersBulkCommand(g *gocui.Gui, v *gocui.View) error { func (gui *Gui) handleContainersBulkCommand(g *gocui.Gui, v *gocui.View) error {
baseBulkCommands := []config.CustomCommand{ baseBulkCommands := []config.CustomCommand{
{ {Name: gui.Tr.StopAllContainers, InternalFunction: gui.handleStopContainers},
Name: gui.Tr.StopAllContainers, {Name: gui.Tr.RemoveAllContainers, InternalFunction: gui.handleRemoveContainers},
InternalFunction: gui.handleStopContainers, }
}, if gui.ContainerCommand == nil || gui.ContainerCommand.Supports(commands.FeatureContainerPrune) {
{ baseBulkCommands = append(baseBulkCommands, config.CustomCommand{ // show only if supported
Name: gui.Tr.RemoveAllContainers,
InternalFunction: gui.handleRemoveContainers,
},
{
Name: gui.Tr.PruneContainers, Name: gui.Tr.PruneContainers,
InternalFunction: gui.handlePruneContainers, InternalFunction: gui.handlePruneContainers,
}, })
} }
bulkCommands := append(baseBulkCommands, gui.Config.UserConfig.BulkCommands.Containers...) bulkCommands := append(baseBulkCommands, gui.Config.UserConfig.BulkCommands.Containers...)
commandObject := gui.DockerCommand.NewCommandObject(commands.CommandObject{}) commandObject := gui.ContainerCommand.NewCommandObject(commands.CommandObject{})
return gui.createBulkCommandMenu(bulkCommands, commandObject) return gui.createBulkCommandMenu(bulkCommands, commandObject)
} }
@ -549,3 +599,29 @@ func (gui *Gui) openContainerInBrowser(ctr *commands.Container) error {
link := fmt.Sprintf("http://%s:%d/", ip, port.PublicPort) link := fmt.Sprintf("http://%s:%d/", ip, port.PublicPort)
return gui.OSCommand.OpenLink(link) return gui.OSCommand.OpenLink(link)
} }
// renderContainerVolumesToMain renders the container's volume/mount information for Apple runtime
func (gui *Gui) renderContainerVolumesToMain(container *commands.Container) tasks.TaskFunc {
return gui.NewTickerTask(TickerTaskOpts{
Func: func(ctx context.Context, notifyStopped chan struct{}) {
// Get the container's mount information from Apple Container
if container.DockerCommand != nil {
if appleCmd, ok := container.DockerCommand.(*commands.AppleContainerCommand); ok {
mountInfo, err := appleCmd.GetContainerMounts(container.ID)
if err != nil {
gui.reRenderStringMain(fmt.Sprintf("Error getting mount information: %v", err))
return
}
gui.reRenderStringMain(mountInfo)
return
}
}
gui.reRenderStringMain("Volume information not available")
},
Duration: time.Hour, // Don't refresh automatically since mounts don't change
Before: func(ctx context.Context) { gui.clearMainView() },
Wrap: false,
Autoscroll: false,
})
}

View file

@ -7,6 +7,7 @@ import (
"time" "time"
"github.com/docker/docker/api/types/events" "github.com/docker/docker/api/types/events"
"github.com/docker/docker/client"
"github.com/go-errors/errors" "github.com/go-errors/errors"
@ -25,17 +26,18 @@ import (
// Gui wraps the gocui Gui object which handles rendering and events // Gui wraps the gocui Gui object which handles rendering and events
type Gui struct { type Gui struct {
g *gocui.Gui g *gocui.Gui
Log *logrus.Entry Log *logrus.Entry
DockerCommand *commands.DockerCommand DockerCommand *commands.DockerCommand
OSCommand *commands.OSCommand ContainerCommand *commands.GuiContainerCommand
State guiState OSCommand *commands.OSCommand
Config *config.AppConfig State guiState
Tr *i18n.TranslationSet Config *config.AppConfig
statusManager *statusManager Tr *i18n.TranslationSet
taskManager *tasks.TaskManager statusManager *statusManager
ErrorChan chan error taskManager *tasks.TaskManager
Views Views ErrorChan chan error
Views Views
// if we've suspended the gui (e.g. because we've switched to a subprocess) // if we've suspended the gui (e.g. because we've switched to a subprocess)
// we typically want to pause some things that are running like background // we typically want to pause some things that are running like background
@ -125,7 +127,7 @@ func getScreenMode(config *config.AppConfig) WindowMaximisation {
} }
// NewGui builds a new gui handler // NewGui builds a new gui handler
func NewGui(log *logrus.Entry, dockerCommand *commands.DockerCommand, oSCommand *commands.OSCommand, tr *i18n.TranslationSet, config *config.AppConfig, errorChan chan error) (*Gui, error) { func NewGui(log *logrus.Entry, dockerCommand *commands.DockerCommand, containerCommand *commands.GuiContainerCommand, oSCommand *commands.OSCommand, tr *i18n.TranslationSet, config *config.AppConfig, errorChan chan error) (*Gui, error) {
initialState := guiState{ initialState := guiState{
Platform: *oSCommand.Platform, Platform: *oSCommand.Platform,
Panels: &panelStates{ Panels: &panelStates{
@ -140,15 +142,16 @@ func NewGui(log *logrus.Entry, dockerCommand *commands.DockerCommand, oSCommand
} }
gui := &Gui{ gui := &Gui{
Log: log, Log: log,
DockerCommand: dockerCommand, DockerCommand: dockerCommand,
OSCommand: oSCommand, ContainerCommand: containerCommand,
State: initialState, OSCommand: oSCommand,
Config: config, State: initialState,
Tr: tr, Config: config,
statusManager: &statusManager{}, Tr: tr,
taskManager: tasks.NewTaskManager(log, tr), statusManager: &statusManager{},
ErrorChan: errorChan, taskManager: tasks.NewTaskManager(log, tr),
ErrorChan: errorChan,
} }
deadlock.Opts.Disable = !gui.Config.Debug deadlock.Opts.Disable = !gui.Config.Debug
@ -292,7 +295,9 @@ func (gui *Gui) setPanels() {
} }
func (gui *Gui) updateContainerDetails() error { func (gui *Gui) updateContainerDetails() error {
return gui.DockerCommand.RefreshContainerDetails(gui.Panels.Containers.List.GetAllItems()) containers := gui.Panels.Containers.List.GetAllItems()
_, _, err := gui.ContainerCommand.RefreshContainersAndServices(gui.Panels.Services.List.GetAllItems(), containers)
return err
} }
func (gui *Gui) refresh() { func (gui *Gui) refresh() {
@ -336,7 +341,18 @@ func (gui *Gui) listenForEvents(ctx context.Context, refresh func()) {
outer: outer:
for { for {
messageChan, errChan := gui.DockerCommand.Client.Events(context.Background(), events.ListOptions{}) // Event monitoring is runtime-dependent
if !gui.ContainerCommand.Supports(commands.FeatureEventsStream) {
// No event stream for this runtime; exit listener
return
}
clientInterface := gui.ContainerCommand.GetClient()
if clientInterface == nil {
return
}
dockerClient := clientInterface.(*client.Client)
messageChan, errChan := dockerClient.Events(context.Background(), events.ListOptions{})
if errorCount > 0 { if errorCount > 0 {
select { select {
@ -459,7 +475,7 @@ func (gui *Gui) ShouldRefresh(key string) bool {
} }
func (gui *Gui) initiallyFocusedViewName() string { func (gui *Gui) initiallyFocusedViewName() string {
if gui.DockerCommand.InDockerComposeProject { if gui.ContainerCommand.InDockerComposeProject() {
return "services" return "services"
} }
return "containers" return "containers"
@ -485,7 +501,7 @@ func (gui *Gui) monitorContainerStats(ctx context.Context) {
case <-ticker.C: case <-ticker.C:
for _, container := range gui.Panels.Containers.List.GetAllItems() { for _, container := range gui.Panels.Containers.List.GetAllItems() {
if !container.MonitoringStats { if !container.MonitoringStats {
go gui.DockerCommand.CreateClientStatMonitor(container) go gui.ContainerCommand.CreateClientStatMonitor(container)
} }
} }
} }

104
pkg/gui/gui_test.go Normal file
View file

@ -0,0 +1,104 @@
package gui
import (
"testing"
"github.com/jesseduffield/lazydocker/pkg/commands"
"github.com/jesseduffield/lazydocker/pkg/config"
"github.com/jesseduffield/lazydocker/pkg/i18n"
"github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert"
)
func TestNewGuiWithContainerCommand(t *testing.T) {
// Create test dependencies
log := logrus.NewEntry(logrus.New())
osCommand := commands.NewOSCommand(log, &config.AppConfig{})
tr := &i18n.TranslationSet{}
errorChan := make(chan error)
tests := []struct {
name string
runtime string
expectError bool
}{
{
name: "docker runtime",
runtime: "docker",
expectError: false,
},
{
name: "apple runtime",
runtime: "apple",
expectError: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Create app config
appConfig, err := config.NewAppConfig(
"lazydocker",
"test-version",
"test-commit",
"test-date",
"test-build-source",
false,
[]string{},
"/tmp",
tt.runtime,
)
assert.Nil(t, err)
// Create mock container runtime
containerRuntime := commands.NewContainerRuntimeAdapter(nil, nil, tt.runtime)
containerCommand := commands.NewGuiContainerCommand(containerRuntime, nil, appConfig)
// Create GUI
gui, err := NewGui(log, nil, containerCommand, osCommand, tr, appConfig, errorChan)
if tt.expectError {
assert.NotNil(t, err)
} else {
assert.Nil(t, err)
assert.NotNil(t, gui)
assert.Equal(t, containerCommand, gui.ContainerCommand)
assert.Equal(t, appConfig, gui.Config)
}
})
}
}
func TestGuiContainerCommandIntegration(t *testing.T) {
// Test that GUI can use ContainerCommand methods
appConfig, err := config.NewAppConfig(
"lazydocker",
"test-version",
"test-commit",
"test-date",
"test-build-source",
false,
[]string{},
"/tmp",
"docker",
)
assert.Nil(t, err)
// Create mock runtime adapter
containerRuntime := &commands.ContainerRuntimeAdapter{}
containerCommand := commands.NewGuiContainerCommand(containerRuntime, nil, appConfig)
// Test runtime name
assert.Equal(t, "", containerCommand.GetRuntimeName()) // Empty because we didn't set runtimeType
// Test that methods don't panic when called with nil internals
containers, err := containerCommand.GetContainers(nil)
assert.NotNil(t, err) // Should error because runtime is not properly initialized
assert.Nil(t, containers)
// Test InDockerComposeProject
assert.False(t, containerCommand.InDockerComposeProject())
// Test DockerComposeConfig
assert.Empty(t, containerCommand.DockerComposeConfig())
}

View file

@ -80,11 +80,20 @@ func (gui *Gui) imageConfigStr(image *commands.Image) string {
output += utils.WithPadding("ID: ", padding) + image.Image.ID + "\n" output += utils.WithPadding("ID: ", padding) + image.Image.ID + "\n"
output += utils.WithPadding("Tags: ", padding) + utils.ColoredString(strings.Join(image.Image.RepoTags, ", "), color.FgGreen) + "\n" output += utils.WithPadding("Tags: ", padding) + utils.ColoredString(strings.Join(image.Image.RepoTags, ", "), color.FgGreen) + "\n"
output += utils.WithPadding("Size: ", padding) + utils.FormatDecimalBytes(int(image.Image.Size)) + "\n" output += utils.WithPadding("Size: ", padding) + utils.FormatDecimalBytes(int(image.Image.Size)) + "\n"
output += utils.WithPadding("Created: ", padding) + fmt.Sprintf("%v", time.Unix(image.Image.Created, 0).Format(time.RFC1123)) + "\n" created := "n/a"
if image.Image.Created != 0 {
created = fmt.Sprintf("%v", time.Unix(image.Image.Created, 0).Format(time.RFC1123))
}
output += utils.WithPadding("Created: ", padding) + created + "\n"
history, err := image.RenderHistory() history, err := image.RenderHistory()
if err != nil { if err != nil {
gui.Log.Error(err) gui.Log.Error(err)
if gui.ContainerCommand != nil && !gui.ContainerCommand.Supports(commands.FeatureImageHistory) {
history = "History not available for Apple Container runtime"
} else {
history = "History unavailable: " + err.Error()
}
} }
output += "\n\n" + history output += "\n\n" + history
@ -101,7 +110,7 @@ func (gui *Gui) reloadImages() error {
} }
func (gui *Gui) refreshStateImages() error { func (gui *Gui) refreshStateImages() error {
images, err := gui.DockerCommand.RefreshImages() images, err := gui.ContainerCommand.RefreshImages()
if err != nil { if err != nil {
return err return err
} }
@ -131,6 +140,11 @@ func (gui *Gui) handleImagesRemoveMenu(g *gocui.Gui, v *gocui.View) error {
return nil return nil
} }
// Apple Container runtime: image removal not yet supported via our adapter
if gui.ContainerCommand != nil && !gui.ContainerCommand.Supports(commands.FeatureImageRemove) {
return gui.createErrorPanel("Image removal is not available in the Apple Container runtime from LazyDocker yet.")
}
shortSha := img.ID[7:17] shortSha := img.ID[7:17]
// TODO: have a way of toggling in a menu instead of showing each permutation as a separate menu item // TODO: have a way of toggling in a menu instead of showing each permutation as a separate menu item
@ -180,9 +194,12 @@ func (gui *Gui) handleImagesRemoveMenu(g *gocui.Gui, v *gocui.View) error {
} }
func (gui *Gui) handlePruneImages() error { func (gui *Gui) handlePruneImages() error {
if gui.ContainerCommand != nil && !gui.ContainerCommand.Supports(commands.FeatureImagePrune) {
return gui.createErrorPanel("Image pruning is not supported by the current container runtime.")
}
return gui.createConfirmationPanel(gui.Tr.Confirm, gui.Tr.ConfirmPruneImages, func(g *gocui.Gui, v *gocui.View) error { return gui.createConfirmationPanel(gui.Tr.Confirm, gui.Tr.ConfirmPruneImages, func(g *gocui.Gui, v *gocui.View) error {
return gui.WithWaitingStatus(gui.Tr.PruningStatus, func() error { return gui.WithWaitingStatus(gui.Tr.PruningStatus, func() error {
err := gui.DockerCommand.PruneImages() err := gui.ContainerCommand.PruneImages()
if err != nil { if err != nil {
return gui.createErrorPanel(err.Error()) return gui.createErrorPanel(err.Error())
} }
@ -197,7 +214,7 @@ func (gui *Gui) handleImagesCustomCommand(g *gocui.Gui, v *gocui.View) error {
return nil return nil
} }
commandObject := gui.DockerCommand.NewCommandObject(commands.CommandObject{ commandObject := gui.ContainerCommand.NewCommandObject(commands.CommandObject{
Image: img, Image: img,
}) })
@ -207,15 +224,16 @@ func (gui *Gui) handleImagesCustomCommand(g *gocui.Gui, v *gocui.View) error {
} }
func (gui *Gui) handleImagesBulkCommand(g *gocui.Gui, v *gocui.View) error { func (gui *Gui) handleImagesBulkCommand(g *gocui.Gui, v *gocui.View) error {
baseBulkCommands := []config.CustomCommand{ baseBulkCommands := []config.CustomCommand{}
{ if gui.ContainerCommand == nil || gui.ContainerCommand.Supports(commands.FeatureImagePrune) {
baseBulkCommands = append(baseBulkCommands, config.CustomCommand{
Name: gui.Tr.PruneImages, Name: gui.Tr.PruneImages,
InternalFunction: gui.handlePruneImages, InternalFunction: gui.handlePruneImages,
}, })
} }
bulkCommands := append(baseBulkCommands, gui.Config.UserConfig.BulkCommands.Images...) bulkCommands := append(baseBulkCommands, gui.Config.UserConfig.BulkCommands.Images...)
commandObject := gui.DockerCommand.NewCommandObject(commands.CommandObject{}) commandObject := gui.ContainerCommand.NewCommandObject(commands.CommandObject{})
return gui.createBulkCommandMenu(bulkCommands, commandObject) return gui.createBulkCommandMenu(bulkCommands, commandObject)
} }

View file

@ -4,6 +4,7 @@ import (
"fmt" "fmt"
"github.com/jesseduffield/gocui" "github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazydocker/pkg/commands"
) )
// Binding - a keybinding mapping a key and modifier to a handler. The keypress // Binding - a keybinding mapping a key and modifier to a handler. The keypress
@ -220,13 +221,7 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
Handler: gui.handleContainerRestart, Handler: gui.handleContainerRestart,
Description: gui.Tr.Restart, Description: gui.Tr.Restart,
}, },
{ // container attach: added conditionally below based on runtime support
ViewName: "containers",
Key: 'a',
Modifier: gocui.ModNone,
Handler: gui.handleContainerAttach,
Description: gui.Tr.Attach,
},
{ {
ViewName: "containers", ViewName: "containers",
Key: 'm', Key: 'm',
@ -304,13 +299,7 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
Handler: gui.handleServiceStart, Handler: gui.handleServiceStart,
Description: gui.Tr.Start, Description: gui.Tr.Start,
}, },
{ // service attach: added conditionally below based on runtime support
ViewName: "services",
Key: 'a',
Modifier: gocui.ModNone,
Handler: gui.handleServiceAttach,
Description: gui.Tr.Attach,
},
{ {
ViewName: "services", ViewName: "services",
Key: 'm', Key: 'm',
@ -395,6 +384,7 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
Handler: gui.handleVolumesCustomCommand, Handler: gui.handleVolumesCustomCommand,
Description: gui.Tr.RunCustomCommand, Description: gui.Tr.RunCustomCommand,
}, },
// Create volume binding will be added conditionally below based on capability
{ {
ViewName: "volumes", ViewName: "volumes",
Key: 'd', Key: 'd',
@ -511,6 +501,21 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
}, },
} }
// Conditionally add attach bindings if supported by runtime
if gui.ContainerCommand == nil || gui.ContainerCommand.Supports(commands.FeatureContainerAttach) {
bindings = append(bindings,
&Binding{ViewName: "containers", Key: 'a', Modifier: gocui.ModNone, Handler: gui.handleContainerAttach, Description: gui.Tr.Attach},
&Binding{ViewName: "services", Key: 'a', Modifier: gocui.ModNone, Handler: gui.handleServiceAttach, Description: gui.Tr.Attach},
)
}
// Conditionally add create-volume binding if supported
if gui.ContainerCommand == nil || gui.ContainerCommand.Supports(commands.FeatureVolumeCreate) {
bindings = append(bindings,
&Binding{ViewName: "volumes", Key: 'n', Modifier: gocui.ModNone, Handler: gui.handleCreateVolume, Description: "Create Volume"},
)
}
for _, panel := range gui.allSidePanels() { for _, panel := range gui.allSidePanels() {
bindings = append(bindings, []*Binding{ bindings = append(bindings, []*Binding{
{ViewName: panel.GetView().Name(), Key: gocui.KeyArrowLeft, Modifier: gocui.ModNone, Handler: gui.previousView}, {ViewName: panel.GetView().Name(), Key: gocui.KeyArrowLeft, Modifier: gocui.ModNone, Handler: gui.previousView},

View file

@ -89,7 +89,7 @@ func (gui *Gui) reloadNetworks() error {
} }
func (gui *Gui) refreshStateNetworks() error { func (gui *Gui) refreshStateNetworks() error {
networks, err := gui.DockerCommand.RefreshNetworks() networks, err := gui.ContainerCommand.RefreshNetworks()
if err != nil { if err != nil {
return err return err
} }
@ -110,10 +110,16 @@ func (gui *Gui) handleNetworksRemoveMenu(g *gocui.Gui, v *gocui.View) error {
command string command string
} }
runtimeName := gui.ContainerCommand.GetRuntimeName()
rmCmd := utils.WithShortSha("docker network rm " + network.Name)
if runtimeName == "apple" {
rmCmd = utils.WithShortSha("container network rm " + network.Name)
}
options := []*removeNetworkOption{ options := []*removeNetworkOption{
{ {
description: gui.Tr.Remove, description: gui.Tr.Remove,
command: utils.WithShortSha("docker network rm " + network.Name), command: rmCmd,
}, },
} }
@ -138,9 +144,12 @@ func (gui *Gui) handleNetworksRemoveMenu(g *gocui.Gui, v *gocui.View) error {
} }
func (gui *Gui) handlePruneNetworks() error { func (gui *Gui) handlePruneNetworks() error {
if gui.ContainerCommand != nil && !gui.ContainerCommand.Supports(commands.FeatureNetworkPrune) {
return gui.createErrorPanel("Network pruning is not supported by the current container runtime.")
}
return gui.createConfirmationPanel(gui.Tr.Confirm, gui.Tr.ConfirmPruneNetworks, func(g *gocui.Gui, v *gocui.View) error { return gui.createConfirmationPanel(gui.Tr.Confirm, gui.Tr.ConfirmPruneNetworks, func(g *gocui.Gui, v *gocui.View) error {
return gui.WithWaitingStatus(gui.Tr.PruningStatus, func() error { return gui.WithWaitingStatus(gui.Tr.PruningStatus, func() error {
err := gui.DockerCommand.PruneNetworks() err := gui.ContainerCommand.PruneNetworks()
if err != nil { if err != nil {
return gui.createErrorPanel(err.Error()) return gui.createErrorPanel(err.Error())
} }
@ -155,7 +164,7 @@ func (gui *Gui) handleNetworksCustomCommand(g *gocui.Gui, v *gocui.View) error {
return nil return nil
} }
commandObject := gui.DockerCommand.NewCommandObject(commands.CommandObject{ commandObject := gui.ContainerCommand.NewCommandObject(commands.CommandObject{
Network: network, Network: network,
}) })
@ -165,15 +174,16 @@ func (gui *Gui) handleNetworksCustomCommand(g *gocui.Gui, v *gocui.View) error {
} }
func (gui *Gui) handleNetworksBulkCommand(g *gocui.Gui, v *gocui.View) error { func (gui *Gui) handleNetworksBulkCommand(g *gocui.Gui, v *gocui.View) error {
baseBulkCommands := []config.CustomCommand{ baseBulkCommands := []config.CustomCommand{}
{ if gui.ContainerCommand == nil || gui.ContainerCommand.Supports(commands.FeatureNetworkPrune) {
baseBulkCommands = append(baseBulkCommands, config.CustomCommand{
Name: gui.Tr.PruneNetworks, Name: gui.Tr.PruneNetworks,
InternalFunction: gui.handlePruneNetworks, InternalFunction: gui.handlePruneNetworks,
}, })
} }
bulkCommands := append(baseBulkCommands, gui.Config.UserConfig.BulkCommands.Networks...) bulkCommands := append(baseBulkCommands, gui.Config.UserConfig.BulkCommands.Networks...)
commandObject := gui.DockerCommand.NewCommandObject(commands.CommandObject{}) commandObject := gui.ContainerCommand.NewCommandObject(commands.CommandObject{})
return gui.createBulkCommandMenu(bulkCommands, commandObject) return gui.createBulkCommandMenu(bulkCommands, commandObject)
} }

View file

@ -29,7 +29,7 @@ type ISideListPanel interface {
// list panel at the side of the screen that renders content to the main panel // list panel at the side of the screen that renders content to the main panel
type SideListPanel[T comparable] struct { type SideListPanel[T comparable] struct {
ContextState *ContextState[T] ContextState *ContextState[T]
ListPanel[T] ListPanel[T]
@ -47,9 +47,14 @@ type SideListPanel[T comparable] struct {
// a callback to invoke when the item is clicked // a callback to invoke when the item is clicked
OnClick func(T) error OnClick func(T) error
// returns the cells that we render to the view in a table format. The cells will // returns the cells that we render to the view in a table format. The cells will
// be rendered with padding. // be rendered with padding.
GetTableCells func(T) []string GetTableCells func(T) []string
// optional: returns a header row to render above the list. If nil, no header
// row is rendered. When provided, the returned slice length must match
// GetTableCells(T) length.
GetTableHeaders func() []string
// function to be called after re-rendering list. Can be nil // function to be called after re-rendering list. Can be nil
OnRerender func() error OnRerender func() error
@ -222,18 +227,26 @@ func (self *SideListPanel[T]) FilterAndSort() {
} }
func (self *SideListPanel[T]) RerenderList() error { func (self *SideListPanel[T]) RerenderList() error {
self.FilterAndSort() self.FilterAndSort()
self.Gui.Update(func() error { self.Gui.Update(func() error {
self.View.Clear() self.View.Clear()
table := lo.Map(self.List.GetItems(), func(item T, index int) []string { var table [][]string
return self.GetTableCells(item) if self.GetTableHeaders != nil {
}) headers := self.GetTableHeaders()
renderedTable, err := utils.RenderTable(table) if headers != nil && len(headers) > 0 {
if err != nil { table = append(table, headers)
return err }
} }
fmt.Fprint(self.View, renderedTable) body := lo.Map(self.List.GetItems(), func(item T, index int) []string {
return self.GetTableCells(item)
})
table = append(table, body...)
renderedTable, err := utils.RenderTable(table)
if err != nil {
return err
}
fmt.Fprint(self.View, renderedTable)
if self.OnRerender != nil { if self.OnRerender != nil {
if err := self.OnRerender(); err != nil { if err := self.OnRerender(); err != nil {

View file

@ -14,15 +14,24 @@ import (
"github.com/samber/lo" "github.com/samber/lo"
) )
func GetContainerDisplayStrings(guiConfig *config.GuiConfig, container *commands.Container) []string { func GetContainerDisplayStrings(guiConfig *config.GuiConfig, container *commands.Container, runtime string) []string {
return []string{ cols := []string{
getContainerDisplayStatus(guiConfig, container), getContainerDisplayStatus(guiConfig, container),
getContainerDisplaySubstatus(guiConfig, container), getContainerDisplaySubstatus(guiConfig, container),
container.Name, container.Name,
getDisplayCPUPerc(container), getDisplayCPUPerc(container),
}
// Show ADDR for Apple runtime if available
if runtime == "apple" && container.Addr != "" {
cols = append(cols, utils.ColoredString(container.Addr, color.FgCyan))
} else {
cols = append(cols, "")
}
cols = append(cols,
utils.ColoredString(displayPorts(container), color.FgYellow), utils.ColoredString(displayPorts(container), color.FgYellow),
utils.ColoredString(displayContainerImage(container), color.FgMagenta), utils.ColoredString(displayContainerImage(container), color.FgMagenta),
} )
return cols
} }
func displayContainerImage(container *commands.Container) string { func displayContainerImage(container *commands.Container) string {

View file

@ -0,0 +1,112 @@
package presentation
import (
"testing"
"github.com/jesseduffield/lazydocker/pkg/commands"
"github.com/jesseduffield/lazydocker/pkg/config"
"github.com/stretchr/testify/assert"
dockerTypes "github.com/docker/docker/api/types"
)
func TestGetContainerDisplayStringsWithRuntime(t *testing.T) {
guiConfig := &config.GuiConfig{
ContainerStatusHealthStyle: "long",
}
container := &commands.Container{
Name: "test-container",
ID: "abc123",
Container: dockerTypes.Container{
State: "running",
Image: "nginx:latest",
Ports: []dockerTypes.Port{},
},
}
tests := []struct {
name string
runtime string
expectedIndicator string
}{
{
name: "docker runtime",
runtime: "docker",
expectedIndicator: "test-container",
},
{
name: "apple runtime",
runtime: "apple",
expectedIndicator: "test-container",
},
{
name: "unknown runtime",
runtime: "unknown",
expectedIndicator: "test-container",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := GetContainerDisplayStrings(guiConfig, container, tt.runtime)
// Check that we get the expected number of columns (status, substatus, name, cpu, addr, ports, image)
assert.Len(t, result, 7)
// Check that the container name includes the runtime indicator
assert.Equal(t, tt.expectedIndicator, result[2])
// Check other columns are populated
assert.Equal(t, "running", result[0]) // status
assert.NotEmpty(t, result[6]) // image
})
}
}
func TestGetContainerDisplayStringsWithDifferentStates(t *testing.T) {
guiConfig := &config.GuiConfig{
ContainerStatusHealthStyle: "icon",
}
tests := []struct {
name string
containerState string
expectedIcon string
}{
{
name: "running container",
containerState: "running",
expectedIcon: "▶",
},
{
name: "exited container",
containerState: "exited",
expectedIcon: "",
},
{
name: "paused container",
containerState: "paused",
expectedIcon: "◫",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
container := &commands.Container{
Name: "test-container",
ID: "abc123",
Container: dockerTypes.Container{
State: tt.containerState,
Image: "nginx:latest",
Ports: []dockerTypes.Port{},
},
}
result := GetContainerDisplayStrings(guiConfig, container, "docker")
// Check status icon
assert.Equal(t, tt.expectedIcon, result[0])
})
}
}

View file

@ -10,7 +10,6 @@ import (
"github.com/jesseduffield/gocui" "github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazydocker/pkg/commands" "github.com/jesseduffield/lazydocker/pkg/commands"
"github.com/jesseduffield/lazydocker/pkg/gui/panels" "github.com/jesseduffield/lazydocker/pkg/gui/panels"
"github.com/jesseduffield/lazydocker/pkg/gui/presentation"
"github.com/jesseduffield/lazydocker/pkg/tasks" "github.com/jesseduffield/lazydocker/pkg/tasks"
"github.com/jesseduffield/lazydocker/pkg/utils" "github.com/jesseduffield/lazydocker/pkg/utils"
"github.com/jesseduffield/yaml" "github.com/jesseduffield/yaml"
@ -23,7 +22,7 @@ func (gui *Gui) getProjectPanel() *panels.SideListPanel[*commands.Project] {
return &panels.SideListPanel[*commands.Project]{ return &panels.SideListPanel[*commands.Project]{
ContextState: &panels.ContextState[*commands.Project]{ ContextState: &panels.ContextState[*commands.Project]{
GetMainTabs: func() []panels.MainTab[*commands.Project] { GetMainTabs: func() []panels.MainTab[*commands.Project] {
if gui.DockerCommand.InDockerComposeProject { if gui.ContainerCommand.InDockerComposeProject() {
return []panels.MainTab[*commands.Project]{ return []panels.MainTab[*commands.Project]{
{ {
Key: "logs", Key: "logs",
@ -35,6 +34,16 @@ func (gui *Gui) getProjectPanel() *panels.SideListPanel[*commands.Project] {
Title: gui.Tr.DockerComposeConfigTitle, Title: gui.Tr.DockerComposeConfigTitle,
Render: gui.renderDockerComposeConfig, Render: gui.renderDockerComposeConfig,
}, },
{
Key: "runtime",
Title: "Runtime Info",
Render: gui.renderRuntimeInfo,
},
{
Key: "system",
Title: "System Status",
Render: gui.renderSystemStatus,
},
{ {
Key: "credits", Key: "credits",
Title: gui.Tr.CreditsTitle, Title: gui.Tr.CreditsTitle,
@ -49,6 +58,16 @@ func (gui *Gui) getProjectPanel() *panels.SideListPanel[*commands.Project] {
Title: gui.Tr.CreditsTitle, Title: gui.Tr.CreditsTitle,
Render: gui.renderCredits, Render: gui.renderCredits,
}, },
{
Key: "runtime",
Title: "Runtime Info",
Render: gui.renderRuntimeInfo,
},
{
Key: "system",
Title: "System Status",
Render: gui.renderSystemStatus,
},
} }
}, },
GetItemContextCacheKey: func(project *commands.Project) string { GetItemContextCacheKey: func(project *commands.Project) string {
@ -66,7 +85,15 @@ func (gui *Gui) getProjectPanel() *panels.SideListPanel[*commands.Project] {
Sort: func(a *commands.Project, b *commands.Project) bool { Sort: func(a *commands.Project, b *commands.Project) bool {
return false return false
}, },
GetTableCells: presentation.GetProjectDisplayStrings, GetTableCells: func(project *commands.Project) []string {
runtimeIndicator := ""
if gui.Config.Runtime == "apple" {
runtimeIndicator = " 🍎"
} else if gui.Config.Runtime == "docker" {
runtimeIndicator = " 🐳"
}
return []string{project.Name + runtimeIndicator}
},
// It doesn't make sense to filter a list of only one item. // It doesn't make sense to filter a list of only one item.
DisableFilter: true, DisableFilter: true,
} }
@ -79,7 +106,7 @@ func (gui *Gui) refreshProject() error {
func (gui *Gui) getProjectName() string { func (gui *Gui) getProjectName() string {
projectName := path.Base(gui.Config.ProjectDir) projectName := path.Base(gui.Config.ProjectDir)
if gui.DockerCommand.InDockerComposeProject { if gui.ContainerCommand.InDockerComposeProject() {
for _, service := range gui.Panels.Services.List.GetAllItems() { for _, service := range gui.Panels.Services.List.GetAllItems() {
container := service.Container container := service.Container
if container != nil && container.DetailsLoaded() { if container != nil && container.DetailsLoaded() {
@ -122,7 +149,7 @@ func (gui *Gui) renderAllLogs(_project *commands.Project) tasks.TaskFunc {
cmd := gui.OSCommand.RunCustomCommand( cmd := gui.OSCommand.RunCustomCommand(
utils.ApplyTemplate( utils.ApplyTemplate(
gui.Config.UserConfig.CommandTemplates.AllLogs, gui.Config.UserConfig.CommandTemplates.AllLogs,
gui.DockerCommand.NewCommandObject(commands.CommandObject{}), gui.ContainerCommand.NewCommandObject(commands.CommandObject{}),
), ),
) )
@ -146,10 +173,76 @@ func (gui *Gui) renderAllLogs(_project *commands.Project) tasks.TaskFunc {
func (gui *Gui) renderDockerComposeConfig(_project *commands.Project) tasks.TaskFunc { func (gui *Gui) renderDockerComposeConfig(_project *commands.Project) tasks.TaskFunc {
return gui.NewSimpleRenderStringTask(func() string { return gui.NewSimpleRenderStringTask(func() string {
return utils.ColoredYamlString(gui.DockerCommand.DockerComposeConfig()) return utils.ColoredYamlString(gui.ContainerCommand.DockerComposeConfig())
}) })
} }
// renderRuntimeInfo shows the current runtime and detected capabilities
func (gui *Gui) renderRuntimeInfo(_project *commands.Project) tasks.TaskFunc {
return gui.NewSimpleRenderStringTask(func() string { return gui.runtimeInfoStr() })
}
// renderSystemStatus shows runtime-specific system status (Apple runtime)
func (gui *Gui) renderSystemStatus(_project *commands.Project) tasks.TaskFunc {
return gui.NewSimpleRenderStringTask(func() string {
status, err := gui.ContainerCommand.SystemStatus()
if err != nil {
return utils.ColoredString("System status unavailable: "+err.Error(), color.FgRed)
}
if len(status) == 0 {
return "No system status available for this runtime"
}
var buf bytes.Buffer
_ = yaml.NewEncoder(&buf, yaml.IncludeOmitted).Encode(status)
return utils.ColoredYamlString(buf.String())
})
}
func (gui *Gui) runtimeInfoStr() string {
var b strings.Builder
runtimeName := gui.ContainerCommand.GetRuntimeName()
runtimeVersion := gui.ContainerCommand.GetRuntimeVersion()
b.WriteString("Runtime\n=======\n\n")
b.WriteString("Name: " + runtimeName + "\n")
b.WriteString("Info: " + runtimeVersion + "\n\n")
// Capabilities table
b.WriteString("Capabilities\n============\n\n")
rows := [][]string{{"Feature", "Supported"}}
feature := func(label string, f commands.Feature) {
ok := gui.ContainerCommand.Supports(f)
status := color.New(color.FgGreen).Sprint("yes")
if !ok {
status = color.New(color.FgRed).Sprint("no")
}
rows = append(rows, []string{label, status})
}
feature("Image History", commands.FeatureImageHistory)
feature("Image Remove", commands.FeatureImageRemove)
feature("Image Prune", commands.FeatureImagePrune)
feature("Container Attach", commands.FeatureContainerAttach)
feature("Container Exec", commands.FeatureContainerExec)
feature("Container Top", commands.FeatureContainerTop)
feature("Container Prune", commands.FeatureContainerPrune)
feature("Volume Prune", commands.FeatureVolumePrune)
feature("Network Prune", commands.FeatureNetworkPrune)
feature("Services/Compose", commands.FeatureServices)
feature("Events Stream", commands.FeatureEventsStream)
feature("Live Stats", commands.FeatureStats)
table, _ := utils.RenderTable(rows)
b.WriteString(table)
b.WriteString("\n")
b.WriteString("Notes\n=====\n")
b.WriteString("- Capabilities are detected from your local CLI (help output).\n")
b.WriteString("- If a feature appears unsupported but your CLI gained it, restart LazyDocker.\n")
return b.String()
}
func (gui *Gui) handleOpenConfig(g *gocui.Gui, v *gocui.View) error { func (gui *Gui) handleOpenConfig(g *gocui.Gui, v *gocui.View) error {
return gui.openFile(gui.Config.ConfigFilename()) return gui.openFile(gui.Config.ConfigFilename())
} }
@ -173,7 +266,7 @@ func lazydockerTitle() string {
// handleViewAllLogs switches to a subprocess viewing all the logs from docker-compose // handleViewAllLogs switches to a subprocess viewing all the logs from docker-compose
func (gui *Gui) handleViewAllLogs(g *gocui.Gui, v *gocui.View) error { func (gui *Gui) handleViewAllLogs(g *gocui.Gui, v *gocui.View) error {
c, err := gui.DockerCommand.ViewAllLogs() c, err := gui.ContainerCommand.ViewAllLogs()
if err != nil { if err != nil {
return gui.createErrorPanel(err.Error()) return gui.createErrorPanel(err.Error())
} }

View file

@ -21,33 +21,20 @@ func (gui *Gui) getServicesPanel() *panels.SideListPanel[*commands.Service] {
return &panels.SideListPanel[*commands.Service]{ return &panels.SideListPanel[*commands.Service]{
ContextState: &panels.ContextState[*commands.Service]{ ContextState: &panels.ContextState[*commands.Service]{
GetMainTabs: func() []panels.MainTab[*commands.Service] { GetMainTabs: func() []panels.MainTab[*commands.Service] {
return []panels.MainTab[*commands.Service]{ tabs := []panels.MainTab[*commands.Service]{
{ {Key: "logs", Title: gui.Tr.LogsTitle, Render: gui.renderServiceLogs},
Key: "logs",
Title: gui.Tr.LogsTitle,
Render: gui.renderServiceLogs,
},
{
Key: "stats",
Title: gui.Tr.StatsTitle,
Render: gui.renderServiceStats,
},
{
Key: "container-env",
Title: gui.Tr.ContainerEnvTitle,
Render: gui.renderServiceContainerEnv,
},
{
Key: "container-config",
Title: gui.Tr.ContainerConfigTitle,
Render: gui.renderServiceContainerConfig,
},
{
Key: "top",
Title: gui.Tr.TopTitle,
Render: gui.renderServiceTop,
},
} }
if gui.ContainerCommand.Supports(commands.FeatureStats) {
tabs = append(tabs, panels.MainTab[*commands.Service]{Key: "stats", Title: gui.Tr.StatsTitle, Render: gui.renderServiceStats})
}
tabs = append(tabs,
panels.MainTab[*commands.Service]{Key: "container-env", Title: gui.Tr.ContainerEnvTitle, Render: gui.renderServiceContainerEnv},
panels.MainTab[*commands.Service]{Key: "container-config", Title: gui.Tr.ContainerConfigTitle, Render: gui.renderServiceContainerConfig},
)
if gui.ContainerCommand.Supports(commands.FeatureContainerTop) {
tabs = append(tabs, panels.MainTab[*commands.Service]{Key: "top", Title: gui.Tr.TopTitle, Render: gui.renderServiceTop})
}
return tabs
}, },
GetItemContextCacheKey: func(service *commands.Service) string { GetItemContextCacheKey: func(service *commands.Service) string {
if service.Container == nil { if service.Container == nil {
@ -78,7 +65,7 @@ func (gui *Gui) getServicesPanel() *panels.SideListPanel[*commands.Service] {
return presentation.GetServiceDisplayStrings(&gui.Config.UserConfig.Gui, service) return presentation.GetServiceDisplayStrings(&gui.Config.UserConfig.Gui, service)
}, },
Hide: func() bool { Hide: func() bool {
return !gui.DockerCommand.InDockerComposeProject return !gui.ContainerCommand.InDockerComposeProject()
}, },
} }
} }
@ -292,7 +279,7 @@ func (gui *Gui) handleProjectUp(g *gocui.Gui, v *gocui.View) error {
return gui.createConfirmationPanel(gui.Tr.Confirm, gui.Tr.ConfirmUpProject, func(g *gocui.Gui, v *gocui.View) error { return gui.createConfirmationPanel(gui.Tr.Confirm, gui.Tr.ConfirmUpProject, func(g *gocui.Gui, v *gocui.View) error {
cmdStr := utils.ApplyTemplate( cmdStr := utils.ApplyTemplate(
gui.Config.UserConfig.CommandTemplates.Up, gui.Config.UserConfig.CommandTemplates.Up,
gui.DockerCommand.NewCommandObject(commands.CommandObject{}), gui.ContainerCommand.NewCommandObject(commands.CommandObject{}),
) )
return gui.WithWaitingStatus(gui.Tr.UppingProjectStatus, func() error { return gui.WithWaitingStatus(gui.Tr.UppingProjectStatus, func() error {
@ -307,12 +294,12 @@ func (gui *Gui) handleProjectUp(g *gocui.Gui, v *gocui.View) error {
func (gui *Gui) handleProjectDown(g *gocui.Gui, v *gocui.View) error { func (gui *Gui) handleProjectDown(g *gocui.Gui, v *gocui.View) error {
downCommand := utils.ApplyTemplate( downCommand := utils.ApplyTemplate(
gui.Config.UserConfig.CommandTemplates.Down, gui.Config.UserConfig.CommandTemplates.Down,
gui.DockerCommand.NewCommandObject(commands.CommandObject{}), gui.ContainerCommand.NewCommandObject(commands.CommandObject{}),
) )
downWithVolumesCommand := utils.ApplyTemplate( downWithVolumesCommand := utils.ApplyTemplate(
gui.Config.UserConfig.CommandTemplates.DownWithVolumes, gui.Config.UserConfig.CommandTemplates.DownWithVolumes,
gui.DockerCommand.NewCommandObject(commands.CommandObject{}), gui.ContainerCommand.NewCommandObject(commands.CommandObject{}),
) )
options := []*commandOption{ options := []*commandOption{
@ -363,12 +350,12 @@ func (gui *Gui) handleServiceRestartMenu(g *gocui.Gui, v *gocui.View) error {
rebuildCommand := utils.ApplyTemplate( rebuildCommand := utils.ApplyTemplate(
gui.Config.UserConfig.CommandTemplates.RebuildService, gui.Config.UserConfig.CommandTemplates.RebuildService,
gui.DockerCommand.NewCommandObject(commands.CommandObject{Service: service}), gui.ContainerCommand.NewCommandObject(commands.CommandObject{Service: service}),
) )
recreateCommand := utils.ApplyTemplate( recreateCommand := utils.ApplyTemplate(
gui.Config.UserConfig.CommandTemplates.RecreateService, gui.Config.UserConfig.CommandTemplates.RecreateService,
gui.DockerCommand.NewCommandObject(commands.CommandObject{Service: service}), gui.ContainerCommand.NewCommandObject(commands.CommandObject{Service: service}),
) )
options := []*commandOption{ options := []*commandOption{
@ -376,7 +363,7 @@ func (gui *Gui) handleServiceRestartMenu(g *gocui.Gui, v *gocui.View) error {
description: gui.Tr.Restart, description: gui.Tr.Restart,
command: utils.ApplyTemplate( command: utils.ApplyTemplate(
gui.Config.UserConfig.CommandTemplates.RestartService, gui.Config.UserConfig.CommandTemplates.RestartService,
gui.DockerCommand.NewCommandObject(commands.CommandObject{Service: service}), gui.ContainerCommand.NewCommandObject(commands.CommandObject{Service: service}),
), ),
onPress: func() error { onPress: func() error {
return gui.WithWaitingStatus(gui.Tr.RestartingStatus, func() error { return gui.WithWaitingStatus(gui.Tr.RestartingStatus, func() error {
@ -391,7 +378,7 @@ func (gui *Gui) handleServiceRestartMenu(g *gocui.Gui, v *gocui.View) error {
description: gui.Tr.Recreate, description: gui.Tr.Recreate,
command: utils.ApplyTemplate( command: utils.ApplyTemplate(
gui.Config.UserConfig.CommandTemplates.RecreateService, gui.Config.UserConfig.CommandTemplates.RecreateService,
gui.DockerCommand.NewCommandObject(commands.CommandObject{Service: service}), gui.ContainerCommand.NewCommandObject(commands.CommandObject{Service: service}),
), ),
onPress: func() error { onPress: func() error {
return gui.WithWaitingStatus(gui.Tr.RestartingStatus, func() error { return gui.WithWaitingStatus(gui.Tr.RestartingStatus, func() error {
@ -406,7 +393,7 @@ func (gui *Gui) handleServiceRestartMenu(g *gocui.Gui, v *gocui.View) error {
description: gui.Tr.Rebuild, description: gui.Tr.Rebuild,
command: utils.ApplyTemplate( command: utils.ApplyTemplate(
gui.Config.UserConfig.CommandTemplates.RebuildService, gui.Config.UserConfig.CommandTemplates.RebuildService,
gui.DockerCommand.NewCommandObject(commands.CommandObject{Service: service}), gui.ContainerCommand.NewCommandObject(commands.CommandObject{Service: service}),
), ),
onPress: func() error { onPress: func() error {
return gui.runSubprocess(gui.OSCommand.RunCustomCommand(rebuildCommand)) return gui.runSubprocess(gui.OSCommand.RunCustomCommand(rebuildCommand))
@ -433,7 +420,7 @@ func (gui *Gui) handleServicesCustomCommand(g *gocui.Gui, v *gocui.View) error {
return nil return nil
} }
commandObject := gui.DockerCommand.NewCommandObject(commands.CommandObject{ commandObject := gui.ContainerCommand.NewCommandObject(commands.CommandObject{
Service: service, Service: service,
Container: service.Container, Container: service.Container,
}) })
@ -466,7 +453,7 @@ L:
func (gui *Gui) handleServicesBulkCommand(g *gocui.Gui, v *gocui.View) error { func (gui *Gui) handleServicesBulkCommand(g *gocui.Gui, v *gocui.View) error {
bulkCommands := gui.Config.UserConfig.BulkCommands.Services bulkCommands := gui.Config.UserConfig.BulkCommands.Services
commandObject := gui.DockerCommand.NewCommandObject(commands.CommandObject{}) commandObject := gui.ContainerCommand.NewCommandObject(commands.CommandObject{})
return gui.createBulkCommandMenu(bulkCommands, commandObject) return gui.createBulkCommandMenu(bulkCommands, commandObject)
} }

View file

@ -128,7 +128,7 @@ func (gui *Gui) createAllViews() error {
gui.Views.Containers.Highlight = true gui.Views.Containers.Highlight = true
gui.Views.Containers.SelBgColor = selectedLineBgColor gui.Views.Containers.SelBgColor = selectedLineBgColor
if gui.Config.UserConfig.Gui.ShowAllContainers || !gui.DockerCommand.InDockerComposeProject { if gui.Config.UserConfig.Gui.ShowAllContainers || !gui.ContainerCommand.InDockerComposeProject() {
gui.Views.Containers.Title = gui.Tr.ContainersTitle gui.Views.Containers.Title = gui.Tr.ContainersTitle
} else { } else {
gui.Views.Containers.Title = gui.Tr.StandaloneContainersTitle gui.Views.Containers.Title = gui.Tr.StandaloneContainersTitle
@ -142,12 +142,13 @@ func (gui *Gui) createAllViews() error {
gui.Views.Volumes.Highlight = true gui.Views.Volumes.Highlight = true
gui.Views.Volumes.Title = gui.Tr.VolumesTitle gui.Views.Volumes.Title = gui.Tr.VolumesTitle
gui.Views.Volumes.TitlePrefix = "[5]"
gui.Views.Volumes.SelBgColor = selectedLineBgColor gui.Views.Volumes.SelBgColor = selectedLineBgColor
gui.Views.Volumes.TitlePrefix = "[5]"
gui.Views.Networks.TitlePrefix = "[6]"
gui.Views.Networks.Highlight = true gui.Views.Networks.Highlight = true
gui.Views.Networks.Title = gui.Tr.NetworksTitle gui.Views.Networks.Title = gui.Tr.NetworksTitle
gui.Views.Networks.TitlePrefix = "[6]"
gui.Views.Networks.SelBgColor = selectedLineBgColor gui.Views.Networks.SelBgColor = selectedLineBgColor
gui.Views.Options.Frame = false gui.Views.Options.Frame = false

View file

@ -2,6 +2,7 @@ package gui
import ( import (
"fmt" "fmt"
"strings"
"github.com/fatih/color" "github.com/fatih/color"
"github.com/jesseduffield/gocui" "github.com/jesseduffield/gocui"
@ -17,6 +18,10 @@ import (
func (gui *Gui) getVolumesPanel() *panels.SideListPanel[*commands.Volume] { func (gui *Gui) getVolumesPanel() *panels.SideListPanel[*commands.Volume] {
return &panels.SideListPanel[*commands.Volume]{ return &panels.SideListPanel[*commands.Volume]{
Hide: func() bool {
// Show volumes panel for both Docker and Apple runtime
return false
},
ContextState: &panels.ContextState[*commands.Volume]{ ContextState: &panels.ContextState[*commands.Volume]{
GetMainTabs: func() []panels.MainTab[*commands.Volume] { GetMainTabs: func() []panels.MainTab[*commands.Volume] {
return []panels.MainTab[*commands.Volume]{ return []panels.MainTab[*commands.Volume]{
@ -94,7 +99,7 @@ func (gui *Gui) reloadVolumes() error {
} }
func (gui *Gui) refreshStateVolumes() error { func (gui *Gui) refreshStateVolumes() error {
volumes, err := gui.DockerCommand.RefreshVolumes() volumes, err := gui.ContainerCommand.RefreshVolumes()
if err != nil { if err != nil {
return err return err
} }
@ -116,15 +121,23 @@ func (gui *Gui) handleVolumesRemoveMenu(g *gocui.Gui, v *gocui.View) error {
force bool force bool
} }
runtimeName := gui.ContainerCommand.GetRuntimeName()
rmCmd := utils.WithShortSha("docker volume rm " + volume.Name)
rmForceCmd := utils.WithShortSha("docker volume rm --force " + volume.Name)
if runtimeName == "apple" {
rmCmd = utils.WithShortSha("container volume rm " + volume.Name)
rmForceCmd = utils.WithShortSha("container volume rm --force " + volume.Name)
}
options := []*removeVolumeOption{ options := []*removeVolumeOption{
{ {
description: gui.Tr.Remove, description: gui.Tr.Remove,
command: utils.WithShortSha("docker volume rm " + volume.Name), command: rmCmd,
force: false, force: false,
}, },
{ {
description: gui.Tr.ForceRemove, description: gui.Tr.ForceRemove,
command: utils.WithShortSha("docker volume rm --force " + volume.Name), command: rmForceCmd,
force: true, force: true,
}, },
} }
@ -150,9 +163,12 @@ func (gui *Gui) handleVolumesRemoveMenu(g *gocui.Gui, v *gocui.View) error {
} }
func (gui *Gui) handlePruneVolumes() error { func (gui *Gui) handlePruneVolumes() error {
if gui.ContainerCommand != nil && !gui.ContainerCommand.Supports(commands.FeatureVolumePrune) {
return gui.createErrorPanel("Volume pruning is not supported by the current container runtime.")
}
return gui.createConfirmationPanel(gui.Tr.Confirm, gui.Tr.ConfirmPruneVolumes, func(g *gocui.Gui, v *gocui.View) error { return gui.createConfirmationPanel(gui.Tr.Confirm, gui.Tr.ConfirmPruneVolumes, func(g *gocui.Gui, v *gocui.View) error {
return gui.WithWaitingStatus(gui.Tr.PruningStatus, func() error { return gui.WithWaitingStatus(gui.Tr.PruningStatus, func() error {
err := gui.DockerCommand.PruneVolumes() err := gui.ContainerCommand.PruneVolumes()
if err != nil { if err != nil {
return gui.createErrorPanel(err.Error()) return gui.createErrorPanel(err.Error())
} }
@ -161,13 +177,45 @@ func (gui *Gui) handlePruneVolumes() error {
}, nil) }, nil)
} }
func (gui *Gui) handleCreateVolume(g *gocui.Gui, v *gocui.View) error {
if gui.ContainerCommand != nil && !gui.ContainerCommand.Supports(commands.FeatureVolumeCreate) {
return gui.createErrorPanel("Volume create is not supported by the current container runtime.")
}
prompt := "Enter: name [opt=value opt2=value2]"
return gui.createPromptPanel("Create Volume", func(g *gocui.Gui, v *gocui.View) error {
input := strings.TrimSpace(v.Buffer())
_ = gui.closeConfirmationPrompt()
if input == "" {
return nil
}
fields := strings.Fields(input)
name := fields[0]
opts := map[string]string{}
for _, tok := range fields[1:] {
if kv := strings.SplitN(tok, "=", 2); len(kv) == 2 {
opts[kv[0]] = kv[1]
} else if tok != "" {
opts[tok] = ""
}
}
if err := gui.ContainerCommand.CreateVolume(name, opts); err != nil {
return gui.createErrorPanel(err.Error())
}
return gui.reloadVolumes()
})
// write prompt after view appears
// we can't write synchronously because the view becomes editable after creation
_ = prompt
return nil
}
func (gui *Gui) handleVolumesCustomCommand(g *gocui.Gui, v *gocui.View) error { func (gui *Gui) handleVolumesCustomCommand(g *gocui.Gui, v *gocui.View) error {
volume, err := gui.Panels.Volumes.GetSelectedItem() volume, err := gui.Panels.Volumes.GetSelectedItem()
if err != nil { if err != nil {
return nil return nil
} }
commandObject := gui.DockerCommand.NewCommandObject(commands.CommandObject{ commandObject := gui.ContainerCommand.NewCommandObject(commands.CommandObject{
Volume: volume, Volume: volume,
}) })
@ -177,15 +225,16 @@ func (gui *Gui) handleVolumesCustomCommand(g *gocui.Gui, v *gocui.View) error {
} }
func (gui *Gui) handleVolumesBulkCommand(g *gocui.Gui, v *gocui.View) error { func (gui *Gui) handleVolumesBulkCommand(g *gocui.Gui, v *gocui.View) error {
baseBulkCommands := []config.CustomCommand{ baseBulkCommands := []config.CustomCommand{}
{ if gui.ContainerCommand == nil || gui.ContainerCommand.Supports(commands.FeatureVolumePrune) {
baseBulkCommands = append(baseBulkCommands, config.CustomCommand{
Name: gui.Tr.PruneVolumes, Name: gui.Tr.PruneVolumes,
InternalFunction: gui.handlePruneVolumes, InternalFunction: gui.handlePruneVolumes,
}, })
} }
bulkCommands := append(baseBulkCommands, gui.Config.UserConfig.BulkCommands.Volumes...) bulkCommands := append(baseBulkCommands, gui.Config.UserConfig.BulkCommands.Volumes...)
commandObject := gui.DockerCommand.NewCommandObject(commands.CommandObject{}) commandObject := gui.ContainerCommand.NewCommandObject(commands.CommandObject{})
return gui.createBulkCommandMenu(bulkCommands, commandObject) return gui.createBulkCommandMenu(bulkCommands, commandObject)
} }

View file

@ -24,7 +24,7 @@ type xdgDefaulter interface {
type osDefaulter struct { type osDefaulter struct {
} }
//This method is used in the testing suit // This method is used in the testing suit
// nolint: deadcode // nolint: deadcode
func setDefaulter(def xdgDefaulter) { func setDefaulter(def xdgDefaulter) {
defaulter = def defaulter = def

View file

@ -1,3 +1,4 @@
//go:build freebsd || openbsd || netbsd
// +build freebsd openbsd netbsd // +build freebsd openbsd netbsd
// Copyright (c) 2017, OpenPeeDeeP. All rights reserved. // Copyright (c) 2017, OpenPeeDeeP. All rights reserved.

View file

@ -11,18 +11,18 @@
// //
// Example with period = time.Second and trailing = false: // Example with period = time.Second and trailing = false:
// //
// Whole seconds after first trigger...|0|0|0|0|1|1|1|1| // Whole seconds after first trigger...|0|0|0|0|1|1|1|1|
// Trigger() gets called...............|X| |X| | |X| | | // Trigger() gets called...............|X| |X| | |X| | |
// Throttled code gets called..........|X| | | | |X| | | // Throttled code gets called..........|X| | | | |X| | |
// //
// Note that the second trigger had no effect. The third Trigger() caused immediate execution of the // Note that the second trigger had no effect. The third Trigger() caused immediate execution of the
// throttled code. // throttled code.
// //
// Example with period = time.Second and trailing = true: // Example with period = time.Second and trailing = true:
// //
// Whole seconds after first trigger...|0|0|0|0|1|1|1|1| // Whole seconds after first trigger...|0|0|0|0|1|1|1|1|
// Trigger() gets called...............|X| |X| | |X| | | // Trigger() gets called...............|X| |X| | |X| | |
// Throttled code gets called..........|X| | | |X| | | | // Throttled code gets called..........|X| | | |X| | | |
// //
// Note that the second Trigger() causes the throttled code to get called once the first period is over. // Note that the second Trigger() causes the throttled code to get called once the first period is over.
// The third Trigger() will do the same. // The third Trigger() will do the same.

View file

@ -1,3 +1,4 @@
//go:build darwin || freebsd || linux || netbsd || openbsd
// +build darwin freebsd linux netbsd openbsd // +build darwin freebsd linux netbsd openbsd
package jibber_jabber package jibber_jabber

View file

@ -1,3 +1,4 @@
//go:build windows
// +build windows // +build windows
package jibber_jabber package jibber_jabber

View file

@ -18,6 +18,7 @@
// tag is deprecated and thus should not be used. // tag is deprecated and thus should not be used.
// Go versions prior to 1.4 are disabled because they use a different layout // Go versions prior to 1.4 are disabled because they use a different layout
// for interfaces which make the implementation of unsafeReflectValue more complex. // for interfaces which make the implementation of unsafeReflectValue more complex.
//go:build !js && !appengine && !safe && !disableunsafe && go1.4
// +build !js,!appengine,!safe,!disableunsafe,go1.4 // +build !js,!appengine,!safe,!disableunsafe,go1.4
package spew package spew

View file

@ -16,6 +16,7 @@
// when the code is running on Google App Engine, compiled by GopherJS, or // when the code is running on Google App Engine, compiled by GopherJS, or
// "-tags safe" is added to the go build command line. The "disableunsafe" // "-tags safe" is added to the go build command line. The "disableunsafe"
// tag is deprecated and thus should not be used. // tag is deprecated and thus should not be used.
//go:build js || appengine || safe || disableunsafe || !go1.4
// +build js appengine safe disableunsafe !go1.4 // +build js appengine safe disableunsafe !go1.4
package spew package spew

View file

@ -254,15 +254,15 @@ pointer addresses used to indirect to the final value. It provides the
following features over the built-in printing facilities provided by the fmt following features over the built-in printing facilities provided by the fmt
package: package:
* Pointers are dereferenced and followed - Pointers are dereferenced and followed
* Circular data structures are detected and handled properly - Circular data structures are detected and handled properly
* Custom Stringer/error interfaces are optionally invoked, including - Custom Stringer/error interfaces are optionally invoked, including
on unexported types on unexported types
* Custom types which only implement the Stringer/error interfaces via - Custom types which only implement the Stringer/error interfaces via
a pointer receiver are optionally invoked when passing non-pointer a pointer receiver are optionally invoked when passing non-pointer
variables variables
* Byte arrays and slices are dumped like the hexdump -C command which - Byte arrays and slices are dumped like the hexdump -C command which
includes offsets, byte values in hex, and ASCII output includes offsets, byte values in hex, and ASCII output
The configuration options are controlled by modifying the public members The configuration options are controlled by modifying the public members
of c. See ConfigState for options documentation. of c. See ConfigState for options documentation.
@ -295,12 +295,12 @@ func (c *ConfigState) convertArgs(args []interface{}) (formatters []interface{})
// NewDefaultConfig returns a ConfigState with the following default settings. // NewDefaultConfig returns a ConfigState with the following default settings.
// //
// Indent: " " // Indent: " "
// MaxDepth: 0 // MaxDepth: 0
// DisableMethods: false // DisableMethods: false
// DisablePointerMethods: false // DisablePointerMethods: false
// ContinueOnMethod: false // ContinueOnMethod: false
// SortKeys: false // SortKeys: false
func NewDefaultConfig() *ConfigState { func NewDefaultConfig() *ConfigState {
return &ConfigState{Indent: " "} return &ConfigState{Indent: " "}
} }

View file

@ -21,35 +21,36 @@ debugging.
A quick overview of the additional features spew provides over the built-in A quick overview of the additional features spew provides over the built-in
printing facilities for Go data types are as follows: printing facilities for Go data types are as follows:
* Pointers are dereferenced and followed - Pointers are dereferenced and followed
* Circular data structures are detected and handled properly - Circular data structures are detected and handled properly
* Custom Stringer/error interfaces are optionally invoked, including - Custom Stringer/error interfaces are optionally invoked, including
on unexported types on unexported types
* Custom types which only implement the Stringer/error interfaces via - Custom types which only implement the Stringer/error interfaces via
a pointer receiver are optionally invoked when passing non-pointer a pointer receiver are optionally invoked when passing non-pointer
variables variables
* Byte arrays and slices are dumped like the hexdump -C command which - Byte arrays and slices are dumped like the hexdump -C command which
includes offsets, byte values in hex, and ASCII output (only when using includes offsets, byte values in hex, and ASCII output (only when using
Dump style) Dump style)
There are two different approaches spew allows for dumping Go data structures: There are two different approaches spew allows for dumping Go data structures:
* Dump style which prints with newlines, customizable indentation, - Dump style which prints with newlines, customizable indentation,
and additional debug information such as types and all pointer addresses and additional debug information such as types and all pointer addresses
used to indirect to the final value used to indirect to the final value
* A custom Formatter interface that integrates cleanly with the standard fmt - A custom Formatter interface that integrates cleanly with the standard fmt
package and replaces %v, %+v, %#v, and %#+v to provide inline printing package and replaces %v, %+v, %#v, and %#+v to provide inline printing
similar to the default %v while providing the additional functionality similar to the default %v while providing the additional functionality
outlined above and passing unsupported format verbs such as %x and %q outlined above and passing unsupported format verbs such as %x and %q
along to fmt along to fmt
Quick Start # Quick Start
This section demonstrates how to quickly get started with spew. See the This section demonstrates how to quickly get started with spew. See the
sections below for further details on formatting and configuration options. sections below for further details on formatting and configuration options.
To dump a variable with full newlines, indentation, type, and pointer To dump a variable with full newlines, indentation, type, and pointer
information use Dump, Fdump, or Sdump: information use Dump, Fdump, or Sdump:
spew.Dump(myVar1, myVar2, ...) spew.Dump(myVar1, myVar2, ...)
spew.Fdump(someWriter, myVar1, myVar2, ...) spew.Fdump(someWriter, myVar1, myVar2, ...)
str := spew.Sdump(myVar1, myVar2, ...) str := spew.Sdump(myVar1, myVar2, ...)
@ -58,12 +59,13 @@ Alternatively, if you would prefer to use format strings with a compacted inline
printing style, use the convenience wrappers Printf, Fprintf, etc with printing style, use the convenience wrappers Printf, Fprintf, etc with
%v (most compact), %+v (adds pointer addresses), %#v (adds types), or %v (most compact), %+v (adds pointer addresses), %#v (adds types), or
%#+v (adds types and pointer addresses): %#+v (adds types and pointer addresses):
spew.Printf("myVar1: %v -- myVar2: %+v", myVar1, myVar2) spew.Printf("myVar1: %v -- myVar2: %+v", myVar1, myVar2)
spew.Printf("myVar3: %#v -- myVar4: %#+v", myVar3, myVar4) spew.Printf("myVar3: %#v -- myVar4: %#+v", myVar3, myVar4)
spew.Fprintf(someWriter, "myVar1: %v -- myVar2: %+v", myVar1, myVar2) spew.Fprintf(someWriter, "myVar1: %v -- myVar2: %+v", myVar1, myVar2)
spew.Fprintf(someWriter, "myVar3: %#v -- myVar4: %#+v", myVar3, myVar4) spew.Fprintf(someWriter, "myVar3: %#v -- myVar4: %#+v", myVar3, myVar4)
Configuration Options # Configuration Options
Configuration of spew is handled by fields in the ConfigState type. For Configuration of spew is handled by fields in the ConfigState type. For
convenience, all of the top-level functions use a global state available convenience, all of the top-level functions use a global state available
@ -74,51 +76,52 @@ equivalent to the top-level functions. This allows concurrent configuration
options. See the ConfigState documentation for more details. options. See the ConfigState documentation for more details.
The following configuration options are available: The following configuration options are available:
* Indent
String to use for each indentation level for Dump functions.
It is a single space by default. A popular alternative is "\t".
* MaxDepth - Indent
Maximum number of levels to descend into nested data structures. String to use for each indentation level for Dump functions.
There is no limit by default. It is a single space by default. A popular alternative is "\t".
* DisableMethods - MaxDepth
Disables invocation of error and Stringer interface methods. Maximum number of levels to descend into nested data structures.
Method invocation is enabled by default. There is no limit by default.
* DisablePointerMethods - DisableMethods
Disables invocation of error and Stringer interface methods on types Disables invocation of error and Stringer interface methods.
which only accept pointer receivers from non-pointer variables. Method invocation is enabled by default.
Pointer method invocation is enabled by default.
* DisablePointerAddresses - DisablePointerMethods
DisablePointerAddresses specifies whether to disable the printing of Disables invocation of error and Stringer interface methods on types
pointer addresses. This is useful when diffing data structures in tests. which only accept pointer receivers from non-pointer variables.
Pointer method invocation is enabled by default.
* DisableCapacities - DisablePointerAddresses
DisableCapacities specifies whether to disable the printing of DisablePointerAddresses specifies whether to disable the printing of
capacities for arrays, slices, maps and channels. This is useful when pointer addresses. This is useful when diffing data structures in tests.
diffing data structures in tests.
* ContinueOnMethod - DisableCapacities
Enables recursion into types after invoking error and Stringer interface DisableCapacities specifies whether to disable the printing of
methods. Recursion after method invocation is disabled by default. capacities for arrays, slices, maps and channels. This is useful when
diffing data structures in tests.
* SortKeys - ContinueOnMethod
Specifies map keys should be sorted before being printed. Use Enables recursion into types after invoking error and Stringer interface
this to have a more deterministic, diffable output. Note that methods. Recursion after method invocation is disabled by default.
only native types (bool, int, uint, floats, uintptr and string)
and types which implement error or Stringer interfaces are
supported with other types sorted according to the
reflect.Value.String() output which guarantees display
stability. Natural map order is used by default.
* SpewKeys - SortKeys
Specifies that, as a last resort attempt, map keys should be Specifies map keys should be sorted before being printed. Use
spewed to strings and sorted by those strings. This is only this to have a more deterministic, diffable output. Note that
considered if SortKeys is true. only native types (bool, int, uint, floats, uintptr and string)
and types which implement error or Stringer interfaces are
supported with other types sorted according to the
reflect.Value.String() output which guarantees display
stability. Natural map order is used by default.
Dump Usage - SpewKeys
Specifies that, as a last resort attempt, map keys should be
spewed to strings and sorted by those strings. This is only
considered if SortKeys is true.
# Dump Usage
Simply call spew.Dump with a list of variables you want to dump: Simply call spew.Dump with a list of variables you want to dump:
@ -133,7 +136,7 @@ A third option is to call spew.Sdump to get the formatted output as a string:
str := spew.Sdump(myVar1, myVar2, ...) str := spew.Sdump(myVar1, myVar2, ...)
Sample Dump Output # Sample Dump Output
See the Dump example for details on the setup of the types and variables being See the Dump example for details on the setup of the types and variables being
shown here. shown here.
@ -150,13 +153,14 @@ shown here.
Byte (and uint8) arrays and slices are displayed uniquely like the hexdump -C Byte (and uint8) arrays and slices are displayed uniquely like the hexdump -C
command as shown. command as shown.
([]uint8) (len=32 cap=32) { ([]uint8) (len=32 cap=32) {
00000000 11 12 13 14 15 16 17 18 19 1a 1b 1c 1d 1e 1f 20 |............... | 00000000 11 12 13 14 15 16 17 18 19 1a 1b 1c 1d 1e 1f 20 |............... |
00000010 21 22 23 24 25 26 27 28 29 2a 2b 2c 2d 2e 2f 30 |!"#$%&'()*+,-./0| 00000010 21 22 23 24 25 26 27 28 29 2a 2b 2c 2d 2e 2f 30 |!"#$%&'()*+,-./0|
00000020 31 32 |12| 00000020 31 32 |12|
} }
Custom Formatter # Custom Formatter
Spew provides a custom formatter that implements the fmt.Formatter interface Spew provides a custom formatter that implements the fmt.Formatter interface
so that it integrates cleanly with standard fmt package printing functions. The so that it integrates cleanly with standard fmt package printing functions. The
@ -170,7 +174,7 @@ standard fmt package for formatting. In addition, the custom formatter ignores
the width and precision arguments (however they will still work on the format the width and precision arguments (however they will still work on the format
specifiers not handled by the custom formatter). specifiers not handled by the custom formatter).
Custom Formatter Usage # Custom Formatter Usage
The simplest way to make use of the spew custom formatter is to call one of the The simplest way to make use of the spew custom formatter is to call one of the
convenience functions such as spew.Printf, spew.Println, or spew.Printf. The convenience functions such as spew.Printf, spew.Println, or spew.Printf. The
@ -184,15 +188,17 @@ functions have syntax you are most likely already familiar with:
See the Index for the full list convenience functions. See the Index for the full list convenience functions.
Sample Formatter Output # Sample Formatter Output
Double pointer to a uint8: Double pointer to a uint8:
%v: <**>5 %v: <**>5
%+v: <**>(0xf8400420d0->0xf8400420c8)5 %+v: <**>(0xf8400420d0->0xf8400420c8)5
%#v: (**uint8)5 %#v: (**uint8)5
%#+v: (**uint8)(0xf8400420d0->0xf8400420c8)5 %#+v: (**uint8)(0xf8400420d0->0xf8400420c8)5
Pointer to circular struct with a uint8 field and a pointer to itself: Pointer to circular struct with a uint8 field and a pointer to itself:
%v: <*>{1 <*><shown>} %v: <*>{1 <*><shown>}
%+v: <*>(0xf84003e260){ui8:1 c:<*>(0xf84003e260)<shown>} %+v: <*>(0xf84003e260){ui8:1 c:<*>(0xf84003e260)<shown>}
%#v: (*main.circular){ui8:(uint8)1 c:(*main.circular)<shown>} %#v: (*main.circular){ui8:(uint8)1 c:(*main.circular)<shown>}
@ -201,7 +207,7 @@ Pointer to circular struct with a uint8 field and a pointer to itself:
See the Printf example for details on the setup of variables being shown See the Printf example for details on the setup of variables being shown
here. here.
Errors # Errors
Since it is possible for custom Stringer/error interfaces to panic, spew Since it is possible for custom Stringer/error interfaces to panic, spew
detects them and handles them internally by printing the panic information detects them and handles them internally by printing the panic information

View file

@ -488,15 +488,15 @@ pointer addresses used to indirect to the final value. It provides the
following features over the built-in printing facilities provided by the fmt following features over the built-in printing facilities provided by the fmt
package: package:
* Pointers are dereferenced and followed - Pointers are dereferenced and followed
* Circular data structures are detected and handled properly - Circular data structures are detected and handled properly
* Custom Stringer/error interfaces are optionally invoked, including - Custom Stringer/error interfaces are optionally invoked, including
on unexported types on unexported types
* Custom types which only implement the Stringer/error interfaces via - Custom types which only implement the Stringer/error interfaces via
a pointer receiver are optionally invoked when passing non-pointer a pointer receiver are optionally invoked when passing non-pointer
variables variables
* Byte arrays and slices are dumped like the hexdump -C command which - Byte arrays and slices are dumped like the hexdump -C command which
includes offsets, byte values in hex, and ASCII output includes offsets, byte values in hex, and ASCII output
The configuration options are controlled by an exported package global, The configuration options are controlled by an exported package global,
spew.Config. See ConfigState for options documentation. spew.Config. See ConfigState for options documentation.

133
vendor/github.com/fatih/color/doc.go generated vendored
View file

@ -5,106 +5,105 @@ that suits you.
Use simple and default helper functions with predefined foreground colors: Use simple and default helper functions with predefined foreground colors:
color.Cyan("Prints text in cyan.") color.Cyan("Prints text in cyan.")
// a newline will be appended automatically // a newline will be appended automatically
color.Blue("Prints %s in blue.", "text") color.Blue("Prints %s in blue.", "text")
// More default foreground colors.. // More default foreground colors..
color.Red("We have red") color.Red("We have red")
color.Yellow("Yellow color too!") color.Yellow("Yellow color too!")
color.Magenta("And many others ..") color.Magenta("And many others ..")
// Hi-intensity colors // Hi-intensity colors
color.HiGreen("Bright green color.") color.HiGreen("Bright green color.")
color.HiBlack("Bright black means gray..") color.HiBlack("Bright black means gray..")
color.HiWhite("Shiny white color!") color.HiWhite("Shiny white color!")
However there are times where custom color mixes are required. Below are some However there are times where custom color mixes are required. Below are some
examples to create custom color objects and use the print functions of each examples to create custom color objects and use the print functions of each
separate color object. separate color object.
// Create a new color object // Create a new color object
c := color.New(color.FgCyan).Add(color.Underline) c := color.New(color.FgCyan).Add(color.Underline)
c.Println("Prints cyan text with an underline.") c.Println("Prints cyan text with an underline.")
// Or just add them to New() // Or just add them to New()
d := color.New(color.FgCyan, color.Bold) d := color.New(color.FgCyan, color.Bold)
d.Printf("This prints bold cyan %s\n", "too!.") d.Printf("This prints bold cyan %s\n", "too!.")
// Mix up foreground and background colors, create new mixes! // Mix up foreground and background colors, create new mixes!
red := color.New(color.FgRed) red := color.New(color.FgRed)
boldRed := red.Add(color.Bold) boldRed := red.Add(color.Bold)
boldRed.Println("This will print text in bold red.") boldRed.Println("This will print text in bold red.")
whiteBackground := red.Add(color.BgWhite) whiteBackground := red.Add(color.BgWhite)
whiteBackground.Println("Red text with White background.") whiteBackground.Println("Red text with White background.")
// Use your own io.Writer output // Use your own io.Writer output
color.New(color.FgBlue).Fprintln(myWriter, "blue color!") color.New(color.FgBlue).Fprintln(myWriter, "blue color!")
blue := color.New(color.FgBlue) blue := color.New(color.FgBlue)
blue.Fprint(myWriter, "This will print text in blue.") blue.Fprint(myWriter, "This will print text in blue.")
You can create PrintXxx functions to simplify even more: You can create PrintXxx functions to simplify even more:
// Create a custom print function for convenient // Create a custom print function for convenient
red := color.New(color.FgRed).PrintfFunc() red := color.New(color.FgRed).PrintfFunc()
red("warning") red("warning")
red("error: %s", err) red("error: %s", err)
// Mix up multiple attributes // Mix up multiple attributes
notice := color.New(color.Bold, color.FgGreen).PrintlnFunc() notice := color.New(color.Bold, color.FgGreen).PrintlnFunc()
notice("don't forget this...") notice("don't forget this...")
You can also FprintXxx functions to pass your own io.Writer: You can also FprintXxx functions to pass your own io.Writer:
blue := color.New(FgBlue).FprintfFunc() blue := color.New(FgBlue).FprintfFunc()
blue(myWriter, "important notice: %s", stars) blue(myWriter, "important notice: %s", stars)
// Mix up with multiple attributes
success := color.New(color.Bold, color.FgGreen).FprintlnFunc()
success(myWriter, don't forget this...")
// Mix up with multiple attributes
success := color.New(color.Bold, color.FgGreen).FprintlnFunc()
success(myWriter, don't forget this...")
Or create SprintXxx functions to mix strings with other non-colorized strings: Or create SprintXxx functions to mix strings with other non-colorized strings:
yellow := New(FgYellow).SprintFunc() yellow := New(FgYellow).SprintFunc()
red := New(FgRed).SprintFunc() red := New(FgRed).SprintFunc()
fmt.Printf("this is a %s and this is %s.\n", yellow("warning"), red("error")) fmt.Printf("this is a %s and this is %s.\n", yellow("warning"), red("error"))
info := New(FgWhite, BgGreen).SprintFunc() info := New(FgWhite, BgGreen).SprintFunc()
fmt.Printf("this %s rocks!\n", info("package")) fmt.Printf("this %s rocks!\n", info("package"))
Windows support is enabled by default. All Print functions work as intended. Windows support is enabled by default. All Print functions work as intended.
However only for color.SprintXXX functions, user should use fmt.FprintXXX and However only for color.SprintXXX functions, user should use fmt.FprintXXX and
set the output to color.Output: set the output to color.Output:
fmt.Fprintf(color.Output, "Windows support: %s", color.GreenString("PASS")) fmt.Fprintf(color.Output, "Windows support: %s", color.GreenString("PASS"))
info := New(FgWhite, BgGreen).SprintFunc() info := New(FgWhite, BgGreen).SprintFunc()
fmt.Fprintf(color.Output, "this %s rocks!\n", info("package")) fmt.Fprintf(color.Output, "this %s rocks!\n", info("package"))
Using with existing code is possible. Just use the Set() method to set the Using with existing code is possible. Just use the Set() method to set the
standard output to the given parameters. That way a rewrite of an existing standard output to the given parameters. That way a rewrite of an existing
code is not required. code is not required.
// Use handy standard colors. // Use handy standard colors.
color.Set(color.FgYellow) color.Set(color.FgYellow)
fmt.Println("Existing text will be now in Yellow") fmt.Println("Existing text will be now in Yellow")
fmt.Printf("This one %s\n", "too") fmt.Printf("This one %s\n", "too")
color.Unset() // don't forget to unset color.Unset() // don't forget to unset
// You can mix up parameters // You can mix up parameters
color.Set(color.FgMagenta, color.Bold) color.Set(color.FgMagenta, color.Bold)
defer color.Unset() // use it in your function defer color.Unset() // use it in your function
fmt.Println("All text will be now bold magenta.") fmt.Println("All text will be now bold magenta.")
There might be a case where you want to disable color output (for example to There might be a case where you want to disable color output (for example to
pipe the standard output of your app to somewhere else). `Color` has support to pipe the standard output of your app to somewhere else). `Color` has support to
@ -112,22 +111,22 @@ disable colors both globally and for single color definition. For example
suppose you have a CLI app and a `--no-color` bool flag. You can easily disable suppose you have a CLI app and a `--no-color` bool flag. You can easily disable
the color output with: the color output with:
var flagNoColor = flag.Bool("no-color", false, "Disable color output") var flagNoColor = flag.Bool("no-color", false, "Disable color output")
if *flagNoColor { if *flagNoColor {
color.NoColor = true // disables colorized output color.NoColor = true // disables colorized output
} }
It also has support for single color definitions (local). You can It also has support for single color definitions (local). You can
disable/enable color output on the fly: disable/enable color output on the fly:
c := color.New(color.FgCyan) c := color.New(color.FgCyan)
c.Println("Prints cyan text") c.Println("Prints cyan text")
c.DisableColor() c.DisableColor()
c.Println("This is printed without any color") c.Println("This is printed without any color")
c.EnableColor() c.EnableColor()
c.Println("This prints again cyan...") c.Println("This prints again cyan...")
*/ */
package color package color

View file

@ -1,4 +1,6 @@
//go:build go1.8
// +build go1.8 // +build go1.8
// Code generated by "httpsnoop/codegen"; DO NOT EDIT. // Code generated by "httpsnoop/codegen"; DO NOT EDIT.
package httpsnoop package httpsnoop

View file

@ -1,4 +1,6 @@
//go:build !go1.8
// +build !go1.8 // +build !go1.8
// Code generated by "httpsnoop/codegen"; DO NOT EDIT. // Code generated by "httpsnoop/codegen"; DO NOT EDIT.
package httpsnoop package httpsnoop

View file

@ -9,36 +9,36 @@
// //
// For example: // For example:
// //
// package crashy // package crashy
// //
// import "github.com/go-errors/errors" // import "github.com/go-errors/errors"
// //
// var Crashed = errors.Errorf("oh dear") // var Crashed = errors.Errorf("oh dear")
// //
// func Crash() error { // func Crash() error {
// return errors.New(Crashed) // return errors.New(Crashed)
// } // }
// //
// This can be called as follows: // This can be called as follows:
// //
// package main // package main
// //
// import ( // import (
// "crashy" // "crashy"
// "fmt" // "fmt"
// "github.com/go-errors/errors" // "github.com/go-errors/errors"
// ) // )
// //
// func main() { // func main() {
// err := crashy.Crash() // err := crashy.Crash()
// if err != nil { // if err != nil {
// if errors.Is(err, crashy.Crashed) { // if errors.Is(err, crashy.Crashed) {
// fmt.Println(err.(*errors.Error).ErrorStack()) // fmt.Println(err.(*errors.Error).ErrorStack())
// } else { // } else {
// panic(err) // panic(err)
// } // }
// } // }
// } // }
// //
// This package was original written to allow reporting to Bugsnag, // This package was original written to allow reporting to Bugsnag,
// but after I found similar packages by Facebook and Dropbox, it // but after I found similar packages by Facebook and Dropbox, it

View file

@ -75,8 +75,8 @@ func ParsePanic(text string) (*Error, error) {
// The lines we're passing look like this: // The lines we're passing look like this:
// //
// main.(*foo).destruct(0xc208067e98) // main.(*foo).destruct(0xc208067e98)
// /0/go/src/github.com/bugsnag/bugsnag-go/pan/main.go:22 +0x151 // /0/go/src/github.com/bugsnag/bugsnag-go/pan/main.go:22 +0x151
func parsePanicFrame(name string, line string, createdBy bool) (*StackFrame, error) { func parsePanicFrame(name string, line string, createdBy bool) (*StackFrame, error) {
idx := strings.LastIndex(name, "(") idx := strings.LastIndex(name, "(")
if idx == -1 && !createdBy { if idx == -1 && !createdBy {

View file

@ -89,43 +89,42 @@ func (s MapSlice) ToMap() map[interface{}]interface{} {
// //
// The field tag format accepted is: // The field tag format accepted is:
// //
// `(...) yaml:"[<key>][,<flag1>[,<flag2>]]" (...)` // `(...) yaml:"[<key>][,<flag1>[,<flag2>]]" (...)`
// //
// The following flags are currently supported: // The following flags are currently supported:
// //
// omitempty Only include the field if it's not set to the zero // omitempty Only include the field if it's not set to the zero
// value for the type or to empty slices or maps. // value for the type or to empty slices or maps.
// Zero valued structs will be omitted if all their public // Zero valued structs will be omitted if all their public
// fields are zero, unless they implement an IsZero // fields are zero, unless they implement an IsZero
// method (see the IsZeroer interface type), in which // method (see the IsZeroer interface type), in which
// case the field will be included if that method returns true. // case the field will be included if that method returns true.
// //
// flow Marshal using a flow style (useful for structs, // flow Marshal using a flow style (useful for structs,
// sequences and maps). // sequences and maps).
// //
// inline Inline the field, which must be a struct or a map, // inline Inline the field, which must be a struct or a map,
// causing all of its fields or keys to be processed as if // causing all of its fields or keys to be processed as if
// they were part of the outer struct. For maps, keys must // they were part of the outer struct. For maps, keys must
// not conflict with the yaml keys of other struct fields. // not conflict with the yaml keys of other struct fields.
// //
// anchor Marshal with anchor. If want to define anchor name explicitly, use anchor=name style. // anchor Marshal with anchor. If want to define anchor name explicitly, use anchor=name style.
// Otherwise, if used 'anchor' name only, used the field name lowercased as the anchor name // Otherwise, if used 'anchor' name only, used the field name lowercased as the anchor name
// //
// alias Marshal with alias. If want to define alias name explicitly, use alias=name style. // alias Marshal with alias. If want to define alias name explicitly, use alias=name style.
// Otherwise, If omitted alias name and the field type is pointer type, // Otherwise, If omitted alias name and the field type is pointer type,
// assigned anchor name automatically from same pointer address. // assigned anchor name automatically from same pointer address.
// //
// In addition, if the key is "-", the field is ignored. // In addition, if the key is "-", the field is ignored.
// //
// For example: // For example:
// //
// type T struct { // type T struct {
// F int `yaml:"a,omitempty"` // F int `yaml:"a,omitempty"`
// B int // B int
// } // }
// yaml.Marshal(&T{B: 2}) // Returns "b: 2\n" // yaml.Marshal(&T{B: 2}) // Returns "b: 2\n"
// yaml.Marshal(&T{F: 1}) // Returns "a: 1\nb: 0\n" // yaml.Marshal(&T{F: 1}) // Returns "a: 1\nb: 0\n"
//
func Marshal(v interface{}) ([]byte, error) { func Marshal(v interface{}) ([]byte, error) {
return MarshalWithOptions(v) return MarshalWithOptions(v)
} }
@ -167,16 +166,15 @@ func ValueToNode(v interface{}, opts ...EncodeOption) (ast.Node, error) {
// //
// For example: // For example:
// //
// type T struct { // type T struct {
// F int `yaml:"a,omitempty"` // F int `yaml:"a,omitempty"`
// B int // B int
// } // }
// var t T // var t T
// yaml.Unmarshal([]byte("a: 1\nb: 2"), &t) // yaml.Unmarshal([]byte("a: 1\nb: 2"), &t)
// //
// See the documentation of Marshal for the format of tags and a list of // See the documentation of Marshal for the format of tags and a list of
// supported tag options. // supported tag options.
//
func Unmarshal(data []byte, v interface{}) error { func Unmarshal(data []byte, v interface{}) error {
return UnmarshalWithOptions(data, v) return UnmarshalWithOptions(data, v)
} }

View file

@ -39,35 +39,35 @@ for a protocol buffer variable v:
- Names are turned from camel_case to CamelCase for export. - Names are turned from camel_case to CamelCase for export.
- There are no methods on v to set fields; just treat - There are no methods on v to set fields; just treat
them as structure fields. them as structure fields.
- There are getters that return a field's value if set, - There are getters that return a field's value if set,
and return the field's default value if unset. and return the field's default value if unset.
The getters work even if the receiver is a nil message. The getters work even if the receiver is a nil message.
- The zero value for a struct is its correct initialization state. - The zero value for a struct is its correct initialization state.
All desired fields must be set before marshaling. All desired fields must be set before marshaling.
- A Reset() method will restore a protobuf struct to its zero state. - A Reset() method will restore a protobuf struct to its zero state.
- Non-repeated fields are pointers to the values; nil means unset. - Non-repeated fields are pointers to the values; nil means unset.
That is, optional or required field int32 f becomes F *int32. That is, optional or required field int32 f becomes F *int32.
- Repeated fields are slices. - Repeated fields are slices.
- Helper functions are available to aid the setting of fields. - Helper functions are available to aid the setting of fields.
msg.Foo = proto.String("hello") // set field msg.Foo = proto.String("hello") // set field
- Constants are defined to hold the default values of all fields that - Constants are defined to hold the default values of all fields that
have them. They have the form Default_StructName_FieldName. have them. They have the form Default_StructName_FieldName.
Because the getter methods handle defaulted values, Because the getter methods handle defaulted values,
direct use of these constants should be rare. direct use of these constants should be rare.
- Enums are given type names and maps from names to values. - Enums are given type names and maps from names to values.
Enum values are prefixed by the enclosing message's name, or by the Enum values are prefixed by the enclosing message's name, or by the
enum's type name if it is a top-level enum. Enum types have a String enum's type name if it is a top-level enum. Enum types have a String
method, and a Enum method to assist in message construction. method, and a Enum method to assist in message construction.
- Nested messages, groups and enums have type names prefixed with the name of - Nested messages, groups and enums have type names prefixed with the name of
the surrounding message type. the surrounding message type.
- Extensions are given descriptor names that start with E_, - Extensions are given descriptor names that start with E_,
followed by an underscore-delimited list of the nested messages followed by an underscore-delimited list of the nested messages
that contain it (if any) followed by the CamelCased name of the that contain it (if any) followed by the CamelCased name of the
extension field itself. HasExtension, ClearExtension, GetExtension extension field itself. HasExtension, ClearExtension, GetExtension
and SetExtension are functions for manipulating extensions. and SetExtension are functions for manipulating extensions.
- Oneof field sets are given a single field in their message, - Oneof field sets are given a single field in their message,
with distinguished wrapper types for each possible field value. with distinguished wrapper types for each possible field value.
- Marshal and Unmarshal are functions to encode and decode the wire format. - Marshal and Unmarshal are functions to encode and decode the wire format.
When the .proto file specifies `syntax="proto3"`, there are some differences: When the .proto file specifies `syntax="proto3"`, there are some differences:

View file

@ -29,6 +29,7 @@
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
//go:build purego || appengine || js
// +build purego appengine js // +build purego appengine js
// This file contains an implementation of proto field accesses using package reflect. // This file contains an implementation of proto field accesses using package reflect.

View file

@ -26,6 +26,7 @@
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
//go:build purego || appengine || js
// +build purego appengine js // +build purego appengine js
// This file contains an implementation of proto field accesses using package reflect. // This file contains an implementation of proto field accesses using package reflect.

View file

@ -29,6 +29,7 @@
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
//go:build !purego && !appengine && !js
// +build !purego,!appengine,!js // +build !purego,!appengine,!js
// This file contains the implementation of the proto field accesses using package unsafe. // This file contains the implementation of the proto field accesses using package unsafe.

View file

@ -26,6 +26,7 @@
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
//go:build !purego && !appengine && !js
// +build !purego,!appengine,!js // +build !purego,!appengine,!js
// This file contains the implementation of the proto field accesses using package unsafe. // This file contains the implementation of the proto field accesses using package unsafe.

View file

@ -2004,12 +2004,14 @@ func makeUnmarshalMap(f *reflect.StructField) unmarshaler {
// makeUnmarshalOneof makes an unmarshaler for oneof fields. // makeUnmarshalOneof makes an unmarshaler for oneof fields.
// for: // for:
// message Msg { //
// oneof F { // message Msg {
// int64 X = 1; // oneof F {
// float64 Y = 2; // int64 X = 1;
// } // float64 Y = 2;
// } // }
// }
//
// typ is the type of the concrete entry for a oneof case (e.g. Msg_X). // typ is the type of the concrete entry for a oneof case (e.g. Msg_X).
// ityp is the interface type of the oneof field (e.g. isMsg_F). // ityp is the interface type of the oneof field (e.g. isMsg_F).
// unmarshal is the unmarshaler for the base type of the oneof case (e.g. int64). // unmarshal is the unmarshaler for the base type of the oneof case (e.g. int64).

View file

@ -29,7 +29,8 @@ const (
// color render templates // color render templates
// ESC 操作的表示: // ESC 操作的表示:
// "\033"(Octal 8进制) = "\x1b"(Hexadecimal 16进制) = 27 (10进制) //
// "\033"(Octal 8进制) = "\x1b"(Hexadecimal 16进制) = 27 (10进制)
const ( const (
SettingTpl = "\x1b[%sm" SettingTpl = "\x1b[%sm"
FullColorTpl = "\x1b[%sm%s\x1b[0m" FullColorTpl = "\x1b[%sm%s\x1b[0m"
@ -179,7 +180,8 @@ func InnerErrs() []error {
// RenderCode render message by color code. // RenderCode render message by color code.
// Usage: // Usage:
// msg := RenderCode("3;32;45", "some", "message") //
// msg := RenderCode("3;32;45", "some", "message")
func RenderCode(code string, args ...interface{}) string { func RenderCode(code string, args ...interface{}) string {
var message string var message string
if ln := len(args); ln == 0 { if ln := len(args); ln == 0 {
@ -217,7 +219,8 @@ func RenderWithSpaces(code string, args ...interface{}) string {
// RenderString render a string with color code. // RenderString render a string with color code.
// Usage: // Usage:
// msg := RenderString("3;32;45", "a message") //
// msg := RenderString("3;32;45", "a message")
func RenderString(code string, str string) string { func RenderString(code string, str string) string {
if len(code) == 0 || str == "" { if len(code) == 0 || str == "" {
return str return str

View file

@ -191,8 +191,9 @@ func (c Color) Text(message string) string {
// Render messages by color setting // Render messages by color setting
// Usage: // Usage:
// green := color.FgGreen.Render //
// fmt.Println(green("message")) // green := color.FgGreen.Render
// fmt.Println(green("message"))
func (c Color) Render(a ...interface{}) string { func (c Color) Render(a ...interface{}) string {
return RenderCode(c.String(), a...) return RenderCode(c.String(), a...)
} }
@ -200,8 +201,9 @@ func (c Color) Render(a ...interface{}) string {
// Renderln messages by color setting. // Renderln messages by color setting.
// like Println, will add spaces for each argument // like Println, will add spaces for each argument
// Usage: // Usage:
// green := color.FgGreen.Renderln //
// fmt.Println(green("message")) // green := color.FgGreen.Renderln
// fmt.Println(green("message"))
func (c Color) Renderln(a ...interface{}) string { func (c Color) Renderln(a ...interface{}) string {
return RenderWithSpaces(c.String(), a...) return RenderWithSpaces(c.String(), a...)
} }
@ -213,25 +215,30 @@ func (c Color) Sprint(a ...interface{}) string {
// Sprintf format and render message. // Sprintf format and render message.
// Usage: // Usage:
// green := color.Green.Sprintf //
// colored := green("message") // green := color.Green.Sprintf
// colored := green("message")
func (c Color) Sprintf(format string, args ...interface{}) string { func (c Color) Sprintf(format string, args ...interface{}) string {
return RenderString(c.String(), fmt.Sprintf(format, args...)) return RenderString(c.String(), fmt.Sprintf(format, args...))
} }
// Print messages. // Print messages.
// Usage: // Usage:
// color.Green.Print("message") //
// color.Green.Print("message")
//
// OR: // OR:
// green := color.FgGreen.Print //
// green("message") // green := color.FgGreen.Print
// green("message")
func (c Color) Print(args ...interface{}) { func (c Color) Print(args ...interface{}) {
doPrintV2(c.Code(), fmt.Sprint(args...)) doPrintV2(c.Code(), fmt.Sprint(args...))
} }
// Printf format and print messages. // Printf format and print messages.
// Usage: // Usage:
// color.Cyan.Printf("string %s", "arg0") //
// color.Cyan.Printf("string %s", "arg0")
func (c Color) Printf(format string, a ...interface{}) { func (c Color) Printf(format string, a ...interface{}) {
doPrintV2(c.Code(), fmt.Sprintf(format, a...)) doPrintV2(c.Code(), fmt.Sprintf(format, a...))
} }
@ -244,8 +251,9 @@ func (c Color) Println(a ...interface{}) {
// Light current color. eg: 36(FgCyan) -> 96(FgLightCyan). // Light current color. eg: 36(FgCyan) -> 96(FgLightCyan).
// //
// Usage: // Usage:
// lightCyan := Cyan.Light() //
// lightCyan.Print("message") // lightCyan := Cyan.Light()
// lightCyan.Print("message")
func (c Color) Light() Color { func (c Color) Light() Color {
val := int(c) val := int(c)
if val >= 30 && val <= 47 { if val >= 30 && val <= 47 {
@ -259,8 +267,9 @@ func (c Color) Light() Color {
// Darken current color. eg. 96(FgLightCyan) -> 36(FgCyan) // Darken current color. eg. 96(FgLightCyan) -> 36(FgCyan)
// //
// Usage: // Usage:
// cyan := LightCyan.Darken() //
// cyan.Print("message") // cyan := LightCyan.Darken()
// cyan.Print("message")
func (c Color) Darken() Color { func (c Color) Darken() Color {
val := int(c) val := int(c)
if val >= 90 && val <= 107 { if val >= 90 && val <= 107 {

View file

@ -19,16 +19,19 @@ from wikipedia, 256 color:
// tpl for 8 bit 256 color(`2^8`) // tpl for 8 bit 256 color(`2^8`)
// //
// format: // format:
// ESC[ … 38;5;<n> … m // 选择前景色 //
// ESC[ … 48;5;<n> … m // 选择背景色 // ESC[ … 38;5;<n> … m // 选择前景色
// ESC[ … 48;5;<n> … m // 选择背景色
// //
// example: // example:
// fg "\x1b[38;5;242m" //
// bg "\x1b[48;5;208m" // fg "\x1b[38;5;242m"
// both "\x1b[38;5;242;48;5;208m" // bg "\x1b[48;5;208m"
// both "\x1b[38;5;242;48;5;208m"
// //
// links: // links:
// https://zh.wikipedia.org/wiki/ANSI%E8%BD%AC%E4%B9%89%E5%BA%8F%E5%88%97#8位 //
// https://zh.wikipedia.org/wiki/ANSI%E8%BD%AC%E4%B9%89%E5%BA%8F%E5%88%97#8位
const ( const (
TplFg256 = "38;5;%d" TplFg256 = "38;5;%d"
TplBg256 = "48;5;%d" TplBg256 = "48;5;%d"
@ -45,12 +48,14 @@ const (
// 颜色值使用10进制和16进制都可 0x98 = 152 // 颜色值使用10进制和16进制都可 0x98 = 152
// //
// The color consists of two uint8: // The color consists of two uint8:
// 0: color value //
// 1: color type; Fg=0, Bg=1, >1: unset value // 0: color value
// 1: color type; Fg=0, Bg=1, >1: unset value
// //
// example: // example:
// fg color: [152, 0] //
// bg color: [152, 1] // fg color: [152, 0]
// bg color: [152, 1]
// //
// NOTICE: now support 256 color on windows CMD, PowerShell // NOTICE: now support 256 color on windows CMD, PowerShell
// lint warn - Name starts with package name // lint warn - Name starts with package name
@ -210,9 +215,10 @@ type Style256 struct {
// S256 create a color256 style // S256 create a color256 style
// Usage: // Usage:
// s := color.S256() //
// s := color.S256(132) // fg // s := color.S256()
// s := color.S256(132, 203) // fg and bg // s := color.S256(132) // fg
// s := color.S256(132, 203) // fg and bg
func S256(fgAndBg ...uint8) *Style256 { func S256(fgAndBg ...uint8) *Style256 {
s := &Style256{} s := &Style256{}
vl := len(fgAndBg) vl := len(fgAndBg)

View file

@ -8,20 +8,24 @@ import (
// 24 bit RGB color // 24 bit RGB color
// RGB: // RGB:
// R 0-255 G 0-255 B 0-255 //
// R 00-FF G 00-FF B 00-FF (16进制) // R 0-255 G 0-255 B 0-255
// R 00-FF G 00-FF B 00-FF (16进制)
// //
// Format: // Format:
// ESC[ … 38;2;<r>;<g>;<b> … m // Select RGB foreground color //
// ESC[ … 48;2;<r>;<g>;<b> … m // Choose RGB background color // ESC[ … 38;2;<r>;<g>;<b> … m // Select RGB foreground color
// ESC[ … 48;2;<r>;<g>;<b> … m // Choose RGB background color
// //
// links: // links:
// https://zh.wikipedia.org/wiki/ANSI%E8%BD%AC%E4%B9%89%E5%BA%8F%E5%88%97#24位 //
// https://zh.wikipedia.org/wiki/ANSI%E8%BD%AC%E4%B9%89%E5%BA%8F%E5%88%97#24位
// //
// example: // example:
// fg: \x1b[38;2;30;144;255mMESSAGE\x1b[0m //
// bg: \x1b[48;2;30;144;255mMESSAGE\x1b[0m // fg: \x1b[38;2;30;144;255mMESSAGE\x1b[0m
// both: \x1b[38;2;233;90;203;48;2;30;144;255mMESSAGE\x1b[0m // bg: \x1b[48;2;30;144;255mMESSAGE\x1b[0m
// both: \x1b[38;2;233;90;203;48;2;30;144;255mMESSAGE\x1b[0m
const ( const (
TplFgRGB = "38;2;%d;%d;%d" TplFgRGB = "38;2;%d;%d;%d"
TplBgRGB = "48;2;%d;%d;%d" TplBgRGB = "48;2;%d;%d;%d"
@ -66,10 +70,11 @@ const (
// The last digit represents the foreground(0), background(1), >1 is unset value // The last digit represents the foreground(0), background(1), >1 is unset value
// //
// Usage: // Usage:
// // 0, 1, 2 is R,G,B. //
// // 3rd: Fg=0, Bg=1, >1: unset value // // 0, 1, 2 is R,G,B.
// RGBColor{30,144,255, 0} // // 3rd: Fg=0, Bg=1, >1: unset value
// RGBColor{30,144,255, 1} // RGBColor{30,144,255, 0}
// RGBColor{30,144,255, 1}
// //
// NOTICE: now support RGB color on Windows CMD, PowerShell // NOTICE: now support RGB color on Windows CMD, PowerShell
type RGBColor [4]uint8 type RGBColor [4]uint8
@ -79,9 +84,10 @@ var emptyRGBColor = RGBColor{3: 99}
// RGB color create. // RGB color create.
// Usage: // Usage:
// c := RGB(30,144,255) //
// c := RGB(30,144,255, true) // c := RGB(30,144,255)
// c.Print("message") // c := RGB(30,144,255, true)
// c.Print("message")
func RGB(r, g, b uint8, isBg ...bool) RGBColor { func RGB(r, g, b uint8, isBg ...bool) RGBColor {
rgb := RGBColor{r, g, b} rgb := RGBColor{r, g, b}
if len(isBg) > 0 && isBg[0] { if len(isBg) > 0 && isBg[0] {
@ -110,11 +116,12 @@ func RgbFromInts(rgb []int, isBg ...bool) RGBColor {
// HEX create RGB color from a HEX color string. // HEX create RGB color from a HEX color string.
// //
// Usage: // Usage:
// c := HEX("ccc") // rgb: [204 204 204] //
// c := HEX("aabbcc") // rgb: [170 187 204] // c := HEX("ccc") // rgb: [204 204 204]
// c := HEX("#aabbcc") // c := HEX("aabbcc") // rgb: [170 187 204]
// c := HEX("0xaabbcc") // c := HEX("#aabbcc")
// c.Print("message") // c := HEX("0xaabbcc")
// c.Print("message")
func HEX(hex string, isBg ...bool) RGBColor { func HEX(hex string, isBg ...bool) RGBColor {
if rgb := HexToRgb(hex); len(rgb) > 0 { if rgb := HexToRgb(hex); len(rgb) > 0 {
return RGB(uint8(rgb[0]), uint8(rgb[1]), uint8(rgb[2]), isBg...) return RGB(uint8(rgb[0]), uint8(rgb[1]), uint8(rgb[2]), isBg...)
@ -164,11 +171,12 @@ func RGBFromSlice(rgb []uint8, isBg ...bool) RGBColor {
// support use color name in the {namedRgbMap} // support use color name in the {namedRgbMap}
// //
// Usage: // Usage:
// c := RGBFromString("170,187,204")
// c.Print("message")
// //
// c := RGBFromString("brown") // c := RGBFromString("170,187,204")
// c.Print("message with color brown") // c.Print("message")
//
// c := RGBFromString("brown")
// c.Print("message with color brown")
func RGBFromString(rgb string, isBg ...bool) RGBColor { func RGBFromString(rgb string, isBg ...bool) RGBColor {
// use color name in the {namedRgbMap} // use color name in the {namedRgbMap}
if rgbVal, ok := namedRgbMap[rgb]; ok { if rgbVal, ok := namedRgbMap[rgb]; ok {
@ -327,8 +335,9 @@ func NewRGBStyle(fg RGBColor, bg ...RGBColor) *RGBStyle {
// HEXStyle create a RGBStyle from HEX color string. // HEXStyle create a RGBStyle from HEX color string.
// Usage: // Usage:
// s := HEXStyle("aabbcc", "eee") //
// s.Print("message") // s := HEXStyle("aabbcc", "eee")
// s.Print("message")
func HEXStyle(fg string, bg ...string) *RGBStyle { func HEXStyle(fg string, bg ...string) *RGBStyle {
s := &RGBStyle{} s := &RGBStyle{}
if len(bg) > 0 { if len(bg) > 0 {
@ -344,8 +353,9 @@ func HEXStyle(fg string, bg ...string) *RGBStyle {
// RGBStyleFromString create a RGBStyle from color value string. // RGBStyleFromString create a RGBStyle from color value string.
// Usage: // Usage:
// s := RGBStyleFromString("170,187,204", "70,87,4") //
// s.Print("message") // s := RGBStyleFromString("170,187,204", "70,87,4")
// s.Print("message")
func RGBStyleFromString(fg string, bg ...string) *RGBStyle { func RGBStyleFromString(fg string, bg ...string) *RGBStyle {
s := &RGBStyle{} s := &RGBStyle{}
if len(bg) > 0 { if len(bg) > 0 {

View file

@ -234,23 +234,30 @@ func ReplaceTag(str string) string {
// ParseCodeFromAttr parse color attributes. // ParseCodeFromAttr parse color attributes.
// //
// attr format: // attr format:
// // VALUE please see var: FgColors, BgColors, AllOptions //
// "fg=VALUE;bg=VALUE;op=VALUE" // // VALUE please see var: FgColors, BgColors, AllOptions
// "fg=VALUE;bg=VALUE;op=VALUE"
//
// 16 color: // 16 color:
// "fg=yellow" //
// "bg=red" // "fg=yellow"
// "op=bold,underscore" option is allow multi value // "bg=red"
// "fg=white;bg=blue;op=bold" // "op=bold,underscore" option is allow multi value
// "fg=white;op=bold,underscore" // "fg=white;bg=blue;op=bold"
// "fg=white;op=bold,underscore"
//
// 256 color: // 256 color:
//
// "fg=167" // "fg=167"
// "fg=167;bg=23" // "fg=167;bg=23"
// "fg=167;bg=23;op=bold" // "fg=167;bg=23;op=bold"
//
// true color: // true color:
// // hex //
// // hex
// "fg=fc1cac" // "fg=fc1cac"
// "fg=fc1cac;bg=c2c3c4" // "fg=fc1cac;bg=c2c3c4"
// // r,g,b // // r,g,b
// "fg=23,45,214" // "fg=23,45,214"
// "fg=23,45,214;bg=109,99,88" // "fg=23,45,214;bg=109,99,88"
func ParseCodeFromAttr(attr string) (code string) { func ParseCodeFromAttr(attr string) (code string) {
@ -371,7 +378,8 @@ func IsDefinedTag(name string) bool {
// Tag value is a defined style name // Tag value is a defined style name
// Usage: // Usage:
// Tag("info").Println("message") //
// Tag("info").Println("message")
type Tag string type Tag string
// Print messages // Print messages

View file

@ -32,14 +32,14 @@ var (
// ---------- basic(16) <=> RGB color convert ---------- // ---------- basic(16) <=> RGB color convert ----------
// refer from Hyper app // refer from Hyper app
basic2hexMap = map[uint8]string{ basic2hexMap = map[uint8]string{
30: "000000", // black 30: "000000", // black
31: "c51e14", // red 31: "c51e14", // red
32: "1dc121", // green 32: "1dc121", // green
33: "c7c329", // yellow 33: "c7c329", // yellow
34: "0a2fc4", // blue 34: "0a2fc4", // blue
35: "c839c5", // magenta 35: "c839c5", // magenta
36: "20c5c6", // cyan 36: "20c5c6", // cyan
37: "c7c7c7", // white 37: "c7c7c7", // white
// - don't add bg color // - don't add bg color
// 40: "000000", // black // 40: "000000", // black
// 41: "c51e14", // red // 41: "c51e14", // red
@ -49,14 +49,14 @@ var (
// 45: "c839c5", // magenta // 45: "c839c5", // magenta
// 46: "20c5c6", // cyan // 46: "20c5c6", // cyan
// 47: "c7c7c7", // white // 47: "c7c7c7", // white
90: "686868", // lightBlack/darkGray 90: "686868", // lightBlack/darkGray
91: "fd6f6b", // lightRed 91: "fd6f6b", // lightRed
92: "67f86f", // lightGreen 92: "67f86f", // lightGreen
93: "fffa72", // lightYellow 93: "fffa72", // lightYellow
94: "6a76fb", // lightBlue 94: "6a76fb", // lightBlue
95: "fd7cfc", // lightMagenta 95: "fd7cfc", // lightMagenta
96: "68fdfe", // lightCyan 96: "68fdfe", // lightCyan
97: "ffffff", // lightWhite 97: "ffffff", // lightWhite
// - don't add bg color // - don't add bg color
// 100: "686868", // lightBlack/darkGray // 100: "686868", // lightBlack/darkGray
// 101: "fd6f6b", // lightRed // 101: "fd6f6b", // lightRed
@ -407,10 +407,11 @@ func HexToRGB(hex string) []int { return HexToRgb(hex) }
// HexToRgb convert hex color string to RGB numbers // HexToRgb convert hex color string to RGB numbers
// //
// Usage: // Usage:
// rgb := HexToRgb("ccc") // rgb: [204 204 204] //
// rgb := HexToRgb("aabbcc") // rgb: [170 187 204] // rgb := HexToRgb("ccc") // rgb: [204 204 204]
// rgb := HexToRgb("#aabbcc") // rgb: [170 187 204] // rgb := HexToRgb("aabbcc") // rgb: [170 187 204]
// rgb := HexToRgb("0xad99c0") // rgb: [170 187 204] // rgb := HexToRgb("#aabbcc") // rgb: [170 187 204]
// rgb := HexToRgb("0xad99c0") // rgb: [170 187 204]
func HexToRgb(hex string) (rgb []int) { func HexToRgb(hex string) (rgb []int) {
hex = strings.TrimSpace(hex) hex = strings.TrimSpace(hex)
if hex == "" { if hex == "" {
@ -453,6 +454,7 @@ func Rgb2hex(rgb []int) string { return RgbToHex(rgb) }
// RgbToHex convert RGB-code to hex-code // RgbToHex convert RGB-code to hex-code
// //
// Usage: // Usage:
//
// hex := RgbToHex([]int{170, 187, 204}) // hex: "aabbcc" // hex := RgbToHex([]int{170, 187, 204}) // hex: "aabbcc"
func RgbToHex(rgb []int) string { func RgbToHex(rgb []int) string {
hexNodes := make([]string, len(rgb)) hexNodes := make([]string, len(rgb))
@ -642,6 +644,7 @@ func C256ToRgbV1(val uint8) (rgb []uint8) {
// returns r, g, and b in the set [0, 255]. // returns r, g, and b in the set [0, 255].
// //
// Usage: // Usage:
//
// HslIntToRgb(0, 100, 50) // red // HslIntToRgb(0, 100, 50) // red
// HslIntToRgb(120, 100, 50) // lime // HslIntToRgb(120, 100, 50) // lime
// HslIntToRgb(120, 100, 25) // dark green // HslIntToRgb(120, 100, 25) // dark green
@ -656,6 +659,7 @@ func HslIntToRgb(h, s, l int) (rgb []uint8) {
// returns r, g, and b in the set [0, 255]. // returns r, g, and b in the set [0, 255].
// //
// Usage: // Usage:
//
// rgbVals := HslToRgb(0, 1, 0.5) // red // rgbVals := HslToRgb(0, 1, 0.5) // red
func HslToRgb(h, s, l float64) (rgb []uint8) { func HslToRgb(h, s, l float64) (rgb []uint8) {
var r, g, b float64 var r, g, b float64

View file

@ -19,7 +19,8 @@ import (
// DetectColorLevel for current env // DetectColorLevel for current env
// //
// NOTICE: The method will detect terminal info each times, // NOTICE: The method will detect terminal info each times,
// if only want get current color level, please direct call SupportColor() or TermColorLevel() //
// if only want get current color level, please direct call SupportColor() or TermColorLevel()
func DetectColorLevel() terminfo.ColorLevel { func DetectColorLevel() terminfo.ColorLevel {
level, _ := detectTermColorLevel() level, _ := detectTermColorLevel()
return level return level
@ -173,7 +174,9 @@ func detectWSL() bool {
} }
// refer // refer
// https://github.com/Delta456/box-cli-maker/blob/7b5a1ad8a016ce181e7d8b05e24b54ff60b4b38a/detect_unix.go#L27-L45 //
// https://github.com/Delta456/box-cli-maker/blob/7b5a1ad8a016ce181e7d8b05e24b54ff60b4b38a/detect_unix.go#L27-L45
//
// detect WSL as it has True Color support // detect WSL as it has True Color support
func isWSL() bool { func isWSL() bool {
// on windows WSL: // on windows WSL:
@ -238,7 +241,8 @@ func IsMSys() bool {
// IsSupportColor check current console is support color. // IsSupportColor check current console is support color.
// //
// NOTICE: The method will detect terminal info each times, // NOTICE: The method will detect terminal info each times,
// if only want get current color level, please direct call SupportColor() or TermColorLevel() //
// if only want get current color level, please direct call SupportColor() or TermColorLevel()
func IsSupportColor() bool { func IsSupportColor() bool {
return IsSupport16Color() return IsSupport16Color()
} }
@ -246,7 +250,8 @@ func IsSupportColor() bool {
// IsSupportColor check current console is support color. // IsSupportColor check current console is support color.
// //
// NOTICE: The method will detect terminal info each times, // NOTICE: The method will detect terminal info each times,
// if only want get current color level, please direct call SupportColor() or TermColorLevel() //
// if only want get current color level, please direct call SupportColor() or TermColorLevel()
func IsSupport16Color() bool { func IsSupport16Color() bool {
level, _ := detectTermColorLevel() level, _ := detectTermColorLevel()
return level > terminfo.ColorLevelNone return level > terminfo.ColorLevelNone
@ -255,7 +260,8 @@ func IsSupport16Color() bool {
// IsSupport256Color render check // IsSupport256Color render check
// //
// NOTICE: The method will detect terminal info each times, // NOTICE: The method will detect terminal info each times,
// if only want get current color level, please direct call SupportColor() or TermColorLevel() //
// if only want get current color level, please direct call SupportColor() or TermColorLevel()
func IsSupport256Color() bool { func IsSupport256Color() bool {
level, _ := detectTermColorLevel() level, _ := detectTermColorLevel()
return level > terminfo.ColorLevelBasic return level > terminfo.ColorLevelBasic
@ -264,7 +270,8 @@ func IsSupport256Color() bool {
// IsSupportRGBColor check. alias of the IsSupportTrueColor() // IsSupportRGBColor check. alias of the IsSupportTrueColor()
// //
// NOTICE: The method will detect terminal info each times, // NOTICE: The method will detect terminal info each times,
// if only want get current color level, please direct call SupportColor() or TermColorLevel() //
// if only want get current color level, please direct call SupportColor() or TermColorLevel()
func IsSupportRGBColor() bool { func IsSupportRGBColor() bool {
return IsSupportTrueColor() return IsSupportTrueColor()
} }
@ -272,7 +279,8 @@ func IsSupportRGBColor() bool {
// IsSupportTrueColor render check. // IsSupportTrueColor render check.
// //
// NOTICE: The method will detect terminal info each times, // NOTICE: The method will detect terminal info each times,
// if only want get current color level, please direct call SupportColor() or TermColorLevel() //
// if only want get current color level, please direct call SupportColor() or TermColorLevel()
// //
// ENV: // ENV:
// "COLORTERM=truecolor" // "COLORTERM=truecolor"

View file

@ -1,3 +1,4 @@
//go:build !windows
// +build !windows // +build !windows
// The method in the file has no effect // The method in the file has no effect
@ -42,7 +43,8 @@ func detectSpecialTermColor(termVal string) (terminfo.ColorLevel, bool) {
// IsTerminal returns true if the given file descriptor is a terminal. // IsTerminal returns true if the given file descriptor is a terminal.
// //
// Usage: // Usage:
// IsTerminal(os.Stdout.Fd()) //
// IsTerminal(os.Stdout.Fd())
func IsTerminal(fd uintptr) bool { func IsTerminal(fd uintptr) bool {
return fd == uintptr(syscall.Stdout) || fd == uintptr(syscall.Stdin) || fd == uintptr(syscall.Stderr) return fd == uintptr(syscall.Stdout) || fd == uintptr(syscall.Stdin) || fd == uintptr(syscall.Stderr)
} }

View file

@ -1,10 +1,12 @@
//go:build windows
// +build windows // +build windows
// Display color on windows // Display color on windows
// refer: // refer:
// golang.org/x/sys/windows //
// golang.org/x/crypto/ssh/terminal // golang.org/x/sys/windows
// https://docs.microsoft.com/en-us/windows/console // golang.org/x/crypto/ssh/terminal
// https://docs.microsoft.com/en-us/windows/console
package color package color
import ( import (
@ -111,8 +113,10 @@ var (
) )
// refer // refer
// https://github.com/Delta456/box-cli-maker/blob/7b5a1ad8a016ce181e7d8b05e24b54ff60b4b38a/detect_windows.go#L30-L57 //
// https://github.com/gookit/color/issues/25#issuecomment-738727917 // https://github.com/Delta456/box-cli-maker/blob/7b5a1ad8a016ce181e7d8b05e24b54ff60b4b38a/detect_windows.go#L30-L57
// https://github.com/gookit/color/issues/25#issuecomment-738727917
//
// detects the Color Level Supported on windows: cmd, powerShell // detects the Color Level Supported on windows: cmd, powerShell
func detectSpecialTermColor(termVal string) (tl terminfo.ColorLevel, needVTP bool) { func detectSpecialTermColor(termVal string) (tl terminfo.ColorLevel, needVTP bool) {
if os.Getenv("ConEmuANSI") == "ON" { if os.Getenv("ConEmuANSI") == "ON" {
@ -166,9 +170,10 @@ const (
// doc https://docs.microsoft.com/zh-cn/windows/console/console-virtual-terminal-sequences#samples // doc https://docs.microsoft.com/zh-cn/windows/console/console-virtual-terminal-sequences#samples
// //
// Usage: // Usage:
// err := EnableVirtualTerminalProcessing(syscall.Stdout, true) //
// // support print color text // err := EnableVirtualTerminalProcessing(syscall.Stdout, true)
// err = EnableVirtualTerminalProcessing(syscall.Stdout, false) // // support print color text
// err = EnableVirtualTerminalProcessing(syscall.Stdout, false)
func EnableVirtualTerminalProcessing(stream syscall.Handle, enable bool) error { func EnableVirtualTerminalProcessing(stream syscall.Handle, enable bool) error {
var mode uint32 var mode uint32
// Check if it is currently in the terminal // Check if it is currently in the terminal
@ -231,9 +236,10 @@ func IsTty(fd uintptr) bool {
// IsTerminal returns true if the given file descriptor is a terminal. // IsTerminal returns true if the given file descriptor is a terminal.
// //
// Usage: // Usage:
// fd := os.Stdout.Fd() //
// fd := uintptr(syscall.Stdout) // for windows // fd := os.Stdout.Fd()
// IsTerminal(fd) // fd := uintptr(syscall.Stdout) // for windows
// IsTerminal(fd)
func IsTerminal(fd uintptr) bool { func IsTerminal(fd uintptr) bool {
initKernel32Proc() initKernel32Proc()

View file

@ -19,8 +19,9 @@ type PrinterFace interface {
// Printer a generic color message printer. // Printer a generic color message printer.
// //
// Usage: // Usage:
// p := &Printer{Code: "32;45;3"} //
// p.Print("message") // p := &Printer{Code: "32;45;3"}
// p.Print("message")
type Printer struct { type Printer struct {
// NoColor disable color. // NoColor disable color.
NoColor bool NoColor bool

View file

@ -12,12 +12,14 @@ import (
// Style a 16 color style. can add: fg color, bg color, color options // Style a 16 color style. can add: fg color, bg color, color options
// //
// Example: // Example:
// color.Style{color.FgGreen}.Print("message") //
// color.Style{color.FgGreen}.Print("message")
type Style []Color type Style []Color
// New create a custom style // New create a custom style
// //
// Usage: // Usage:
//
// color.New(color.FgGreen).Print("message") // color.New(color.FgGreen).Print("message")
// equals to: // equals to:
// color.Style{color.FgGreen}.Print("message") // color.Style{color.FgGreen}.Print("message")
@ -37,8 +39,9 @@ func (s *Style) Add(cs ...Color) {
// Render render text // Render render text
// Usage: // Usage:
// color.New(color.FgGreen).Render("text") //
// color.New(color.FgGreen, color.BgBlack, color.OpBold).Render("text") // color.New(color.FgGreen).Render("text")
// color.New(color.FgGreen, color.BgBlack, color.OpBold).Render("text")
func (s Style) Render(a ...interface{}) string { func (s Style) Render(a ...interface{}) string {
return RenderCode(s.String(), a...) return RenderCode(s.String(), a...)
} }
@ -46,8 +49,9 @@ func (s Style) Render(a ...interface{}) string {
// Renderln render text line. // Renderln render text line.
// like Println, will add spaces for each argument // like Println, will add spaces for each argument
// Usage: // Usage:
// color.New(color.FgGreen).Renderln("text", "more") //
// color.New(color.FgGreen, color.BgBlack, color.OpBold).Render("text", "more") // color.New(color.FgGreen).Renderln("text", "more")
// color.New(color.FgGreen, color.BgBlack, color.OpBold).Render("text", "more")
func (s Style) Renderln(a ...interface{}) string { func (s Style) Renderln(a ...interface{}) string {
return RenderWithSpaces(s.String(), a...) return RenderWithSpaces(s.String(), a...)
} }
@ -140,10 +144,11 @@ func (t *Theme) Block(format string, a ...interface{}) {
// internal themes(like bootstrap style) // internal themes(like bootstrap style)
// Usage: // Usage:
// color.Info.Print("message") //
// color.Info.Printf("a %s message", "test") // color.Info.Print("message")
// color.Warn.Println("message") // color.Info.Printf("a %s message", "test")
// color.Error.Println("message") // color.Warn.Println("message")
// color.Error.Println("message")
var ( var (
// Info color style // Info color style
Info = &Theme{"info", Style{OpReset, FgGreen}} Info = &Theme{"info", Style{OpReset, FgGreen}}
@ -175,7 +180,8 @@ var (
// Themes internal defined themes. // Themes internal defined themes.
// Usage: // Usage:
// color.Themes["info"].Println("message") //
// color.Themes["info"].Println("message")
var Themes = map[string]*Theme{ var Themes = map[string]*Theme{
"info": Info, "info": Info,
"note": Note, "note": Note,
@ -211,7 +217,8 @@ func GetTheme(name string) *Theme {
// Styles internal defined styles, like bootstrap styles. // Styles internal defined styles, like bootstrap styles.
// Usage: // Usage:
// color.Styles["info"].Println("message") //
// color.Styles["info"].Println("message")
var Styles = map[string]Style{ var Styles = map[string]Style{
"info": {OpReset, FgGreen}, "info": {OpReset, FgGreen},
"note": {OpBold, FgLightCyan}, "note": {OpBold, FgLightCyan},

View file

@ -85,6 +85,7 @@ func Lprint(l *log.Logger, a ...interface{}) {
// Render parse color tags, return rendered string. // Render parse color tags, return rendered string.
// Usage: // Usage:
//
// text := Render("<info>hello</> <cyan>world</>!") // text := Render("<info>hello</> <cyan>world</>!")
// fmt.Println(text) // fmt.Println(text)
func Render(a ...interface{}) string { func Render(a ...interface{}) string {

View file

@ -8,11 +8,11 @@ A helper to merge structs and maps in Golang. Useful for configuration default v
Mergo merges same-type structs and maps by setting default values in zero-value fields. Mergo won't merge unexported (private) fields. It will do recursively any exported one. It also won't merge structs inside maps (because they are not addressable using Go reflection). Mergo merges same-type structs and maps by setting default values in zero-value fields. Mergo won't merge unexported (private) fields. It will do recursively any exported one. It also won't merge structs inside maps (because they are not addressable using Go reflection).
Status # Status
It is ready for production use. It is used in several projects by Docker, Google, The Linux Foundation, VMWare, Shopify, etc. It is ready for production use. It is used in several projects by Docker, Google, The Linux Foundation, VMWare, Shopify, etc.
Important note # Important note
Please keep in mind that a problematic PR broke 0.3.9. We reverted it in 0.3.10. We consider 0.3.10 as stable but not bug-free. . Also, this version adds suppot for go modules. Please keep in mind that a problematic PR broke 0.3.9. We reverted it in 0.3.10. We consider 0.3.10 as stable but not bug-free. . Also, this version adds suppot for go modules.
@ -20,18 +20,18 @@ Keep in mind that in 0.3.2, Mergo changed Merge() and Map() signatures to suppor
If you were using Mergo before April 6th, 2015, please check your project works as intended after updating your local copy with go get -u github.com/imdario/mergo. I apologize for any issue caused by its previous behavior and any future bug that Mergo could cause in existing projects after the change (release 0.2.0). If you were using Mergo before April 6th, 2015, please check your project works as intended after updating your local copy with go get -u github.com/imdario/mergo. I apologize for any issue caused by its previous behavior and any future bug that Mergo could cause in existing projects after the change (release 0.2.0).
Install # Install
Do your usual installation procedure: Do your usual installation procedure:
go get github.com/imdario/mergo go get github.com/imdario/mergo
// use in your .go code // use in your .go code
import ( import (
"github.com/imdario/mergo" "github.com/imdario/mergo"
) )
Usage # Usage
You can only merge same-type structs with exported fields initialized as zero value of their type and same-types maps. Mergo won't merge unexported (private) fields but will do recursively any exported one. It won't merge empty structs value as they are zero values too. Also, maps will be merged recursively except for structs inside maps (because they are not addressable using Go reflection). You can only merge same-type structs with exported fields initialized as zero value of their type and same-types maps. Mergo won't merge unexported (private) fields but will do recursively any exported one. It won't merge empty structs value as they are zero values too. Also, maps will be merged recursively except for structs inside maps (because they are not addressable using Go reflection).
@ -81,7 +81,7 @@ Here is a nice example:
// {two 2} // {two 2}
} }
Transformers # Transformers
Transformers allow to merge specific types differently than in the default behavior. In other words, now you can customize how some types are merged. For example, time.Time is a struct; it doesn't have zero value but IsZero can return true because it has fields with zero value. How can we merge a non-zero time.Time? Transformers allow to merge specific types differently than in the default behavior. In other words, now you can customize how some types are merged. For example, time.Time is a struct; it doesn't have zero value but IsZero can return true because it has fields with zero value. How can we merge a non-zero time.Time?
@ -127,17 +127,16 @@ Transformers allow to merge specific types differently than in the default behav
// { 2018-01-12 01:15:00 +0000 UTC m=+0.000000001 } // { 2018-01-12 01:15:00 +0000 UTC m=+0.000000001 }
} }
Contact me # Contact me
If I can help you, you have an idea or you are using Mergo in your projects, don't hesitate to drop me a line (or a pull request): https://twitter.com/im_dario If I can help you, you have an idea or you are using Mergo in your projects, don't hesitate to drop me a line (or a pull request): https://twitter.com/im_dario
About # About
Written by Dario Castañé: https://da.rio.hn Written by Dario Castañé: https://da.rio.hn
License # License
BSD 3-Clause license, as Go language. BSD 3-Clause license, as Go language.
*/ */
package mergo package mergo

View file

@ -398,8 +398,9 @@ func parseArgWithValue(arg string) (key string, value string) {
} }
// parseFlagToName parses a flag with space value down to a key name: // parseFlagToName parses a flag with space value down to a key name:
// --path -> path //
// -p -> p // --path -> path
// -p -> p
func parseFlagToName(arg string) string { func parseFlagToName(arg string) string {
// remove minus from start // remove minus from start
arg = strings.TrimLeft(arg, "-") arg = strings.TrimLeft(arg, "-")

View file

@ -138,7 +138,7 @@ func yaml_emitter_set_canonical(emitter *yaml_emitter_t, canonical bool) {
emitter.canonical = canonical emitter.canonical = canonical
} }
//// Set the indentation increment. // // Set the indentation increment.
func yaml_emitter_set_indent(emitter *yaml_emitter_t, indent int) { func yaml_emitter_set_indent(emitter *yaml_emitter_t, indent int) {
if indent < 2 || indent > 9 { if indent < 2 || indent > 9 {
indent = 2 indent = 2

View file

@ -130,10 +130,9 @@ func yaml_emitter_emit(emitter *yaml_emitter_t, event *yaml_event_t) bool {
// Check if we need to accumulate more events before emitting. // Check if we need to accumulate more events before emitting.
// //
// We accumulate extra // We accumulate extra
// - 1 event for DOCUMENT-START // - 1 event for DOCUMENT-START
// - 2 events for SEQUENCE-START // - 2 events for SEQUENCE-START
// - 3 events for MAPPING-START // - 3 events for MAPPING-START
//
func yaml_emitter_need_more_events(emitter *yaml_emitter_t) bool { func yaml_emitter_need_more_events(emitter *yaml_emitter_t) bool {
if emitter.events_head == len(emitter.events) { if emitter.events_head == len(emitter.events) {
return true return true

View file

@ -170,7 +170,8 @@ func yaml_parser_state_machine(parser *yaml_parser_t, event *yaml_event_t) bool
// Parse the production: // Parse the production:
// stream ::= STREAM-START implicit_document? explicit_document* STREAM-END // stream ::= STREAM-START implicit_document? explicit_document* STREAM-END
// ************ //
// ************
func yaml_parser_parse_stream_start(parser *yaml_parser_t, event *yaml_event_t) bool { func yaml_parser_parse_stream_start(parser *yaml_parser_t, event *yaml_event_t) bool {
token := peek_token(parser) token := peek_token(parser)
if token == nil { if token == nil {
@ -192,9 +193,12 @@ func yaml_parser_parse_stream_start(parser *yaml_parser_t, event *yaml_event_t)
// Parse the productions: // Parse the productions:
// implicit_document ::= block_node DOCUMENT-END* // implicit_document ::= block_node DOCUMENT-END*
// * //
// *
//
// explicit_document ::= DIRECTIVE* DOCUMENT-START block_node? DOCUMENT-END* // explicit_document ::= DIRECTIVE* DOCUMENT-START block_node? DOCUMENT-END*
// ************************* //
// *************************
func yaml_parser_parse_document_start(parser *yaml_parser_t, event *yaml_event_t, implicit bool) bool { func yaml_parser_parse_document_start(parser *yaml_parser_t, event *yaml_event_t, implicit bool) bool {
token := peek_token(parser) token := peek_token(parser)
@ -277,8 +281,8 @@ func yaml_parser_parse_document_start(parser *yaml_parser_t, event *yaml_event_t
// Parse the productions: // Parse the productions:
// explicit_document ::= DIRECTIVE* DOCUMENT-START block_node? DOCUMENT-END* // explicit_document ::= DIRECTIVE* DOCUMENT-START block_node? DOCUMENT-END*
// ***********
// //
// ***********
func yaml_parser_parse_document_content(parser *yaml_parser_t, event *yaml_event_t) bool { func yaml_parser_parse_document_content(parser *yaml_parser_t, event *yaml_event_t) bool {
token := peek_token(parser) token := peek_token(parser)
if token == nil { if token == nil {
@ -299,9 +303,10 @@ func yaml_parser_parse_document_content(parser *yaml_parser_t, event *yaml_event
// Parse the productions: // Parse the productions:
// implicit_document ::= block_node DOCUMENT-END* // implicit_document ::= block_node DOCUMENT-END*
// *************
// explicit_document ::= DIRECTIVE* DOCUMENT-START block_node? DOCUMENT-END*
// //
// *************
//
// explicit_document ::= DIRECTIVE* DOCUMENT-START block_node? DOCUMENT-END*
func yaml_parser_parse_document_end(parser *yaml_parser_t, event *yaml_event_t) bool { func yaml_parser_parse_document_end(parser *yaml_parser_t, event *yaml_event_t) bool {
token := peek_token(parser) token := peek_token(parser)
if token == nil { if token == nil {
@ -332,30 +337,41 @@ func yaml_parser_parse_document_end(parser *yaml_parser_t, event *yaml_event_t)
// Parse the productions: // Parse the productions:
// block_node_or_indentless_sequence ::= // block_node_or_indentless_sequence ::=
// ALIAS //
// ***** // ALIAS
// | properties (block_content | indentless_block_sequence)? // *****
// ********** * // | properties (block_content | indentless_block_sequence)?
// | block_content | indentless_block_sequence // ********** *
// * // | block_content | indentless_block_sequence
// *
//
// block_node ::= ALIAS // block_node ::= ALIAS
// ***** //
// | properties block_content? // *****
// ********** * // | properties block_content?
// | block_content // ********** *
// * // | block_content
// *
//
// flow_node ::= ALIAS // flow_node ::= ALIAS
// ***** //
// | properties flow_content? // *****
// ********** * // | properties flow_content?
// | flow_content // ********** *
// * // | flow_content
// *
//
// properties ::= TAG ANCHOR? | ANCHOR TAG? // properties ::= TAG ANCHOR? | ANCHOR TAG?
// ************************* //
// *************************
//
// block_content ::= block_collection | flow_collection | SCALAR // block_content ::= block_collection | flow_collection | SCALAR
// ****** //
// ******
//
// flow_content ::= flow_collection | SCALAR // flow_content ::= flow_collection | SCALAR
// ****** //
// ******
func yaml_parser_parse_node(parser *yaml_parser_t, event *yaml_event_t, block, indentless_sequence bool) bool { func yaml_parser_parse_node(parser *yaml_parser_t, event *yaml_event_t, block, indentless_sequence bool) bool {
//defer trace("yaml_parser_parse_node", "block:", block, "indentless_sequence:", indentless_sequence)() //defer trace("yaml_parser_parse_node", "block:", block, "indentless_sequence:", indentless_sequence)()
@ -574,8 +590,8 @@ func yaml_parser_parse_node(parser *yaml_parser_t, event *yaml_event_t, block, i
// Parse the productions: // Parse the productions:
// block_sequence ::= BLOCK-SEQUENCE-START (BLOCK-ENTRY block_node?)* BLOCK-END // block_sequence ::= BLOCK-SEQUENCE-START (BLOCK-ENTRY block_node?)* BLOCK-END
// ******************** *********** * *********
// //
// ******************** *********** * *********
func yaml_parser_parse_block_sequence_entry(parser *yaml_parser_t, event *yaml_event_t, first bool) bool { func yaml_parser_parse_block_sequence_entry(parser *yaml_parser_t, event *yaml_event_t, first bool) bool {
if first { if first {
token := peek_token(parser) token := peek_token(parser)
@ -627,7 +643,8 @@ func yaml_parser_parse_block_sequence_entry(parser *yaml_parser_t, event *yaml_e
// Parse the productions: // Parse the productions:
// indentless_sequence ::= (BLOCK-ENTRY block_node?)+ // indentless_sequence ::= (BLOCK-ENTRY block_node?)+
// *********** * //
// *********** *
func yaml_parser_parse_indentless_sequence_entry(parser *yaml_parser_t, event *yaml_event_t) bool { func yaml_parser_parse_indentless_sequence_entry(parser *yaml_parser_t, event *yaml_event_t) bool {
token := peek_token(parser) token := peek_token(parser)
if token == nil { if token == nil {
@ -664,14 +681,14 @@ func yaml_parser_parse_indentless_sequence_entry(parser *yaml_parser_t, event *y
// Parse the productions: // Parse the productions:
// block_mapping ::= BLOCK-MAPPING_START // block_mapping ::= BLOCK-MAPPING_START
// *******************
// ((KEY block_node_or_indentless_sequence?)?
// *** *
// (VALUE block_node_or_indentless_sequence?)?)*
// //
// BLOCK-END // *******************
// ********* // ((KEY block_node_or_indentless_sequence?)?
// *** *
// (VALUE block_node_or_indentless_sequence?)?)*
// //
// BLOCK-END
// *********
func yaml_parser_parse_block_mapping_key(parser *yaml_parser_t, event *yaml_event_t, first bool) bool { func yaml_parser_parse_block_mapping_key(parser *yaml_parser_t, event *yaml_event_t, first bool) bool {
if first { if first {
token := peek_token(parser) token := peek_token(parser)
@ -723,13 +740,11 @@ func yaml_parser_parse_block_mapping_key(parser *yaml_parser_t, event *yaml_even
// Parse the productions: // Parse the productions:
// block_mapping ::= BLOCK-MAPPING_START // block_mapping ::= BLOCK-MAPPING_START
// //
// ((KEY block_node_or_indentless_sequence?)? // ((KEY block_node_or_indentless_sequence?)?
//
// (VALUE block_node_or_indentless_sequence?)?)*
// ***** *
// BLOCK-END
//
// //
// (VALUE block_node_or_indentless_sequence?)?)*
// ***** *
// BLOCK-END
func yaml_parser_parse_block_mapping_value(parser *yaml_parser_t, event *yaml_event_t) bool { func yaml_parser_parse_block_mapping_value(parser *yaml_parser_t, event *yaml_event_t) bool {
token := peek_token(parser) token := peek_token(parser)
if token == nil { if token == nil {
@ -757,16 +772,18 @@ func yaml_parser_parse_block_mapping_value(parser *yaml_parser_t, event *yaml_ev
// Parse the productions: // Parse the productions:
// flow_sequence ::= FLOW-SEQUENCE-START // flow_sequence ::= FLOW-SEQUENCE-START
// *******************
// (flow_sequence_entry FLOW-ENTRY)*
// * **********
// flow_sequence_entry?
// *
// FLOW-SEQUENCE-END
// *****************
// flow_sequence_entry ::= flow_node | KEY flow_node? (VALUE flow_node?)?
// *
// //
// *******************
// (flow_sequence_entry FLOW-ENTRY)*
// * **********
// flow_sequence_entry?
// *
// FLOW-SEQUENCE-END
// *****************
//
// flow_sequence_entry ::= flow_node | KEY flow_node? (VALUE flow_node?)?
//
// *
func yaml_parser_parse_flow_sequence_entry(parser *yaml_parser_t, event *yaml_event_t, first bool) bool { func yaml_parser_parse_flow_sequence_entry(parser *yaml_parser_t, event *yaml_event_t, first bool) bool {
if first { if first {
token := peek_token(parser) token := peek_token(parser)
@ -825,11 +842,10 @@ func yaml_parser_parse_flow_sequence_entry(parser *yaml_parser_t, event *yaml_ev
return true return true
} }
//
// Parse the productions: // Parse the productions:
// flow_sequence_entry ::= flow_node | KEY flow_node? (VALUE flow_node?)? // flow_sequence_entry ::= flow_node | KEY flow_node? (VALUE flow_node?)?
// *** *
// //
// *** *
func yaml_parser_parse_flow_sequence_entry_mapping_key(parser *yaml_parser_t, event *yaml_event_t) bool { func yaml_parser_parse_flow_sequence_entry_mapping_key(parser *yaml_parser_t, event *yaml_event_t) bool {
token := peek_token(parser) token := peek_token(parser)
if token == nil { if token == nil {
@ -849,8 +865,8 @@ func yaml_parser_parse_flow_sequence_entry_mapping_key(parser *yaml_parser_t, ev
// Parse the productions: // Parse the productions:
// flow_sequence_entry ::= flow_node | KEY flow_node? (VALUE flow_node?)? // flow_sequence_entry ::= flow_node | KEY flow_node? (VALUE flow_node?)?
// ***** *
// //
// ***** *
func yaml_parser_parse_flow_sequence_entry_mapping_value(parser *yaml_parser_t, event *yaml_event_t) bool { func yaml_parser_parse_flow_sequence_entry_mapping_value(parser *yaml_parser_t, event *yaml_event_t) bool {
token := peek_token(parser) token := peek_token(parser)
if token == nil { if token == nil {
@ -873,8 +889,8 @@ func yaml_parser_parse_flow_sequence_entry_mapping_value(parser *yaml_parser_t,
// Parse the productions: // Parse the productions:
// flow_sequence_entry ::= flow_node | KEY flow_node? (VALUE flow_node?)? // flow_sequence_entry ::= flow_node | KEY flow_node? (VALUE flow_node?)?
// *
// //
// *
func yaml_parser_parse_flow_sequence_entry_mapping_end(parser *yaml_parser_t, event *yaml_event_t) bool { func yaml_parser_parse_flow_sequence_entry_mapping_end(parser *yaml_parser_t, event *yaml_event_t) bool {
token := peek_token(parser) token := peek_token(parser)
if token == nil { if token == nil {
@ -891,16 +907,17 @@ func yaml_parser_parse_flow_sequence_entry_mapping_end(parser *yaml_parser_t, ev
// Parse the productions: // Parse the productions:
// flow_mapping ::= FLOW-MAPPING-START // flow_mapping ::= FLOW-MAPPING-START
// ******************
// (flow_mapping_entry FLOW-ENTRY)*
// * **********
// flow_mapping_entry?
// ******************
// FLOW-MAPPING-END
// ****************
// flow_mapping_entry ::= flow_node | KEY flow_node? (VALUE flow_node?)?
// * *** *
// //
// ******************
// (flow_mapping_entry FLOW-ENTRY)*
// * **********
// flow_mapping_entry?
// ******************
// FLOW-MAPPING-END
// ****************
//
// flow_mapping_entry ::= flow_node | KEY flow_node? (VALUE flow_node?)?
// - *** *
func yaml_parser_parse_flow_mapping_key(parser *yaml_parser_t, event *yaml_event_t, first bool) bool { func yaml_parser_parse_flow_mapping_key(parser *yaml_parser_t, event *yaml_event_t, first bool) bool {
if first { if first {
token := peek_token(parser) token := peek_token(parser)
@ -965,8 +982,7 @@ func yaml_parser_parse_flow_mapping_key(parser *yaml_parser_t, event *yaml_event
// Parse the productions: // Parse the productions:
// flow_mapping_entry ::= flow_node | KEY flow_node? (VALUE flow_node?)? // flow_mapping_entry ::= flow_node | KEY flow_node? (VALUE flow_node?)?
// * ***** * // - ***** *
//
func yaml_parser_parse_flow_mapping_value(parser *yaml_parser_t, event *yaml_event_t, empty bool) bool { func yaml_parser_parse_flow_mapping_value(parser *yaml_parser_t, event *yaml_event_t, empty bool) bool {
token := peek_token(parser) token := peek_token(parser)
if token == nil { if token == nil {

View file

@ -95,7 +95,7 @@ func yaml_parser_update_buffer(parser *yaml_parser_t, length int) bool {
// [Go] This function was changed to guarantee the requested length size at EOF. // [Go] This function was changed to guarantee the requested length size at EOF.
// The fact we need to do this is pretty awful, but the description above implies // The fact we need to do this is pretty awful, but the description above implies
// for that to be the case, and there are tests // for that to be the case, and there are tests
// If the EOF flag is set and the raw buffer is empty, do nothing. // If the EOF flag is set and the raw buffer is empty, do nothing.
if parser.eof && parser.raw_buffer_pos == len(parser.raw_buffer) { if parser.eof && parser.raw_buffer_pos == len(parser.raw_buffer) {

View file

@ -180,7 +180,7 @@ func resolve(tag string, in string) (rtag string, out interface{}) {
return yaml_INT_TAG, uintv return yaml_INT_TAG, uintv
} }
} else if strings.HasPrefix(plain, "-0b") { } else if strings.HasPrefix(plain, "-0b") {
intv, err := strconv.ParseInt("-" + plain[3:], 2, 64) intv, err := strconv.ParseInt("-"+plain[3:], 2, 64)
if err == nil { if err == nil {
if true || intv == int64(int(intv)) { if true || intv == int64(int(intv)) {
return yaml_INT_TAG, int(intv) return yaml_INT_TAG, int(intv)

View file

@ -1485,11 +1485,11 @@ func yaml_parser_scan_to_next_token(parser *yaml_parser_t) bool {
// Scan a YAML-DIRECTIVE or TAG-DIRECTIVE token. // Scan a YAML-DIRECTIVE or TAG-DIRECTIVE token.
// //
// Scope: // Scope:
// %YAML 1.1 # a comment \n
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
// %TAG !yaml! tag:yaml.org,2002: \n
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
// //
// %YAML 1.1 # a comment \n
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
// %TAG !yaml! tag:yaml.org,2002: \n
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
func yaml_parser_scan_directive(parser *yaml_parser_t, token *yaml_token_t) bool { func yaml_parser_scan_directive(parser *yaml_parser_t, token *yaml_token_t) bool {
// Eat '%'. // Eat '%'.
start_mark := parser.mark start_mark := parser.mark
@ -1586,11 +1586,11 @@ func yaml_parser_scan_directive(parser *yaml_parser_t, token *yaml_token_t) bool
// Scan the directive name. // Scan the directive name.
// //
// Scope: // Scope:
// %YAML 1.1 # a comment \n
// ^^^^
// %TAG !yaml! tag:yaml.org,2002: \n
// ^^^
// //
// %YAML 1.1 # a comment \n
// ^^^^
// %TAG !yaml! tag:yaml.org,2002: \n
// ^^^
func yaml_parser_scan_directive_name(parser *yaml_parser_t, start_mark yaml_mark_t, name *[]byte) bool { func yaml_parser_scan_directive_name(parser *yaml_parser_t, start_mark yaml_mark_t, name *[]byte) bool {
// Consume the directive name. // Consume the directive name.
if parser.unread < 1 && !yaml_parser_update_buffer(parser, 1) { if parser.unread < 1 && !yaml_parser_update_buffer(parser, 1) {
@ -1625,8 +1625,9 @@ func yaml_parser_scan_directive_name(parser *yaml_parser_t, start_mark yaml_mark
// Scan the value of VERSION-DIRECTIVE. // Scan the value of VERSION-DIRECTIVE.
// //
// Scope: // Scope:
// %YAML 1.1 # a comment \n //
// ^^^^^^ // %YAML 1.1 # a comment \n
// ^^^^^^
func yaml_parser_scan_version_directive_value(parser *yaml_parser_t, start_mark yaml_mark_t, major, minor *int8) bool { func yaml_parser_scan_version_directive_value(parser *yaml_parser_t, start_mark yaml_mark_t, major, minor *int8) bool {
// Eat whitespaces. // Eat whitespaces.
if parser.unread < 1 && !yaml_parser_update_buffer(parser, 1) { if parser.unread < 1 && !yaml_parser_update_buffer(parser, 1) {
@ -1664,10 +1665,11 @@ const max_number_length = 2
// Scan the version number of VERSION-DIRECTIVE. // Scan the version number of VERSION-DIRECTIVE.
// //
// Scope: // Scope:
// %YAML 1.1 # a comment \n //
// ^ // %YAML 1.1 # a comment \n
// %YAML 1.1 # a comment \n // ^
// ^ // %YAML 1.1 # a comment \n
// ^
func yaml_parser_scan_version_directive_number(parser *yaml_parser_t, start_mark yaml_mark_t, number *int8) bool { func yaml_parser_scan_version_directive_number(parser *yaml_parser_t, start_mark yaml_mark_t, number *int8) bool {
// Repeat while the next character is digit. // Repeat while the next character is digit.
@ -1701,9 +1703,9 @@ func yaml_parser_scan_version_directive_number(parser *yaml_parser_t, start_mark
// Scan the value of a TAG-DIRECTIVE token. // Scan the value of a TAG-DIRECTIVE token.
// //
// Scope: // Scope:
// %TAG !yaml! tag:yaml.org,2002: \n
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
// //
// %TAG !yaml! tag:yaml.org,2002: \n
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
func yaml_parser_scan_tag_directive_value(parser *yaml_parser_t, start_mark yaml_mark_t, handle, prefix *[]byte) bool { func yaml_parser_scan_tag_directive_value(parser *yaml_parser_t, start_mark yaml_mark_t, handle, prefix *[]byte) bool {
var handle_value, prefix_value []byte var handle_value, prefix_value []byte

View file

@ -52,7 +52,7 @@ func (l keyList) Less(i, j int) bool {
var ai, bi int var ai, bi int
var an, bn int64 var an, bn int64
if ar[i] == '0' || br[i] == '0' { if ar[i] == '0' || br[i] == '0' {
for j := i-1; j >= 0 && unicode.IsDigit(ar[j]); j-- { for j := i - 1; j >= 0 && unicode.IsDigit(ar[j]); j-- {
if ar[j] != '0' { if ar[j] != '0' {
an = 1 an = 1
bn = 1 bn = 1

View file

@ -2,8 +2,7 @@
// //
// Source code and other details for the project are available at GitHub: // Source code and other details for the project are available at GitHub:
// //
// https://github.com/go-yaml/yaml // https://github.com/go-yaml/yaml
//
package yaml package yaml
import ( import (
@ -67,16 +66,15 @@ type Marshaler interface {
// //
// For example: // For example:
// //
// type T struct { // type T struct {
// F int `yaml:"a,omitempty"` // F int `yaml:"a,omitempty"`
// B int // B int
// } // }
// var t T // var t T
// yaml.Unmarshal([]byte("a: 1\nb: 2"), &t) // yaml.Unmarshal([]byte("a: 1\nb: 2"), &t)
// //
// See the documentation of Marshal for the format of tags and a list of // See the documentation of Marshal for the format of tags and a list of
// supported tag options. // supported tag options.
//
func Unmarshal(in []byte, out interface{}) (err error) { func Unmarshal(in []byte, out interface{}) (err error) {
return unmarshal(in, out, false) return unmarshal(in, out, false)
} }
@ -166,36 +164,35 @@ func unmarshal(in []byte, out interface{}, strict bool) (err error) {
// //
// The field tag format accepted is: // The field tag format accepted is:
// //
// `(...) yaml:"[<key>][,<flag1>[,<flag2>]]" (...)` // `(...) yaml:"[<key>][,<flag1>[,<flag2>]]" (...)`
// //
// The following flags are currently supported: // The following flags are currently supported:
// //
// omitempty Only include the field if it's not set to the zero // omitempty Only include the field if it's not set to the zero
// value for the type or to empty slices or maps. // value for the type or to empty slices or maps.
// Zero valued structs will be omitted if all their public // Zero valued structs will be omitted if all their public
// fields are zero, unless they implement an IsZero // fields are zero, unless they implement an IsZero
// method (see the IsZeroer interface type), in which // method (see the IsZeroer interface type), in which
// case the field will be included if that method returns true. // case the field will be included if that method returns true.
// //
// flow Marshal using a flow style (useful for structs, // flow Marshal using a flow style (useful for structs,
// sequences and maps). // sequences and maps).
// //
// inline Inline the field, which must be a struct or a map, // inline Inline the field, which must be a struct or a map,
// causing all of its fields or keys to be processed as if // causing all of its fields or keys to be processed as if
// they were part of the outer struct. For maps, keys must // they were part of the outer struct. For maps, keys must
// not conflict with the yaml keys of other struct fields. // not conflict with the yaml keys of other struct fields.
// //
// In addition, if the key is "-", the field is ignored. // In addition, if the key is "-", the field is ignored.
// //
// For example: // For example:
// //
// type T struct { // type T struct {
// F int `yaml:"a,omitempty"` // F int `yaml:"a,omitempty"`
// B int // B int
// } // }
// yaml.Marshal(&T{B: 2}) // Returns "b: 2\n" // yaml.Marshal(&T{B: 2}) // Returns "b: 2\n"
// yaml.Marshal(&T{F: 1}} // Returns "a: 1\nb: 0\n" // yaml.Marshal(&T{F: 1}} // Returns "a: 1\nb: 0\n"
//
func Marshal(in interface{}) (out []byte, err error) { func Marshal(in interface{}) (out []byte, err error) {
defer handleErr(&err) defer handleErr(&err)
e := newEncoder() e := newEncoder()

View file

@ -408,7 +408,9 @@ type yaml_document_t struct {
// The number of written bytes should be set to the size_read variable. // The number of written bytes should be set to the size_read variable.
// //
// [in,out] data A pointer to an application data specified by // [in,out] data A pointer to an application data specified by
// yaml_parser_set_input(). //
// yaml_parser_set_input().
//
// [out] buffer The buffer to write the data from the source. // [out] buffer The buffer to write the data from the source.
// [in] size The size of the buffer. // [in] size The size of the buffer.
// [out] size_read The actual number of bytes read from the source. // [out] size_read The actual number of bytes read from the source.
@ -603,13 +605,14 @@ type yaml_parser_t struct {
// @a buffer to the output. // @a buffer to the output.
// //
// @param[in,out] data A pointer to an application data specified by // @param[in,out] data A pointer to an application data specified by
// yaml_emitter_set_output(). //
// yaml_emitter_set_output().
//
// @param[in] buffer The buffer with bytes to be written. // @param[in] buffer The buffer with bytes to be written.
// @param[in] size The size of the buffer. // @param[in] size The size of the buffer.
// //
// @returns On success, the handler should return @c 1. If the handler failed, // @returns On success, the handler should return @c 1. If the handler failed,
// the returned value should be @c 0. // the returned value should be @c 0.
//
type yaml_write_handler_t func(emitter *yaml_emitter_t, buffer []byte) error type yaml_write_handler_t func(emitter *yaml_emitter_t, buffer []byte) error
type yaml_emitter_state_t int type yaml_emitter_state_t int

View file

@ -11,7 +11,8 @@ import "math"
// comparing to the test values, this modified white reference is used internally. // comparing to the test values, this modified white reference is used internally.
// //
// See this GitHub thread for details on these values: // See this GitHub thread for details on these values:
// https://github.com/hsluv/hsluv/issues/79 //
// https://github.com/hsluv/hsluv/issues/79
var hSLuvD65 = [3]float64{0.95045592705167, 1.0, 1.089057750759878} var hSLuvD65 = [3]float64{0.95045592705167, 1.0, 1.089057750759878}
func LuvLChToHSLuv(l, c, h float64) (float64, float64, float64) { func LuvLChToHSLuv(l, c, h float64) (float64, float64, float64) {

View file

@ -1,3 +1,4 @@
//go:build appengine
// +build appengine // +build appengine
package colorable package colorable

View file

@ -1,5 +1,5 @@
// +build !windows //go:build !windows && !appengine
// +build !appengine // +build !windows,!appengine
package colorable package colorable

View file

@ -1,5 +1,5 @@
// +build windows //go:build windows && !appengine
// +build !appengine // +build windows,!appengine
package colorable package colorable

View file

@ -1,3 +1,4 @@
//go:build (darwin || freebsd || openbsd || netbsd || dragonfly) && !appengine
// +build darwin freebsd openbsd netbsd dragonfly // +build darwin freebsd openbsd netbsd dragonfly
// +build !appengine // +build !appengine

View file

@ -1,3 +1,4 @@
//go:build appengine || js || nacl
// +build appengine js nacl // +build appengine js nacl
package isatty package isatty

View file

@ -1,3 +1,4 @@
//go:build plan9
// +build plan9 // +build plan9
package isatty package isatty

View file

@ -1,5 +1,5 @@
// +build solaris //go:build solaris && !appengine
// +build !appengine // +build solaris,!appengine
package isatty package isatty

View file

@ -1,3 +1,4 @@
//go:build (linux || aix) && !appengine
// +build linux aix // +build linux aix
// +build !appengine // +build !appengine

View file

@ -1,5 +1,5 @@
// +build windows //go:build windows && !appengine
// +build !appengine // +build windows,!appengine
package isatty package isatty
@ -42,7 +42,8 @@ func IsTerminal(fd uintptr) bool {
// Check pipe name is used for cygwin/msys2 pty. // Check pipe name is used for cygwin/msys2 pty.
// Cygwin/MSYS2 PTY has a name like: // Cygwin/MSYS2 PTY has a name like:
// \{cygwin,msys}-XXXXXXXXXXXXXXXX-ptyN-{from,to}-master //
// \{cygwin,msys}-XXXXXXXXXXXXXXXX-ptyN-{from,to}-master
func isCygwinPipeName(name string) bool { func isCygwinPipeName(name string) bool {
token := strings.Split(name, "-") token := strings.Split(name, "-")
if len(token) < 5 { if len(token) < 5 {

7
vendor/github.com/mgutz/str/doc.go generated vendored
View file

@ -5,15 +5,14 @@
// Str is based on plain functions instead of object-based methods, // Str is based on plain functions instead of object-based methods,
// consistent with Go standard string packages. // consistent with Go standard string packages.
// //
// str.Between("<a>foo</a>", "<a>", "</a>") == "foo" // str.Between("<a>foo</a>", "<a>", "</a>") == "foo"
// //
// Str supports pipelining instead of chaining // Str supports pipelining instead of chaining
// //
// s := str.Pipe("\nabcdef\n", Clean, BetweenF("a", "f"), ChompLeftF("bc")) // s := str.Pipe("\nabcdef\n", Clean, BetweenF("a", "f"), ChompLeftF("bc"))
// //
// User-defined filters can be added to the pipeline by inserting a function // User-defined filters can be added to the pipeline by inserting a function
// or closure that returns a function with this signature // or closure that returns a function with this signature
// //
// func(string) string // func(string) string
//
package str package str

View file

@ -30,7 +30,7 @@ import (
// //
// The following is an example of the contents of Digest types: // The following is an example of the contents of Digest types:
// //
// sha256:7173b809ca12ec5dee4506cd86be934c4596dd234ee82c0662eac04a8c2c71dc // sha256:7173b809ca12ec5dee4506cd86be934c4596dd234ee82c0662eac04a8c2c71dc
// //
// This allows to abstract the digest behind this type and work only in those // This allows to abstract the digest behind this type and work only in those
// terms. // terms.

View file

@ -19,16 +19,16 @@
// More importantly, it provides tools and wrappers to work with // More importantly, it provides tools and wrappers to work with
// hash.Hash-based digests with little effort. // hash.Hash-based digests with little effort.
// //
// Basics // # Basics
// //
// The format of a digest is simply a string with two parts, dubbed the // The format of a digest is simply a string with two parts, dubbed the
// "algorithm" and the "digest", separated by a colon: // "algorithm" and the "digest", separated by a colon:
// //
// <algorithm>:<digest> // <algorithm>:<digest>
// //
// An example of a sha256 digest representation follows: // An example of a sha256 digest representation follows:
// //
// sha256:7173b809ca12ec5dee4506cd86be934c4596dd234ee82c0662eac04a8c2c71dc // sha256:7173b809ca12ec5dee4506cd86be934c4596dd234ee82c0662eac04a8c2c71dc
// //
// The "algorithm" portion defines both the hashing algorithm used to calculate // The "algorithm" portion defines both the hashing algorithm used to calculate
// the digest and the encoding of the resulting digest, which defaults to "hex" // the digest and the encoding of the resulting digest, which defaults to "hex"
@ -42,7 +42,7 @@
// obtained, comparisons are cheap, quick and simple to express with the // obtained, comparisons are cheap, quick and simple to express with the
// standard equality operator. // standard equality operator.
// //
// Verification // # Verification
// //
// The main benefit of using the Digest type is simple verification against a // The main benefit of using the Digest type is simple verification against a
// given digest. The Verifier interface, modeled after the stdlib hash.Hash // given digest. The Verifier interface, modeled after the stdlib hash.Hash
@ -50,7 +50,7 @@
// writing is complete, calling the Verifier.Verified method will indicate // writing is complete, calling the Verifier.Verified method will indicate
// whether or not the stream of bytes matches the target digest. // whether or not the stream of bytes matches the target digest.
// //
// Missing Features // # Missing Features
// //
// In addition to the above, we intend to add the following features to this // In addition to the above, we intend to add the following features to this
// package: // package:
@ -58,5 +58,4 @@
// 1. A Digester type that supports write sink digest calculation. // 1. A Digester type that supports write sink digest calculation.
// //
// 2. Suspend and resume of ongoing digest calculations to support efficient digest verification in the registry. // 2. Suspend and resume of ongoing digest calculations to support efficient digest verification in the registry.
//
package digest package digest

View file

@ -13,6 +13,7 @@
// permissions and limitations under the License. See the AUTHORS file // permissions and limitations under the License. See the AUTHORS file
// for names of contributors. // for names of contributors.
//go:build gccgo
// +build gccgo // +build gccgo
package goid package goid

View file

@ -13,6 +13,7 @@
// permissions and limitations under the License. See the AUTHORS file // permissions and limitations under the License. See the AUTHORS file
// for names of contributors. // for names of contributors.
//go:build !go1.4
// +build !go1.4 // +build !go1.4
package goid package goid

View file

@ -13,6 +13,7 @@
// permissions and limitations under the License. See the AUTHORS file // permissions and limitations under the License. See the AUTHORS file
// for names of contributors. // for names of contributors.
//go:build go1.4 && !go1.5
// +build go1.4,!go1.5 // +build go1.4,!go1.5
package goid package goid

View file

@ -13,8 +13,10 @@
// permissions and limitations under the License. See the AUTHORS file // permissions and limitations under the License. See the AUTHORS file
// for names of contributors. // for names of contributors.
//go:build (amd64 || amd64p32) && gc && go1.5
// +build amd64 amd64p32 // +build amd64 amd64p32
// +build gc,go1.5 // +build gc
// +build go1.5
package goid package goid

View file

@ -13,8 +13,8 @@
// permissions and limitations under the License. See the AUTHORS file // permissions and limitations under the License. See the AUTHORS file
// for names of contributors. // for names of contributors.
// +build arm //go:build arm && gc && go1.5
// +build gc,go1.5 // +build arm,gc,go1.5
package goid package goid

View file

@ -13,6 +13,7 @@
// permissions and limitations under the License. See the AUTHORS file // permissions and limitations under the License. See the AUTHORS file
// for names of contributors. // for names of contributors.
//go:build (go1.4 && !go1.5 && !amd64 && !amd64p32 && !arm && !386) || (go1.5 && !go1.6 && !amd64 && !amd64p32 && !arm) || (go1.6 && !amd64 && !amd64p32 && !arm) || (go1.9 && !amd64 && !amd64p32 && !arm)
// +build go1.4,!go1.5,!amd64,!amd64p32,!arm,!386 go1.5,!go1.6,!amd64,!amd64p32,!arm go1.6,!amd64,!amd64p32,!arm go1.9,!amd64,!amd64p32,!arm // +build go1.4,!go1.5,!amd64,!amd64p32,!arm,!386 go1.5,!go1.6,!amd64,!amd64p32,!arm go1.6,!amd64,!amd64p32,!arm go1.9,!amd64,!amd64p32,!arm
package goid package goid

View file

@ -1,3 +1,4 @@
//go:build gccgo && go1.8
// +build gccgo,go1.8 // +build gccgo,go1.8
package goid package goid

Some files were not shown because too many files have changed in this diff Show more