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
.vscode
.idea
coverage.txt

View file

@ -40,7 +40,7 @@
<img src="https://user-images.githubusercontent.com/8456633/59972109-8e9c8480-95cc-11e9-8350-38f7f86ba76d.png">
</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)
[![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
### Docker Runtime (Default)
- Docker >= **1.13** (API >= **1.25**)
- 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
### Homebrew
@ -255,14 +261,39 @@ You can also use `go run main.go` to compile and run in one go (pun definitely i
## Usage
### Basic Usage
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
```
(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).
- List of 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 }}`
- [`{{ .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
You can add replacements like so:

View file

@ -29,6 +29,7 @@ var (
configFlag = false
debuggingFlag = false
composeFiles []string
runtimeFlag = "docker"
)
func main() {
@ -51,6 +52,7 @@ func main() {
flaggy.Bool(&configFlag, "c", "config", "Print the current default config")
flaggy.Bool(&debuggingFlag, "d", "debug", "a boolean")
flaggy.StringSlice(&composeFiles, "f", "file", "Specify alternate compose files")
flaggy.String(&runtimeFlag, "r", "runtime", "Container runtime to use (docker, apple)")
flaggy.SetVersion(info)
flaggy.Parse()
@ -71,7 +73,7 @@ func main() {
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 {
log.Fatal(err.Error())
}

View file

@ -17,13 +17,15 @@ import (
type App struct {
closers []io.Closer
Config *config.AppConfig
Log *logrus.Entry
OSCommand *commands.OSCommand
DockerCommand *commands.DockerCommand
Gui *gui.Gui
Tr *i18n.TranslationSet
ErrorChan chan error
Config *config.AppConfig
Log *logrus.Entry
OSCommand *commands.OSCommand
DockerCommand *commands.DockerCommand
AppleContainerCommand *commands.AppleContainerCommand
ContainerRuntime *commands.ContainerRuntimeAdapter
Gui *gui.Gui
Tr *i18n.TranslationSet
ErrorChan chan error
}
// NewApp bootstrap a new application
@ -41,14 +43,29 @@ func NewApp(config *config.AppConfig) (*App, error) {
}
app.OSCommand = commands.NewOSCommand(app.Log, config)
// here is the place to make use of the docker-compose.yml file in the current directory
app.DockerCommand, err = commands.NewDockerCommand(app.Log, app.OSCommand, app.Tr, app.Config, app.ErrorChan)
if err != nil {
return app, err
// Initialize the appropriate container runtime based on config
switch config.Runtime {
case "docker":
// here is the place to make use of the docker-compose.yml file in the current directory
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 {
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",
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 {

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) {
mConfig, err := config.NewAppConfig("lazydocker", "", "", "", "", true, nil, "")
mConfig, err := config.NewAppConfig("lazydocker", "", "", "", "", true, nil, "", "docker")
if err != nil {
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
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
func (c *Container) Remove(options container.RemoveOptions) error {
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 strings.Contains(err.Error(), "Stop the container before attempting removal or force remove") {
return ComplexError{
@ -62,6 +74,14 @@ func (c *Container) Remove(options container.RemoveOptions) error {
// Stop stops the container
func (c *Container) Stop() error {
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{})
}
@ -80,6 +100,17 @@ func (c *Container) Unpause() error {
// Restart restarts the container
func (c *Container) Restart() error {
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{})
}
@ -98,6 +129,11 @@ func (c *Container) Attach() (*exec.Cmd, error) {
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))
// TODO: use SDK
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
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()
if err != nil {
return container.ContainerTopOKBody{}, err
@ -127,6 +173,23 @@ func (c *DockerCommand) PruneContainers() error {
// Inspect returns details about the container
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)
}

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
import (
"errors"
"fmt"
"github.com/go-errors/errors"
goErrors "github.com/go-errors/errors"
"golang.org/x/xerrors"
)
@ -20,7 +21,7 @@ func WrapError(err error) error {
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
@ -55,3 +56,11 @@ func HasErrorCode(err error, code int) bool {
}
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 (
"context"
"fmt"
"strings"
"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
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 {
return "", err
}

View file

@ -2,6 +2,7 @@ package commands
import (
"context"
"fmt"
"os/exec"
"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
func (s *Service) Stop() error {
s.Log.Warn(fmt.Sprintf("stopping service %s", s.Name))
return s.runCommand(s.OSCommand.Config.UserConfig.CommandTemplates.StopService)
}
@ -49,7 +51,12 @@ func (s *Service) runCommand(templateCmdStr string) error {
templateCmdStr,
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

View file

@ -52,5 +52,12 @@ func (c *DockerCommand) PruneVolumes() error {
// Remove removes the volume
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)
}

View file

@ -13,6 +13,7 @@
package config
import (
"fmt"
"os"
"path/filepath"
"strings"
@ -64,6 +65,23 @@ type UserConfig struct {
// will be filtered out and not displayed.
// Not documented because it's subject to change
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.
@ -474,6 +492,7 @@ func GetDefaultConfig() UserConfig {
Replacements: Replacements{
ImageNamePrefixes: map[string]string{},
},
Apple: &AppleRuntimeConfig{},
}
}
@ -485,13 +504,14 @@ type AppConfig struct {
BuildDate string `long:"build-date" env:"BUILD_DATE"`
Name string `long:"name" env:"NAME" default:"lazydocker"`
BuildSource string `long:"build-source" env:"BUILD_SOURCE" default:""`
Runtime string `long:"runtime" env:"RUNTIME" default:"docker"`
UserConfig *UserConfig
ConfigDir string
ProjectDir string
}
// 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)
if err != nil {
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 ")
}
// Validate runtime parameter
if runtime != "docker" && runtime != "apple" {
return nil, fmt.Errorf("unsupported runtime '%s'. Supported runtimes: docker, apple", runtime)
}
appConfig := &AppConfig{
Name: name,
Version: version,
@ -514,6 +539,7 @@ func NewAppConfig(name, version, commit, date string, buildSource string, debugg
BuildDate: date,
Debug: debuggingFlag || os.Getenv("DEBUG") == "TRUE",
BuildSource: buildSource,
Runtime: runtime,
UserConfig: userConfig,
ConfigDir: configDir,
ProjectDir: projectDir,

View file

@ -9,7 +9,7 @@ import (
func TestDockerComposeCommandNoFiles(t *testing.T) {
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 {
t.Fatalf("Unexpected error: %s", err)
}
@ -23,7 +23,7 @@ func TestDockerComposeCommandNoFiles(t *testing.T) {
func TestDockerComposeCommandSingleFile(t *testing.T) {
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 {
t.Fatalf("Unexpected error: %s", err)
}
@ -37,7 +37,7 @@ func TestDockerComposeCommandSingleFile(t *testing.T) {
func TestDockerComposeCommandMultipleFiles(t *testing.T) {
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 {
t.Fatalf("Unexpected error: %s", err)
}
@ -52,7 +52,7 @@ func TestDockerComposeCommandMultipleFiles(t *testing.T) {
func TestWritingToConfigFile(t *testing.T) {
// init the AppConfig
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 {
t.Fatalf("Unexpected error: %s", err)
}
@ -96,3 +96,76 @@ func TestWritingToConfigFile(t *testing.T) {
// modifying an existing file that already has 'ConfirmOnQuit'
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"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/client"
"github.com/docker/docker/pkg/stdcopy"
"github.com/fatih/color"
"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 {
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,
ShowStderr: true,
Timestamps: gui.Config.UserConfig.Logs.Timestamps,
@ -150,3 +160,40 @@ func (gui *Gui) writeContainerLogs(ctr *commands.Container, ctx context.Context,
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]{
ContextState: &panels.ContextState[*commands.Container]{
GetMainTabs: func() []panels.MainTab[*commands.Container] {
return []panels.MainTab[*commands.Container]{
tabs := []panels.MainTab[*commands.Container]{
{
Key: "logs",
Title: gui.Tr.LogsTitle,
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",
Title: gui.Tr.StatsTitle,
Render: gui.renderContainerStats,
},
{
})
}
// Common tabs for all runtimes
tabs = append(tabs,
panels.MainTab[*commands.Container]{
Key: "env",
Title: gui.Tr.EnvTitle,
Render: gui.renderContainerEnv,
},
{
panels.MainTab[*commands.Container]{
Key: "config",
Title: gui.Tr.ConfigTitle,
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",
Title: gui.Tr.TopTitle,
Render: gui.renderContainerTop,
},
})
}
return tabs
},
GetItemContextCacheKey: func(container *commands.Container) string {
// 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
},
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
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.Containers.List.GetAllItems(),
)
@ -283,7 +315,7 @@ func (gui *Gui) refreshContainersAndServices() error {
}
func (gui *Gui) renderContainersAndServices() error {
if gui.DockerCommand.InDockerComposeProject {
if gui.ContainerCommand.InDockerComposeProject() {
if err := gui.Panels.Services.RerenderList(); err != nil {
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{
{
LabelColumns: []string{gui.Tr.Remove, "docker rm " + ctr.ID[1:10]},
LabelColumns: []string{gui.Tr.Remove, rmCmd},
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}) },
},
}
@ -414,9 +454,12 @@ func (gui *Gui) handleContainerAttach(g *gocui.Gui, v *gocui.View) 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.WithWaitingStatus(gui.Tr.PruningStatus, func() error {
err := gui.DockerCommand.PruneContainers()
err := gui.ContainerCommand.PruneContainers()
if err != nil {
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 {
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,
})
// 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)
// attach and return the subprocess error
cmd := gui.OSCommand.ExecutableFromString(resolvedCommand)
return gui.runSubprocess(cmd)
}
@ -463,7 +517,7 @@ func (gui *Gui) handleContainersCustomCommand(g *gocui.Gui, v *gocui.View) error
return nil
}
commandObject := gui.DockerCommand.NewCommandObject(commands.CommandObject{
commandObject := gui.ContainerCommand.NewCommandObject(commands.CommandObject{
Container: ctr,
})
@ -502,22 +556,18 @@ func (gui *Gui) handleRemoveContainers() error {
func (gui *Gui) handleContainersBulkCommand(g *gocui.Gui, v *gocui.View) error {
baseBulkCommands := []config.CustomCommand{
{
Name: gui.Tr.StopAllContainers,
InternalFunction: gui.handleStopContainers,
},
{
Name: gui.Tr.RemoveAllContainers,
InternalFunction: gui.handleRemoveContainers,
},
{
{Name: gui.Tr.StopAllContainers, InternalFunction: gui.handleStopContainers},
{Name: gui.Tr.RemoveAllContainers, InternalFunction: gui.handleRemoveContainers},
}
if gui.ContainerCommand == nil || gui.ContainerCommand.Supports(commands.FeatureContainerPrune) {
baseBulkCommands = append(baseBulkCommands, config.CustomCommand{ // show only if supported
Name: gui.Tr.PruneContainers,
InternalFunction: gui.handlePruneContainers,
},
})
}
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)
}
@ -549,3 +599,29 @@ func (gui *Gui) openContainerInBrowser(ctr *commands.Container) error {
link := fmt.Sprintf("http://%s:%d/", ip, port.PublicPort)
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"
"github.com/docker/docker/api/types/events"
"github.com/docker/docker/client"
"github.com/go-errors/errors"
@ -25,17 +26,18 @@ import (
// Gui wraps the gocui Gui object which handles rendering and events
type Gui struct {
g *gocui.Gui
Log *logrus.Entry
DockerCommand *commands.DockerCommand
OSCommand *commands.OSCommand
State guiState
Config *config.AppConfig
Tr *i18n.TranslationSet
statusManager *statusManager
taskManager *tasks.TaskManager
ErrorChan chan error
Views Views
g *gocui.Gui
Log *logrus.Entry
DockerCommand *commands.DockerCommand
ContainerCommand *commands.GuiContainerCommand
OSCommand *commands.OSCommand
State guiState
Config *config.AppConfig
Tr *i18n.TranslationSet
statusManager *statusManager
taskManager *tasks.TaskManager
ErrorChan chan error
Views Views
// 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
@ -125,7 +127,7 @@ func getScreenMode(config *config.AppConfig) WindowMaximisation {
}
// 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{
Platform: *oSCommand.Platform,
Panels: &panelStates{
@ -140,15 +142,16 @@ func NewGui(log *logrus.Entry, dockerCommand *commands.DockerCommand, oSCommand
}
gui := &Gui{
Log: log,
DockerCommand: dockerCommand,
OSCommand: oSCommand,
State: initialState,
Config: config,
Tr: tr,
statusManager: &statusManager{},
taskManager: tasks.NewTaskManager(log, tr),
ErrorChan: errorChan,
Log: log,
DockerCommand: dockerCommand,
ContainerCommand: containerCommand,
OSCommand: oSCommand,
State: initialState,
Config: config,
Tr: tr,
statusManager: &statusManager{},
taskManager: tasks.NewTaskManager(log, tr),
ErrorChan: errorChan,
}
deadlock.Opts.Disable = !gui.Config.Debug
@ -292,7 +295,9 @@ func (gui *Gui) setPanels() {
}
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() {
@ -336,7 +341,18 @@ func (gui *Gui) listenForEvents(ctx context.Context, refresh func()) {
outer:
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 {
select {
@ -459,7 +475,7 @@ func (gui *Gui) ShouldRefresh(key string) bool {
}
func (gui *Gui) initiallyFocusedViewName() string {
if gui.DockerCommand.InDockerComposeProject {
if gui.ContainerCommand.InDockerComposeProject() {
return "services"
}
return "containers"
@ -485,7 +501,7 @@ func (gui *Gui) monitorContainerStats(ctx context.Context) {
case <-ticker.C:
for _, container := range gui.Panels.Containers.List.GetAllItems() {
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("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("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()
if err != nil {
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
@ -101,7 +110,7 @@ func (gui *Gui) reloadImages() error {
}
func (gui *Gui) refreshStateImages() error {
images, err := gui.DockerCommand.RefreshImages()
images, err := gui.ContainerCommand.RefreshImages()
if err != nil {
return err
}
@ -131,6 +140,11 @@ func (gui *Gui) handleImagesRemoveMenu(g *gocui.Gui, v *gocui.View) error {
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]
// 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 {
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.WithWaitingStatus(gui.Tr.PruningStatus, func() error {
err := gui.DockerCommand.PruneImages()
err := gui.ContainerCommand.PruneImages()
if err != nil {
return gui.createErrorPanel(err.Error())
}
@ -197,7 +214,7 @@ func (gui *Gui) handleImagesCustomCommand(g *gocui.Gui, v *gocui.View) error {
return nil
}
commandObject := gui.DockerCommand.NewCommandObject(commands.CommandObject{
commandObject := gui.ContainerCommand.NewCommandObject(commands.CommandObject{
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 {
baseBulkCommands := []config.CustomCommand{
{
baseBulkCommands := []config.CustomCommand{}
if gui.ContainerCommand == nil || gui.ContainerCommand.Supports(commands.FeatureImagePrune) {
baseBulkCommands = append(baseBulkCommands, config.CustomCommand{
Name: gui.Tr.PruneImages,
InternalFunction: gui.handlePruneImages,
},
})
}
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)
}

View file

@ -4,6 +4,7 @@ import (
"fmt"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazydocker/pkg/commands"
)
// 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,
Description: gui.Tr.Restart,
},
{
ViewName: "containers",
Key: 'a',
Modifier: gocui.ModNone,
Handler: gui.handleContainerAttach,
Description: gui.Tr.Attach,
},
// container attach: added conditionally below based on runtime support
{
ViewName: "containers",
Key: 'm',
@ -304,13 +299,7 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
Handler: gui.handleServiceStart,
Description: gui.Tr.Start,
},
{
ViewName: "services",
Key: 'a',
Modifier: gocui.ModNone,
Handler: gui.handleServiceAttach,
Description: gui.Tr.Attach,
},
// service attach: added conditionally below based on runtime support
{
ViewName: "services",
Key: 'm',
@ -395,6 +384,7 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
Handler: gui.handleVolumesCustomCommand,
Description: gui.Tr.RunCustomCommand,
},
// Create volume binding will be added conditionally below based on capability
{
ViewName: "volumes",
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() {
bindings = append(bindings, []*Binding{
{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 {
networks, err := gui.DockerCommand.RefreshNetworks()
networks, err := gui.ContainerCommand.RefreshNetworks()
if err != nil {
return err
}
@ -110,10 +110,16 @@ func (gui *Gui) handleNetworksRemoveMenu(g *gocui.Gui, v *gocui.View) error {
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{
{
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 {
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.WithWaitingStatus(gui.Tr.PruningStatus, func() error {
err := gui.DockerCommand.PruneNetworks()
err := gui.ContainerCommand.PruneNetworks()
if err != nil {
return gui.createErrorPanel(err.Error())
}
@ -155,7 +164,7 @@ func (gui *Gui) handleNetworksCustomCommand(g *gocui.Gui, v *gocui.View) error {
return nil
}
commandObject := gui.DockerCommand.NewCommandObject(commands.CommandObject{
commandObject := gui.ContainerCommand.NewCommandObject(commands.CommandObject{
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 {
baseBulkCommands := []config.CustomCommand{
{
baseBulkCommands := []config.CustomCommand{}
if gui.ContainerCommand == nil || gui.ContainerCommand.Supports(commands.FeatureNetworkPrune) {
baseBulkCommands = append(baseBulkCommands, config.CustomCommand{
Name: gui.Tr.PruneNetworks,
InternalFunction: gui.handlePruneNetworks,
},
})
}
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)
}

View file

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

View file

@ -14,15 +14,24 @@ import (
"github.com/samber/lo"
)
func GetContainerDisplayStrings(guiConfig *config.GuiConfig, container *commands.Container) []string {
return []string{
func GetContainerDisplayStrings(guiConfig *config.GuiConfig, container *commands.Container, runtime string) []string {
cols := []string{
getContainerDisplayStatus(guiConfig, container),
getContainerDisplaySubstatus(guiConfig, container),
container.Name,
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(displayContainerImage(container), color.FgMagenta),
}
)
return cols
}
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/lazydocker/pkg/commands"
"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/utils"
"github.com/jesseduffield/yaml"
@ -23,7 +22,7 @@ func (gui *Gui) getProjectPanel() *panels.SideListPanel[*commands.Project] {
return &panels.SideListPanel[*commands.Project]{
ContextState: &panels.ContextState[*commands.Project]{
GetMainTabs: func() []panels.MainTab[*commands.Project] {
if gui.DockerCommand.InDockerComposeProject {
if gui.ContainerCommand.InDockerComposeProject() {
return []panels.MainTab[*commands.Project]{
{
Key: "logs",
@ -35,6 +34,16 @@ func (gui *Gui) getProjectPanel() *panels.SideListPanel[*commands.Project] {
Title: gui.Tr.DockerComposeConfigTitle,
Render: gui.renderDockerComposeConfig,
},
{
Key: "runtime",
Title: "Runtime Info",
Render: gui.renderRuntimeInfo,
},
{
Key: "system",
Title: "System Status",
Render: gui.renderSystemStatus,
},
{
Key: "credits",
Title: gui.Tr.CreditsTitle,
@ -49,6 +58,16 @@ func (gui *Gui) getProjectPanel() *panels.SideListPanel[*commands.Project] {
Title: gui.Tr.CreditsTitle,
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 {
@ -66,7 +85,15 @@ func (gui *Gui) getProjectPanel() *panels.SideListPanel[*commands.Project] {
Sort: func(a *commands.Project, b *commands.Project) bool {
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.
DisableFilter: true,
}
@ -79,7 +106,7 @@ func (gui *Gui) refreshProject() error {
func (gui *Gui) getProjectName() string {
projectName := path.Base(gui.Config.ProjectDir)
if gui.DockerCommand.InDockerComposeProject {
if gui.ContainerCommand.InDockerComposeProject() {
for _, service := range gui.Panels.Services.List.GetAllItems() {
container := service.Container
if container != nil && container.DetailsLoaded() {
@ -122,7 +149,7 @@ func (gui *Gui) renderAllLogs(_project *commands.Project) tasks.TaskFunc {
cmd := gui.OSCommand.RunCustomCommand(
utils.ApplyTemplate(
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 {
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 {
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
func (gui *Gui) handleViewAllLogs(g *gocui.Gui, v *gocui.View) error {
c, err := gui.DockerCommand.ViewAllLogs()
c, err := gui.ContainerCommand.ViewAllLogs()
if err != nil {
return gui.createErrorPanel(err.Error())
}

View file

@ -21,33 +21,20 @@ func (gui *Gui) getServicesPanel() *panels.SideListPanel[*commands.Service] {
return &panels.SideListPanel[*commands.Service]{
ContextState: &panels.ContextState[*commands.Service]{
GetMainTabs: func() []panels.MainTab[*commands.Service] {
return []panels.MainTab[*commands.Service]{
{
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,
},
tabs := []panels.MainTab[*commands.Service]{
{Key: "logs", Title: gui.Tr.LogsTitle, Render: gui.renderServiceLogs},
}
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 {
if service.Container == nil {
@ -78,7 +65,7 @@ func (gui *Gui) getServicesPanel() *panels.SideListPanel[*commands.Service] {
return presentation.GetServiceDisplayStrings(&gui.Config.UserConfig.Gui, service)
},
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 {
cmdStr := utils.ApplyTemplate(
gui.Config.UserConfig.CommandTemplates.Up,
gui.DockerCommand.NewCommandObject(commands.CommandObject{}),
gui.ContainerCommand.NewCommandObject(commands.CommandObject{}),
)
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 {
downCommand := utils.ApplyTemplate(
gui.Config.UserConfig.CommandTemplates.Down,
gui.DockerCommand.NewCommandObject(commands.CommandObject{}),
gui.ContainerCommand.NewCommandObject(commands.CommandObject{}),
)
downWithVolumesCommand := utils.ApplyTemplate(
gui.Config.UserConfig.CommandTemplates.DownWithVolumes,
gui.DockerCommand.NewCommandObject(commands.CommandObject{}),
gui.ContainerCommand.NewCommandObject(commands.CommandObject{}),
)
options := []*commandOption{
@ -363,12 +350,12 @@ func (gui *Gui) handleServiceRestartMenu(g *gocui.Gui, v *gocui.View) error {
rebuildCommand := utils.ApplyTemplate(
gui.Config.UserConfig.CommandTemplates.RebuildService,
gui.DockerCommand.NewCommandObject(commands.CommandObject{Service: service}),
gui.ContainerCommand.NewCommandObject(commands.CommandObject{Service: service}),
)
recreateCommand := utils.ApplyTemplate(
gui.Config.UserConfig.CommandTemplates.RecreateService,
gui.DockerCommand.NewCommandObject(commands.CommandObject{Service: service}),
gui.ContainerCommand.NewCommandObject(commands.CommandObject{Service: service}),
)
options := []*commandOption{
@ -376,7 +363,7 @@ func (gui *Gui) handleServiceRestartMenu(g *gocui.Gui, v *gocui.View) error {
description: gui.Tr.Restart,
command: utils.ApplyTemplate(
gui.Config.UserConfig.CommandTemplates.RestartService,
gui.DockerCommand.NewCommandObject(commands.CommandObject{Service: service}),
gui.ContainerCommand.NewCommandObject(commands.CommandObject{Service: service}),
),
onPress: 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,
command: utils.ApplyTemplate(
gui.Config.UserConfig.CommandTemplates.RecreateService,
gui.DockerCommand.NewCommandObject(commands.CommandObject{Service: service}),
gui.ContainerCommand.NewCommandObject(commands.CommandObject{Service: service}),
),
onPress: 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,
command: utils.ApplyTemplate(
gui.Config.UserConfig.CommandTemplates.RebuildService,
gui.DockerCommand.NewCommandObject(commands.CommandObject{Service: service}),
gui.ContainerCommand.NewCommandObject(commands.CommandObject{Service: service}),
),
onPress: func() error {
return gui.runSubprocess(gui.OSCommand.RunCustomCommand(rebuildCommand))
@ -433,7 +420,7 @@ func (gui *Gui) handleServicesCustomCommand(g *gocui.Gui, v *gocui.View) error {
return nil
}
commandObject := gui.DockerCommand.NewCommandObject(commands.CommandObject{
commandObject := gui.ContainerCommand.NewCommandObject(commands.CommandObject{
Service: service,
Container: service.Container,
})
@ -466,7 +453,7 @@ L:
func (gui *Gui) handleServicesBulkCommand(g *gocui.Gui, v *gocui.View) error {
bulkCommands := gui.Config.UserConfig.BulkCommands.Services
commandObject := gui.DockerCommand.NewCommandObject(commands.CommandObject{})
commandObject := gui.ContainerCommand.NewCommandObject(commands.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.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
} else {
gui.Views.Containers.Title = gui.Tr.StandaloneContainersTitle
@ -142,12 +142,13 @@ func (gui *Gui) createAllViews() error {
gui.Views.Volumes.Highlight = true
gui.Views.Volumes.Title = gui.Tr.VolumesTitle
gui.Views.Volumes.TitlePrefix = "[5]"
gui.Views.Volumes.SelBgColor = selectedLineBgColor
gui.Views.Volumes.TitlePrefix = "[5]"
gui.Views.Networks.TitlePrefix = "[6]"
gui.Views.Networks.Highlight = true
gui.Views.Networks.Title = gui.Tr.NetworksTitle
gui.Views.Networks.TitlePrefix = "[6]"
gui.Views.Networks.SelBgColor = selectedLineBgColor
gui.Views.Options.Frame = false

View file

@ -2,6 +2,7 @@ package gui
import (
"fmt"
"strings"
"github.com/fatih/color"
"github.com/jesseduffield/gocui"
@ -17,6 +18,10 @@ import (
func (gui *Gui) getVolumesPanel() *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]{
GetMainTabs: func() []panels.MainTab[*commands.Volume] {
return []panels.MainTab[*commands.Volume]{
@ -94,7 +99,7 @@ func (gui *Gui) reloadVolumes() error {
}
func (gui *Gui) refreshStateVolumes() error {
volumes, err := gui.DockerCommand.RefreshVolumes()
volumes, err := gui.ContainerCommand.RefreshVolumes()
if err != nil {
return err
}
@ -116,15 +121,23 @@ func (gui *Gui) handleVolumesRemoveMenu(g *gocui.Gui, v *gocui.View) error {
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{
{
description: gui.Tr.Remove,
command: utils.WithShortSha("docker volume rm " + volume.Name),
command: rmCmd,
force: false,
},
{
description: gui.Tr.ForceRemove,
command: utils.WithShortSha("docker volume rm --force " + volume.Name),
command: rmForceCmd,
force: true,
},
}
@ -150,9 +163,12 @@ func (gui *Gui) handleVolumesRemoveMenu(g *gocui.Gui, v *gocui.View) 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.WithWaitingStatus(gui.Tr.PruningStatus, func() error {
err := gui.DockerCommand.PruneVolumes()
err := gui.ContainerCommand.PruneVolumes()
if err != nil {
return gui.createErrorPanel(err.Error())
}
@ -161,13 +177,45 @@ func (gui *Gui) handlePruneVolumes() error {
}, 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 {
volume, err := gui.Panels.Volumes.GetSelectedItem()
if err != nil {
return nil
}
commandObject := gui.DockerCommand.NewCommandObject(commands.CommandObject{
commandObject := gui.ContainerCommand.NewCommandObject(commands.CommandObject{
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 {
baseBulkCommands := []config.CustomCommand{
{
baseBulkCommands := []config.CustomCommand{}
if gui.ContainerCommand == nil || gui.ContainerCommand.Supports(commands.FeatureVolumePrune) {
baseBulkCommands = append(baseBulkCommands, config.CustomCommand{
Name: gui.Tr.PruneVolumes,
InternalFunction: gui.handlePruneVolumes,
},
})
}
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)
}

View file

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

View file

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

View file

@ -11,18 +11,18 @@
//
// Example with period = time.Second and trailing = false:
//
// Whole seconds after first trigger...|0|0|0|0|1|1|1|1|
// Trigger() gets called...............|X| |X| | |X| | |
// Throttled code gets called..........|X| | | | |X| | |
// Whole seconds after first trigger...|0|0|0|0|1|1|1|1|
// Trigger() gets called...............|X| |X| | |X| | |
// Throttled code gets called..........|X| | | | |X| | |
//
// Note that the second trigger had no effect. The third Trigger() caused immediate execution of the
// throttled code.
//
// Example with period = time.Second and trailing = true:
//
// Whole seconds after first trigger...|0|0|0|0|1|1|1|1|
// Trigger() gets called...............|X| |X| | |X| | |
// Throttled code gets called..........|X| | | |X| | | |
// Whole seconds after first trigger...|0|0|0|0|1|1|1|1|
// Trigger() gets called...............|X| |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.
// 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
package jibber_jabber

View file

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

View file

@ -18,6 +18,7 @@
// tag is deprecated and thus should not be used.
// Go versions prior to 1.4 are disabled because they use a different layout
// 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
package spew

View file

@ -16,6 +16,7 @@
// 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"
// tag is deprecated and thus should not be used.
//go:build js || appengine || safe || disableunsafe || !go1.4
// +build js appengine safe disableunsafe !go1.4
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
package:
* Pointers are dereferenced and followed
* Circular data structures are detected and handled properly
* Custom Stringer/error interfaces are optionally invoked, including
on unexported types
* Custom types which only implement the Stringer/error interfaces via
a pointer receiver are optionally invoked when passing non-pointer
variables
* Byte arrays and slices are dumped like the hexdump -C command which
includes offsets, byte values in hex, and ASCII output
- Pointers are dereferenced and followed
- Circular data structures are detected and handled properly
- Custom Stringer/error interfaces are optionally invoked, including
on unexported types
- Custom types which only implement the Stringer/error interfaces via
a pointer receiver are optionally invoked when passing non-pointer
variables
- Byte arrays and slices are dumped like the hexdump -C command which
includes offsets, byte values in hex, and ASCII output
The configuration options are controlled by modifying the public members
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.
//
// Indent: " "
// MaxDepth: 0
// DisableMethods: false
// DisablePointerMethods: false
// ContinueOnMethod: false
// SortKeys: false
// Indent: " "
// MaxDepth: 0
// DisableMethods: false
// DisablePointerMethods: false
// ContinueOnMethod: false
// SortKeys: false
func NewDefaultConfig() *ConfigState {
return &ConfigState{Indent: " "}
}

View file

@ -21,35 +21,36 @@ debugging.
A quick overview of the additional features spew provides over the built-in
printing facilities for Go data types are as follows:
* Pointers are dereferenced and followed
* Circular data structures are detected and handled properly
* Custom Stringer/error interfaces are optionally invoked, including
on unexported types
* Custom types which only implement the Stringer/error interfaces via
a pointer receiver are optionally invoked when passing non-pointer
variables
* Byte arrays and slices are dumped like the hexdump -C command which
includes offsets, byte values in hex, and ASCII output (only when using
Dump style)
- Pointers are dereferenced and followed
- Circular data structures are detected and handled properly
- Custom Stringer/error interfaces are optionally invoked, including
on unexported types
- Custom types which only implement the Stringer/error interfaces via
a pointer receiver are optionally invoked when passing non-pointer
variables
- Byte arrays and slices are dumped like the hexdump -C command which
includes offsets, byte values in hex, and ASCII output (only when using
Dump style)
There are two different approaches spew allows for dumping Go data structures:
* Dump style which prints with newlines, customizable indentation,
and additional debug information such as types and all pointer addresses
used to indirect to the final value
* A custom Formatter interface that integrates cleanly with the standard fmt
package and replaces %v, %+v, %#v, and %#+v to provide inline printing
similar to the default %v while providing the additional functionality
outlined above and passing unsupported format verbs such as %x and %q
along to fmt
- Dump style which prints with newlines, customizable indentation,
and additional debug information such as types and all pointer addresses
used to indirect to the final value
- A custom Formatter interface that integrates cleanly with the standard fmt
package and replaces %v, %+v, %#v, and %#+v to provide inline printing
similar to the default %v while providing the additional functionality
outlined above and passing unsupported format verbs such as %x and %q
along to fmt
Quick Start
# Quick Start
This section demonstrates how to quickly get started with spew. See the
sections below for further details on formatting and configuration options.
To dump a variable with full newlines, indentation, type, and pointer
information use Dump, Fdump, or Sdump:
spew.Dump(myVar1, myVar2, ...)
spew.Fdump(someWriter, 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
%v (most compact), %+v (adds pointer addresses), %#v (adds types), or
%#+v (adds types and pointer addresses):
spew.Printf("myVar1: %v -- myVar2: %+v", myVar1, myVar2)
spew.Printf("myVar3: %#v -- myVar4: %#+v", myVar3, myVar4)
spew.Fprintf(someWriter, "myVar1: %v -- myVar2: %+v", myVar1, myVar2)
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
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.
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
Maximum number of levels to descend into nested data structures.
There is no limit by default.
- Indent
String to use for each indentation level for Dump functions.
It is a single space by default. A popular alternative is "\t".
* DisableMethods
Disables invocation of error and Stringer interface methods.
Method invocation is enabled by default.
- MaxDepth
Maximum number of levels to descend into nested data structures.
There is no limit by default.
* DisablePointerMethods
Disables invocation of error and Stringer interface methods on types
which only accept pointer receivers from non-pointer variables.
Pointer method invocation is enabled by default.
- DisableMethods
Disables invocation of error and Stringer interface methods.
Method invocation is enabled by default.
* DisablePointerAddresses
DisablePointerAddresses specifies whether to disable the printing of
pointer addresses. This is useful when diffing data structures in tests.
- DisablePointerMethods
Disables invocation of error and Stringer interface methods on types
which only accept pointer receivers from non-pointer variables.
Pointer method invocation is enabled by default.
* DisableCapacities
DisableCapacities specifies whether to disable the printing of
capacities for arrays, slices, maps and channels. This is useful when
diffing data structures in tests.
- DisablePointerAddresses
DisablePointerAddresses specifies whether to disable the printing of
pointer addresses. This is useful when diffing data structures in tests.
* ContinueOnMethod
Enables recursion into types after invoking error and Stringer interface
methods. Recursion after method invocation is disabled by default.
- DisableCapacities
DisableCapacities specifies whether to disable the printing of
capacities for arrays, slices, maps and channels. This is useful when
diffing data structures in tests.
* SortKeys
Specifies map keys should be sorted before being printed. Use
this to have a more deterministic, diffable output. Note that
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.
- ContinueOnMethod
Enables recursion into types after invoking error and Stringer interface
methods. Recursion after method invocation is disabled by default.
* 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.
- SortKeys
Specifies map keys should be sorted before being printed. Use
this to have a more deterministic, diffable output. Note that
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:
@ -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, ...)
Sample Dump Output
# Sample Dump Output
See the Dump example for details on the setup of the types and variables being
shown here.
@ -150,13 +153,14 @@ shown here.
Byte (and uint8) arrays and slices are displayed uniquely like the hexdump -C
command as shown.
([]uint8) (len=32 cap=32) {
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|
00000020 31 32 |12|
}
Custom Formatter
# Custom Formatter
Spew provides a custom formatter that implements the fmt.Formatter interface
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
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
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.
Sample Formatter Output
# Sample Formatter Output
Double pointer to a uint8:
%v: <**>5
%+v: <**>(0xf8400420d0->0xf8400420c8)5
%#v: (**uint8)5
%#+v: (**uint8)(0xf8400420d0->0xf8400420c8)5
Pointer to circular struct with a uint8 field and a pointer to itself:
%v: <*>{1 <*><shown>}
%+v: <*>(0xf84003e260){ui8:1 c:<*>(0xf84003e260)<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
here.
Errors
# Errors
Since it is possible for custom Stringer/error interfaces to panic, spew
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
package:
* Pointers are dereferenced and followed
* Circular data structures are detected and handled properly
* Custom Stringer/error interfaces are optionally invoked, including
on unexported types
* Custom types which only implement the Stringer/error interfaces via
a pointer receiver are optionally invoked when passing non-pointer
variables
* Byte arrays and slices are dumped like the hexdump -C command which
includes offsets, byte values in hex, and ASCII output
- Pointers are dereferenced and followed
- Circular data structures are detected and handled properly
- Custom Stringer/error interfaces are optionally invoked, including
on unexported types
- Custom types which only implement the Stringer/error interfaces via
a pointer receiver are optionally invoked when passing non-pointer
variables
- Byte arrays and slices are dumped like the hexdump -C command which
includes offsets, byte values in hex, and ASCII output
The configuration options are controlled by an exported package global,
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:
color.Cyan("Prints text in cyan.")
color.Cyan("Prints text in cyan.")
// a newline will be appended automatically
color.Blue("Prints %s in blue.", "text")
// a newline will be appended automatically
color.Blue("Prints %s in blue.", "text")
// More default foreground colors..
color.Red("We have red")
color.Yellow("Yellow color too!")
color.Magenta("And many others ..")
// More default foreground colors..
color.Red("We have red")
color.Yellow("Yellow color too!")
color.Magenta("And many others ..")
// Hi-intensity colors
color.HiGreen("Bright green color.")
color.HiBlack("Bright black means gray..")
color.HiWhite("Shiny white color!")
// Hi-intensity colors
color.HiGreen("Bright green color.")
color.HiBlack("Bright black means gray..")
color.HiWhite("Shiny white color!")
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
separate color object.
// Create a new color object
c := color.New(color.FgCyan).Add(color.Underline)
c.Println("Prints cyan text with an underline.")
// Create a new color object
c := color.New(color.FgCyan).Add(color.Underline)
c.Println("Prints cyan text with an underline.")
// Or just add them to New()
d := color.New(color.FgCyan, color.Bold)
d.Printf("This prints bold cyan %s\n", "too!.")
// Or just add them to New()
d := color.New(color.FgCyan, color.Bold)
d.Printf("This prints bold cyan %s\n", "too!.")
// Mix up foreground and background colors, create new mixes!
red := color.New(color.FgRed)
// Mix up foreground and background colors, create new mixes!
red := color.New(color.FgRed)
boldRed := red.Add(color.Bold)
boldRed.Println("This will print text in bold red.")
boldRed := red.Add(color.Bold)
boldRed.Println("This will print text in bold red.")
whiteBackground := red.Add(color.BgWhite)
whiteBackground.Println("Red text with White background.")
whiteBackground := red.Add(color.BgWhite)
whiteBackground.Println("Red text with White background.")
// Use your own io.Writer output
color.New(color.FgBlue).Fprintln(myWriter, "blue color!")
// Use your own io.Writer output
color.New(color.FgBlue).Fprintln(myWriter, "blue color!")
blue := color.New(color.FgBlue)
blue.Fprint(myWriter, "This will print text in blue.")
blue := color.New(color.FgBlue)
blue.Fprint(myWriter, "This will print text in blue.")
You can create PrintXxx functions to simplify even more:
// Create a custom print function for convenient
red := color.New(color.FgRed).PrintfFunc()
red("warning")
red("error: %s", err)
// Create a custom print function for convenient
red := color.New(color.FgRed).PrintfFunc()
red("warning")
red("error: %s", err)
// Mix up multiple attributes
notice := color.New(color.Bold, color.FgGreen).PrintlnFunc()
notice("don't forget this...")
// Mix up multiple attributes
notice := color.New(color.Bold, color.FgGreen).PrintlnFunc()
notice("don't forget this...")
You can also FprintXxx functions to pass your own io.Writer:
blue := color.New(FgBlue).FprintfFunc()
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...")
blue := color.New(FgBlue).FprintfFunc()
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...")
Or create SprintXxx functions to mix strings with other non-colorized strings:
yellow := New(FgYellow).SprintFunc()
red := New(FgRed).SprintFunc()
yellow := New(FgYellow).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()
fmt.Printf("this %s rocks!\n", info("package"))
info := New(FgWhite, BgGreen).SprintFunc()
fmt.Printf("this %s rocks!\n", info("package"))
Windows support is enabled by default. All Print functions work as intended.
However only for color.SprintXXX functions, user should use fmt.FprintXXX and
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()
fmt.Fprintf(color.Output, "this %s rocks!\n", info("package"))
info := New(FgWhite, BgGreen).SprintFunc()
fmt.Fprintf(color.Output, "this %s rocks!\n", info("package"))
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
code is not required.
// Use handy standard colors.
color.Set(color.FgYellow)
// Use handy standard colors.
color.Set(color.FgYellow)
fmt.Println("Existing text will be now in Yellow")
fmt.Printf("This one %s\n", "too")
fmt.Println("Existing text will be now in Yellow")
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
color.Set(color.FgMagenta, color.Bold)
defer color.Unset() // use it in your function
// You can mix up parameters
color.Set(color.FgMagenta, color.Bold)
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
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
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 {
color.NoColor = true // disables colorized output
}
if *flagNoColor {
color.NoColor = true // disables colorized output
}
It also has support for single color definitions (local). You can
disable/enable color output on the fly:
c := color.New(color.FgCyan)
c.Println("Prints cyan text")
c := color.New(color.FgCyan)
c.Println("Prints cyan text")
c.DisableColor()
c.Println("This is printed without any color")
c.DisableColor()
c.Println("This is printed without any color")
c.EnableColor()
c.Println("This prints again cyan...")
c.EnableColor()
c.Println("This prints again cyan...")
*/
package color

View file

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

View file

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

View file

@ -9,36 +9,36 @@
//
// 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 {
// return errors.New(Crashed)
// }
// func Crash() error {
// return errors.New(Crashed)
// }
//
// This can be called as follows:
//
// package main
// package main
//
// import (
// "crashy"
// "fmt"
// "github.com/go-errors/errors"
// )
// import (
// "crashy"
// "fmt"
// "github.com/go-errors/errors"
// )
//
// func main() {
// err := crashy.Crash()
// if err != nil {
// if errors.Is(err, crashy.Crashed) {
// fmt.Println(err.(*errors.Error).ErrorStack())
// } else {
// panic(err)
// }
// }
// }
// func main() {
// err := crashy.Crash()
// if err != nil {
// if errors.Is(err, crashy.Crashed) {
// fmt.Println(err.(*errors.Error).ErrorStack())
// } else {
// panic(err)
// }
// }
// }
//
// This package was original written to allow reporting to Bugsnag,
// 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:
//
// main.(*foo).destruct(0xc208067e98)
// /0/go/src/github.com/bugsnag/bugsnag-go/pan/main.go:22 +0x151
// main.(*foo).destruct(0xc208067e98)
// /0/go/src/github.com/bugsnag/bugsnag-go/pan/main.go:22 +0x151
func parsePanicFrame(name string, line string, createdBy bool) (*StackFrame, error) {
idx := strings.LastIndex(name, "(")
if idx == -1 && !createdBy {

View file

@ -89,43 +89,42 @@ func (s MapSlice) ToMap() map[interface{}]interface{} {
//
// The field tag format accepted is:
//
// `(...) yaml:"[<key>][,<flag1>[,<flag2>]]" (...)`
// `(...) yaml:"[<key>][,<flag1>[,<flag2>]]" (...)`
//
// The following flags are currently supported:
//
// omitempty Only include the field if it's not set to the zero
// value for the type or to empty slices or maps.
// Zero valued structs will be omitted if all their public
// fields are zero, unless they implement an IsZero
// method (see the IsZeroer interface type), in which
// case the field will be included if that method returns true.
// omitempty Only include the field if it's not set to the zero
// value for the type or to empty slices or maps.
// Zero valued structs will be omitted if all their public
// fields are zero, unless they implement an IsZero
// method (see the IsZeroer interface type), in which
// case the field will be included if that method returns true.
//
// flow Marshal using a flow style (useful for structs,
// sequences and maps).
// flow Marshal using a flow style (useful for structs,
// sequences and maps).
//
// inline Inline the field, which must be a struct or a map,
// causing all of its fields or keys to be processed as if
// they were part of the outer struct. For maps, keys must
// not conflict with the yaml keys of other struct fields.
// inline Inline the field, which must be a struct or a map,
// causing all of its fields or keys to be processed as if
// they were part of the outer struct. For maps, keys must
// 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.
// Otherwise, if used 'anchor' name only, used the field name lowercased as the anchor name
// 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
//
// 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,
// assigned anchor name automatically from same pointer address.
// 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,
// assigned anchor name automatically from same pointer address.
//
// In addition, if the key is "-", the field is ignored.
//
// For example:
//
// type T struct {
// F int `yaml:"a,omitempty"`
// B int
// }
// yaml.Marshal(&T{B: 2}) // Returns "b: 2\n"
// yaml.Marshal(&T{F: 1}) // Returns "a: 1\nb: 0\n"
//
// type T struct {
// F int `yaml:"a,omitempty"`
// B int
// }
// yaml.Marshal(&T{B: 2}) // Returns "b: 2\n"
// yaml.Marshal(&T{F: 1}) // Returns "a: 1\nb: 0\n"
func Marshal(v interface{}) ([]byte, error) {
return MarshalWithOptions(v)
}
@ -167,16 +166,15 @@ func ValueToNode(v interface{}, opts ...EncodeOption) (ast.Node, error) {
//
// For example:
//
// type T struct {
// F int `yaml:"a,omitempty"`
// B int
// }
// var t T
// yaml.Unmarshal([]byte("a: 1\nb: 2"), &t)
// type T struct {
// F int `yaml:"a,omitempty"`
// B int
// }
// var t T
// yaml.Unmarshal([]byte("a: 1\nb: 2"), &t)
//
// See the documentation of Marshal for the format of tags and a list of
// supported tag options.
//
func Unmarshal(data []byte, v interface{}) error {
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.
- 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,
and return the field's default value if unset.
The getters work even if the receiver is a nil message.
and return the field's default value if unset.
The getters work even if the receiver is a nil message.
- 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.
- 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.
- 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
have them. They have the form Default_StructName_FieldName.
Because the getter methods handle defaulted values,
direct use of these constants should be rare.
have them. They have the form Default_StructName_FieldName.
Because the getter methods handle defaulted values,
direct use of these constants should be rare.
- 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'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.
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
method, and a Enum method to assist in message construction.
- 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_,
followed by an underscore-delimited list of the nested messages
that contain it (if any) followed by the CamelCased name of the
extension field itself. HasExtension, ClearExtension, GetExtension
and SetExtension are functions for manipulating extensions.
followed by an underscore-delimited list of the nested messages
that contain it (if any) followed by the CamelCased name of the
extension field itself. HasExtension, ClearExtension, GetExtension
and SetExtension are functions for manipulating extensions.
- 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.
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
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
//go:build purego || appengine || js
// +build purego appengine js
// 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
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
//go:build purego || appengine || js
// +build purego appengine js
// 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
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
//go:build !purego && !appengine && !js
// +build !purego,!appengine,!js
// 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
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
//go:build !purego && !appengine && !js
// +build !purego,!appengine,!js
// 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.
// for:
// message Msg {
// oneof F {
// int64 X = 1;
// float64 Y = 2;
// }
// }
//
// message Msg {
// oneof F {
// int64 X = 1;
// float64 Y = 2;
// }
// }
//
// 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).
// 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
// ESC 操作的表示:
// "\033"(Octal 8进制) = "\x1b"(Hexadecimal 16进制) = 27 (10进制)
//
// "\033"(Octal 8进制) = "\x1b"(Hexadecimal 16进制) = 27 (10进制)
const (
SettingTpl = "\x1b[%sm"
FullColorTpl = "\x1b[%sm%s\x1b[0m"
@ -179,7 +180,8 @@ func InnerErrs() []error {
// RenderCode render message by color code.
// Usage:
// msg := RenderCode("3;32;45", "some", "message")
//
// msg := RenderCode("3;32;45", "some", "message")
func RenderCode(code string, args ...interface{}) string {
var message string
if ln := len(args); ln == 0 {
@ -217,7 +219,8 @@ func RenderWithSpaces(code string, args ...interface{}) string {
// RenderString render a string with color code.
// Usage:
// msg := RenderString("3;32;45", "a message")
//
// msg := RenderString("3;32;45", "a message")
func RenderString(code string, str string) string {
if len(code) == 0 || str == "" {
return str

View file

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

View file

@ -19,16 +19,19 @@ from wikipedia, 256 color:
// tpl for 8 bit 256 color(`2^8`)
//
// format:
// ESC[ … 38;5;<n> … m // 选择前景色
// ESC[ … 48;5;<n> … m // 选择背景色
//
// ESC[ … 38;5;<n> … m // 选择前景色
// ESC[ … 48;5;<n> … m // 选择背景色
//
// example:
// fg "\x1b[38;5;242m"
// bg "\x1b[48;5;208m"
// both "\x1b[38;5;242;48;5;208m"
//
// fg "\x1b[38;5;242m"
// bg "\x1b[48;5;208m"
// both "\x1b[38;5;242;48;5;208m"
//
// 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 (
TplFg256 = "38;5;%d"
TplBg256 = "48;5;%d"
@ -45,12 +48,14 @@ const (
// 颜色值使用10进制和16进制都可 0x98 = 152
//
// 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:
// 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
// lint warn - Name starts with package name
@ -210,9 +215,10 @@ type Style256 struct {
// S256 create a color256 style
// Usage:
// s := color.S256()
// s := color.S256(132) // fg
// s := color.S256(132, 203) // fg and bg
//
// s := color.S256()
// s := color.S256(132) // fg
// s := color.S256(132, 203) // fg and bg
func S256(fgAndBg ...uint8) *Style256 {
s := &Style256{}
vl := len(fgAndBg)

View file

@ -8,20 +8,24 @@ import (
// 24 bit RGB color
// 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:
// 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:
// 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:
// fg: \x1b[38;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
//
// fg: \x1b[38;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 (
TplFgRGB = "38;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
//
// Usage:
// // 0, 1, 2 is R,G,B.
// // 3rd: Fg=0, Bg=1, >1: unset value
// RGBColor{30,144,255, 0}
// RGBColor{30,144,255, 1}
//
// // 0, 1, 2 is R,G,B.
// // 3rd: Fg=0, Bg=1, >1: unset value
// RGBColor{30,144,255, 0}
// RGBColor{30,144,255, 1}
//
// NOTICE: now support RGB color on Windows CMD, PowerShell
type RGBColor [4]uint8
@ -79,9 +84,10 @@ var emptyRGBColor = RGBColor{3: 99}
// RGB color create.
// Usage:
// c := RGB(30,144,255)
// c := RGB(30,144,255, true)
// c.Print("message")
//
// c := RGB(30,144,255)
// c := RGB(30,144,255, true)
// c.Print("message")
func RGB(r, g, b uint8, isBg ...bool) RGBColor {
rgb := RGBColor{r, g, b}
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.
//
// Usage:
// c := HEX("ccc") // rgb: [204 204 204]
// c := HEX("aabbcc") // rgb: [170 187 204]
// c := HEX("#aabbcc")
// c := HEX("0xaabbcc")
// c.Print("message")
//
// c := HEX("ccc") // rgb: [204 204 204]
// c := HEX("aabbcc") // rgb: [170 187 204]
// c := HEX("#aabbcc")
// c := HEX("0xaabbcc")
// c.Print("message")
func HEX(hex string, isBg ...bool) RGBColor {
if rgb := HexToRgb(hex); len(rgb) > 0 {
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}
//
// Usage:
// c := RGBFromString("170,187,204")
// c.Print("message")
//
// c := RGBFromString("brown")
// c.Print("message with color brown")
// c := RGBFromString("170,187,204")
// c.Print("message")
//
// c := RGBFromString("brown")
// c.Print("message with color brown")
func RGBFromString(rgb string, isBg ...bool) RGBColor {
// use color name in the {namedRgbMap}
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.
// Usage:
// s := HEXStyle("aabbcc", "eee")
// s.Print("message")
//
// s := HEXStyle("aabbcc", "eee")
// s.Print("message")
func HEXStyle(fg string, bg ...string) *RGBStyle {
s := &RGBStyle{}
if len(bg) > 0 {
@ -344,8 +353,9 @@ func HEXStyle(fg string, bg ...string) *RGBStyle {
// RGBStyleFromString create a RGBStyle from color value string.
// 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 {
s := &RGBStyle{}
if len(bg) > 0 {

View file

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

View file

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

View file

@ -19,7 +19,8 @@ import (
// DetectColorLevel for current env
//
// 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 {
level, _ := detectTermColorLevel()
return level
@ -173,7 +174,9 @@ func detectWSL() bool {
}
// 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
func isWSL() bool {
// on windows WSL:
@ -238,7 +241,8 @@ func IsMSys() bool {
// IsSupportColor check current console is support color.
//
// 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 {
return IsSupport16Color()
}
@ -246,7 +250,8 @@ func IsSupportColor() bool {
// IsSupportColor check current console is support color.
//
// 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 {
level, _ := detectTermColorLevel()
return level > terminfo.ColorLevelNone
@ -255,7 +260,8 @@ func IsSupport16Color() bool {
// IsSupport256Color render check
//
// 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 {
level, _ := detectTermColorLevel()
return level > terminfo.ColorLevelBasic
@ -264,7 +270,8 @@ func IsSupport256Color() bool {
// IsSupportRGBColor check. alias of the IsSupportTrueColor()
//
// 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 {
return IsSupportTrueColor()
}
@ -272,7 +279,8 @@ func IsSupportRGBColor() bool {
// IsSupportTrueColor render check.
//
// 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:
// "COLORTERM=truecolor"

View file

@ -1,3 +1,4 @@
//go:build !windows
// +build !windows
// 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.
//
// Usage:
// IsTerminal(os.Stdout.Fd())
//
// IsTerminal(os.Stdout.Fd())
func IsTerminal(fd uintptr) bool {
return fd == uintptr(syscall.Stdout) || fd == uintptr(syscall.Stdin) || fd == uintptr(syscall.Stderr)
}

View file

@ -1,10 +1,12 @@
//go:build windows
// +build windows
// Display color on windows
// refer:
// golang.org/x/sys/windows
// golang.org/x/crypto/ssh/terminal
// https://docs.microsoft.com/en-us/windows/console
//
// golang.org/x/sys/windows
// golang.org/x/crypto/ssh/terminal
// https://docs.microsoft.com/en-us/windows/console
package color
import (
@ -111,8 +113,10 @@ var (
)
// 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
func detectSpecialTermColor(termVal string) (tl terminfo.ColorLevel, needVTP bool) {
if os.Getenv("ConEmuANSI") == "ON" {
@ -166,9 +170,10 @@ const (
// doc https://docs.microsoft.com/zh-cn/windows/console/console-virtual-terminal-sequences#samples
//
// Usage:
// err := EnableVirtualTerminalProcessing(syscall.Stdout, true)
// // support print color text
// err = EnableVirtualTerminalProcessing(syscall.Stdout, false)
//
// err := EnableVirtualTerminalProcessing(syscall.Stdout, true)
// // support print color text
// err = EnableVirtualTerminalProcessing(syscall.Stdout, false)
func EnableVirtualTerminalProcessing(stream syscall.Handle, enable bool) error {
var mode uint32
// 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.
//
// Usage:
// fd := os.Stdout.Fd()
// fd := uintptr(syscall.Stdout) // for windows
// IsTerminal(fd)
//
// fd := os.Stdout.Fd()
// fd := uintptr(syscall.Stdout) // for windows
// IsTerminal(fd)
func IsTerminal(fd uintptr) bool {
initKernel32Proc()

View file

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

View file

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

View file

@ -85,6 +85,7 @@ func Lprint(l *log.Logger, a ...interface{}) {
// Render parse color tags, return rendered string.
// Usage:
//
// text := Render("<info>hello</> <cyan>world</>!")
// fmt.Println(text)
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).
Status
# Status
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.
@ -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).
Install
# Install
Do your usual installation procedure:
go get github.com/imdario/mergo
go get github.com/imdario/mergo
// use in your .go code
import (
"github.com/imdario/mergo"
)
// use in your .go code
import (
"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).
@ -81,7 +81,7 @@ Here is a nice example:
// {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?
@ -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 }
}
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
About
# About
Written by Dario Castañé: https://da.rio.hn
License
# License
BSD 3-Clause license, as Go language.
*/
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:
// --path -> path
// -p -> p
//
// --path -> path
// -p -> p
func parseFlagToName(arg string) string {
// remove minus from start
arg = strings.TrimLeft(arg, "-")

View file

@ -138,7 +138,7 @@ func yaml_emitter_set_canonical(emitter *yaml_emitter_t, canonical bool) {
emitter.canonical = canonical
}
//// Set the indentation increment.
// // Set the indentation increment.
func yaml_emitter_set_indent(emitter *yaml_emitter_t, indent int) {
if indent < 2 || indent > 9 {
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.
//
// We accumulate extra
// - 1 event for DOCUMENT-START
// - 2 events for SEQUENCE-START
// - 3 events for MAPPING-START
//
// - 1 event for DOCUMENT-START
// - 2 events for SEQUENCE-START
// - 3 events for MAPPING-START
func yaml_emitter_need_more_events(emitter *yaml_emitter_t) bool {
if emitter.events_head == len(emitter.events) {
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:
// stream ::= STREAM-START implicit_document? explicit_document* STREAM-END
// ************
//
// ************
func yaml_parser_parse_stream_start(parser *yaml_parser_t, event *yaml_event_t) bool {
token := peek_token(parser)
if token == nil {
@ -192,9 +193,12 @@ func yaml_parser_parse_stream_start(parser *yaml_parser_t, event *yaml_event_t)
// Parse the productions:
// implicit_document ::= 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 {
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:
// explicit_document ::= DIRECTIVE* DOCUMENT-START block_node? DOCUMENT-END*
// ***********
//
// ***********
func yaml_parser_parse_document_content(parser *yaml_parser_t, event *yaml_event_t) bool {
token := peek_token(parser)
if token == nil {
@ -299,9 +303,10 @@ func yaml_parser_parse_document_content(parser *yaml_parser_t, event *yaml_event
// Parse the productions:
// 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 {
token := peek_token(parser)
if token == nil {
@ -332,30 +337,41 @@ func yaml_parser_parse_document_end(parser *yaml_parser_t, event *yaml_event_t)
// Parse the productions:
// block_node_or_indentless_sequence ::=
// ALIAS
// *****
// | properties (block_content | indentless_block_sequence)?
// ********** *
// | block_content | indentless_block_sequence
// *
//
// ALIAS
// *****
// | properties (block_content | indentless_block_sequence)?
// ********** *
// | block_content | indentless_block_sequence
// *
//
// block_node ::= ALIAS
// *****
// | properties block_content?
// ********** *
// | block_content
// *
//
// *****
// | properties block_content?
// ********** *
// | block_content
// *
//
// flow_node ::= ALIAS
// *****
// | properties flow_content?
// ********** *
// | flow_content
// *
//
// *****
// | properties flow_content?
// ********** *
// | flow_content
// *
//
// properties ::= TAG ANCHOR? | ANCHOR TAG?
// *************************
//
// *************************
//
// block_content ::= block_collection | 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 {
//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:
// 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 {
if first {
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:
// indentless_sequence ::= (BLOCK-ENTRY block_node?)+
// *********** *
//
// *********** *
func yaml_parser_parse_indentless_sequence_entry(parser *yaml_parser_t, event *yaml_event_t) bool {
token := peek_token(parser)
if token == nil {
@ -664,14 +681,14 @@ func yaml_parser_parse_indentless_sequence_entry(parser *yaml_parser_t, event *y
// Parse the productions:
// 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 {
if first {
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:
// 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_value(parser *yaml_parser_t, event *yaml_event_t) bool {
token := peek_token(parser)
if token == nil {
@ -757,16 +772,18 @@ func yaml_parser_parse_block_mapping_value(parser *yaml_parser_t, event *yaml_ev
// Parse the productions:
// 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 {
if first {
token := peek_token(parser)
@ -825,11 +842,10 @@ func yaml_parser_parse_flow_sequence_entry(parser *yaml_parser_t, event *yaml_ev
return true
}
//
// Parse the productions:
// 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 {
token := peek_token(parser)
if token == nil {
@ -849,8 +865,8 @@ func yaml_parser_parse_flow_sequence_entry_mapping_key(parser *yaml_parser_t, ev
// Parse the productions:
// 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 {
token := peek_token(parser)
if token == nil {
@ -873,8 +889,8 @@ func yaml_parser_parse_flow_sequence_entry_mapping_value(parser *yaml_parser_t,
// Parse the productions:
// 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 {
token := peek_token(parser)
if token == nil {
@ -891,16 +907,17 @@ func yaml_parser_parse_flow_sequence_entry_mapping_end(parser *yaml_parser_t, ev
// Parse the productions:
// 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 {
if first {
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:
// 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 {
token := peek_token(parser)
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.
// 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 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
}
} 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 true || intv == int64(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.
//
// 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 {
// Eat '%'.
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.
//
// 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 {
// Consume the directive name.
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.
//
// 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 {
// Eat whitespaces.
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.
//
// 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 {
// 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.
//
// 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 {
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 an, bn int64
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' {
an = 1
bn = 1

View file

@ -2,8 +2,7 @@
//
// 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
import (
@ -67,16 +66,15 @@ type Marshaler interface {
//
// For example:
//
// type T struct {
// F int `yaml:"a,omitempty"`
// B int
// }
// var t T
// yaml.Unmarshal([]byte("a: 1\nb: 2"), &t)
// type T struct {
// F int `yaml:"a,omitempty"`
// B int
// }
// var t T
// yaml.Unmarshal([]byte("a: 1\nb: 2"), &t)
//
// See the documentation of Marshal for the format of tags and a list of
// supported tag options.
//
func Unmarshal(in []byte, out interface{}) (err error) {
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:
//
// `(...) yaml:"[<key>][,<flag1>[,<flag2>]]" (...)`
// `(...) yaml:"[<key>][,<flag1>[,<flag2>]]" (...)`
//
// The following flags are currently supported:
//
// omitempty Only include the field if it's not set to the zero
// value for the type or to empty slices or maps.
// Zero valued structs will be omitted if all their public
// fields are zero, unless they implement an IsZero
// method (see the IsZeroer interface type), in which
// case the field will be included if that method returns true.
// omitempty Only include the field if it's not set to the zero
// value for the type or to empty slices or maps.
// Zero valued structs will be omitted if all their public
// fields are zero, unless they implement an IsZero
// method (see the IsZeroer interface type), in which
// case the field will be included if that method returns true.
//
// flow Marshal using a flow style (useful for structs,
// sequences and maps).
// flow Marshal using a flow style (useful for structs,
// sequences and maps).
//
// inline Inline the field, which must be a struct or a map,
// causing all of its fields or keys to be processed as if
// they were part of the outer struct. For maps, keys must
// not conflict with the yaml keys of other struct fields.
// inline Inline the field, which must be a struct or a map,
// causing all of its fields or keys to be processed as if
// they were part of the outer struct. For maps, keys must
// not conflict with the yaml keys of other struct fields.
//
// In addition, if the key is "-", the field is ignored.
//
// For example:
//
// type T struct {
// F int `yaml:"a,omitempty"`
// B int
// }
// yaml.Marshal(&T{B: 2}) // Returns "b: 2\n"
// yaml.Marshal(&T{F: 1}} // Returns "a: 1\nb: 0\n"
//
// type T struct {
// F int `yaml:"a,omitempty"`
// B int
// }
// yaml.Marshal(&T{B: 2}) // Returns "b: 2\n"
// yaml.Marshal(&T{F: 1}} // Returns "a: 1\nb: 0\n"
func Marshal(in interface{}) (out []byte, err error) {
defer handleErr(&err)
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.
//
// [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.
// [in] size The size of the buffer.
// [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.
//
// @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] size The size of the buffer.
//
// @returns On success, the handler should return @c 1. If the handler failed,
// the returned value should be @c 0.
//
type yaml_write_handler_t func(emitter *yaml_emitter_t, buffer []byte) error
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.
//
// 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}
func LuvLChToHSLuv(l, c, h float64) (float64, float64, float64) {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,5 +1,5 @@
// +build windows
// +build !appengine
//go:build windows && !appengine
// +build windows,!appengine
package isatty
@ -42,7 +42,8 @@ func IsTerminal(fd uintptr) bool {
// Check pipe name is used for cygwin/msys2 pty.
// 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 {
token := strings.Split(name, "-")
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,
// 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
//
// 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
// or closure that returns a function with this signature
//
// func(string) string
//
// func(string) string
package str

View file

@ -30,7 +30,7 @@ import (
//
// 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
// terms.

View file

@ -19,16 +19,16 @@
// More importantly, it provides tools and wrappers to work with
// hash.Hash-based digests with little effort.
//
// Basics
// # Basics
//
// The format of a digest is simply a string with two parts, dubbed the
// "algorithm" and the "digest", separated by a colon:
//
// <algorithm>:<digest>
// <algorithm>:<digest>
//
// An example of a sha256 digest representation follows:
//
// sha256:7173b809ca12ec5dee4506cd86be934c4596dd234ee82c0662eac04a8c2c71dc
// sha256:7173b809ca12ec5dee4506cd86be934c4596dd234ee82c0662eac04a8c2c71dc
//
// The "algorithm" portion defines both the hashing algorithm used to calculate
// 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
// standard equality operator.
//
// Verification
// # Verification
//
// The main benefit of using the Digest type is simple verification against a
// 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
// 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
// package:
@ -58,5 +58,4 @@
// 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.
//
package digest

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -13,6 +13,7 @@
// permissions and limitations under the License. See the AUTHORS file
// 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
package goid

View file

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

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