mirror of
https://github.com/jesseduffield/lazydocker.git
synced 2026-07-21 23:01:03 +00:00
lots of changes
This commit is contained in:
parent
2f1c246fd7
commit
2e9a80fe6c
9 changed files with 171 additions and 68 deletions
|
|
@ -162,8 +162,8 @@ func (s *ContainerStats) RenderStats(viewWidth int, cpuUsageHistory []float64, m
|
|||
fmt.Sprintf(
|
||||
"%.2f%% Memory (%s/%s)",
|
||||
memoryUsageHistory[len(memoryUsageHistory)-1],
|
||||
formatBinaryBytes(s.MemoryStats.Usage),
|
||||
formatBinaryBytes(int(s.MemoryStats.Limit)),
|
||||
utils.FormatBinaryBytes(s.MemoryStats.Usage),
|
||||
utils.FormatBinaryBytes(int(s.MemoryStats.Limit)),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
|
@ -178,8 +178,8 @@ func (s *ContainerStats) RenderStats(viewWidth int, cpuUsageHistory []float64, m
|
|||
)
|
||||
|
||||
pidsCount := fmt.Sprintf("PIDs: %d", s.PidsStats.Current)
|
||||
dataReceived := fmt.Sprintf("Traffic received: %s", formatDecimalBytes(s.Networks.Eth0.RxBytes))
|
||||
dataSent := fmt.Sprintf("Traffic sent: %s", formatDecimalBytes(s.Networks.Eth0.TxBytes))
|
||||
dataReceived := fmt.Sprintf("Traffic received: %s", utils.FormatDecimalBytes(s.Networks.Eth0.RxBytes))
|
||||
dataSent := fmt.Sprintf("Traffic sent: %s", utils.FormatDecimalBytes(s.Networks.Eth0.TxBytes))
|
||||
|
||||
originalJSON, err := json.MarshalIndent(s, "", " ")
|
||||
if err != nil {
|
||||
|
|
@ -197,29 +197,3 @@ func (s *ContainerStats) RenderStats(viewWidth int, cpuUsageHistory []float64, m
|
|||
|
||||
return contents, nil
|
||||
}
|
||||
|
||||
func formatBinaryBytes(b int) string {
|
||||
n := float64(b)
|
||||
units := []string{"B", "kiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB"}
|
||||
for _, unit := range units {
|
||||
if n > math.Pow(2, 10) {
|
||||
n = n / math.Pow(2, 10)
|
||||
} else {
|
||||
return fmt.Sprintf("%.2f%s", n, unit)
|
||||
}
|
||||
}
|
||||
return "a lot"
|
||||
}
|
||||
|
||||
func formatDecimalBytes(b int) string {
|
||||
n := float64(b)
|
||||
units := []string{"B", "kB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"}
|
||||
for _, unit := range units {
|
||||
if n > math.Pow(10, 3) {
|
||||
n = n / math.Pow(10, 3)
|
||||
} else {
|
||||
return fmt.Sprintf("%.2f%s", n, unit)
|
||||
}
|
||||
}
|
||||
return "a lot"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -78,11 +78,6 @@ func (c *DockerCommand) GetImages() ([]*Image, error) {
|
|||
for i, image := range images {
|
||||
// func (cli *Client) ImageHistory(ctx context.Context, imageID string) ([]image.HistoryResponseItem, error)
|
||||
|
||||
history, err := c.Client.ImageHistory(context.Background(), image.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
name := "none"
|
||||
tags := image.RepoTags
|
||||
if len(tags) > 0 {
|
||||
|
|
@ -99,7 +94,6 @@ func (c *DockerCommand) GetImages() ([]*Image, error) {
|
|||
Client: c.Client,
|
||||
OSCommand: c.OSCommand,
|
||||
Log: c.Log,
|
||||
History: history,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,9 @@ package commands
|
|||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"github.com/davecgh/go-spew/spew"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/fatih/color"
|
||||
|
|
@ -16,7 +18,6 @@ type Image struct {
|
|||
Tag string
|
||||
ID string
|
||||
Image types.ImageSummary
|
||||
History []types.ImageHistory
|
||||
Client *client.Client
|
||||
OSCommand *OSCommand
|
||||
Log *logrus.Entry
|
||||
|
|
@ -24,7 +25,8 @@ type Image struct {
|
|||
|
||||
// GetDisplayStrings returns the display string of Image
|
||||
func (i *Image) GetDisplayStrings(isFocused bool) []string {
|
||||
return []string{utils.ColoredString(i.Name, color.FgWhite), utils.ColoredString(i.Tag, color.FgWhite)}
|
||||
|
||||
return []string{utils.ColoredString(i.Name, color.FgWhite), utils.ColoredString(i.Tag, color.FgWhite), utils.FormatDecimalBytes(int(i.Image.Size))}
|
||||
}
|
||||
|
||||
// Remove removes the image
|
||||
|
|
@ -35,3 +37,66 @@ func (i *Image) Remove(options types.ImageRemoveOptions) error {
|
|||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Layer is a layer in an image's history
|
||||
type Layer struct {
|
||||
types.ImageHistory
|
||||
}
|
||||
|
||||
// GetDisplayStrings returns the array of strings describing the layer
|
||||
func (l *Layer) GetDisplayStrings(isFocused bool) []string {
|
||||
tag := ""
|
||||
if len(l.Tags) > 0 {
|
||||
tag = l.Tags[0]
|
||||
}
|
||||
|
||||
id := strings.TrimPrefix(l.ID, "sha256:")
|
||||
if len(id) > 10 {
|
||||
id = id[0:10]
|
||||
}
|
||||
idColor := color.FgWhite
|
||||
if id == "<missing>" {
|
||||
idColor = color.FgBlack
|
||||
}
|
||||
|
||||
dockerFileCommandPrefix := "/bin/sh -c #(nop) "
|
||||
createdBy := l.CreatedBy
|
||||
if strings.Contains(l.CreatedBy, dockerFileCommandPrefix) {
|
||||
createdBy = strings.Trim(strings.TrimPrefix(l.CreatedBy, dockerFileCommandPrefix), " ")
|
||||
split := strings.Split(createdBy, " ")
|
||||
createdBy = utils.ColoredString(split[0], color.FgYellow) + " " + strings.Join(split[1:], " ")
|
||||
}
|
||||
|
||||
createdBy = strings.ReplaceAll(createdBy, "\t", " ")
|
||||
|
||||
size := utils.FormatBinaryBytes(int(l.Size))
|
||||
sizeColor := color.FgWhite
|
||||
if size == "0B" {
|
||||
sizeColor = color.FgBlack
|
||||
}
|
||||
|
||||
return []string{
|
||||
utils.ColoredString(id, idColor),
|
||||
utils.ColoredString(tag, color.FgGreen),
|
||||
utils.ColoredString(size, sizeColor),
|
||||
createdBy,
|
||||
}
|
||||
}
|
||||
|
||||
// RenderHistory renders the history of the image
|
||||
func (i *Image) RenderHistory() (string, error) {
|
||||
|
||||
history, err := i.Client.ImageHistory(context.Background(), i.ID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
i.Log.Warn(spew.Sdump(history))
|
||||
|
||||
layers := make([]*Layer, len(history))
|
||||
for i, layer := range history {
|
||||
layers[i] = &Layer{layer}
|
||||
}
|
||||
|
||||
return utils.RenderList(layers, utils.WithHeader([]string{"ID", "TAG", "SIZE", "COMMAND"}))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -215,7 +215,7 @@ func (gui *Gui) refreshContainers() error {
|
|||
|
||||
containersView.Clear()
|
||||
isFocused := gui.g.CurrentView().Name() == "containers"
|
||||
list, err := utils.RenderList(gui.State.Containers, isFocused)
|
||||
list, err := utils.RenderList(gui.State.Containers, utils.IsFocused(isFocused))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
package gui
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/fatih/color"
|
||||
|
|
@ -96,21 +97,25 @@ func (gui *Gui) handleImageSelect(g *gocui.Gui, v *gocui.View) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) renderImageConfig(mainView *gocui.View, Image *commands.Image, writerID int) error {
|
||||
func (gui *Gui) renderImageConfig(mainView *gocui.View, image *commands.Image, writerID int) error {
|
||||
mainView.Autoscroll = false
|
||||
mainView.Wrap = false
|
||||
mainView.Title = "Config"
|
||||
|
||||
data, err := json.MarshalIndent(&Image.Image, "", " ")
|
||||
output := ""
|
||||
output += utils.WithPadding("ID: ", 10) + image.Image.ID + "\n"
|
||||
output += utils.WithPadding("Tags: ", 10) + utils.ColoredString(strings.Join(image.Image.RepoTags, ", "), color.FgGreen) + "\n"
|
||||
output += utils.WithPadding("Size: ", 10) + utils.FormatDecimalBytes(int(image.Image.Size)) + "\n"
|
||||
output += utils.WithPadding("Created: ", 10) + fmt.Sprintf("%v", time.Unix(image.Image.Created, 0).Format(time.RFC1123)) + "\n"
|
||||
|
||||
history, err := image.RenderHistory()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
historyData, err := json.MarshalIndent(&Image.History, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
output += "\n\n" + history
|
||||
|
||||
gui.renderString(gui.g, "main", string(data)+"\n"+string(historyData))
|
||||
gui.renderString(gui.g, "main", output)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
@ -136,7 +141,7 @@ func (gui *Gui) refreshImages() error {
|
|||
|
||||
ImagesView.Clear()
|
||||
isFocused := gui.g.CurrentView().Name() == "Images"
|
||||
list, err := utils.RenderList(gui.State.Images, isFocused)
|
||||
list, err := utils.RenderList(gui.State.Images, utils.IsFocused(isFocused))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ func (gui *Gui) handleMenuClose(g *gocui.Gui, v *gocui.View) error {
|
|||
func (gui *Gui) createMenu(title string, items interface{}, itemCount int, handlePress func(int) error) error {
|
||||
isFocused := gui.g.CurrentView().Name() == "menu"
|
||||
gui.State.MenuItemCount = itemCount
|
||||
list, err := utils.RenderList(items, isFocused)
|
||||
list, err := utils.RenderList(items, utils.IsFocused(isFocused))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ func (gui *Gui) nextView(g *gocui.Gui, v *gocui.View) error {
|
|||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
gui.State.Panels.Main.ObjectKey = ""
|
||||
gui.resetMainView()
|
||||
return gui.switchFocus(g, v, focusedView)
|
||||
}
|
||||
|
||||
|
|
@ -68,10 +68,15 @@ func (gui *Gui) previousView(g *gocui.Gui, v *gocui.View) error {
|
|||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
gui.State.Panels.Main.ObjectKey = ""
|
||||
gui.resetMainView()
|
||||
return gui.switchFocus(g, v, focusedView)
|
||||
}
|
||||
|
||||
func (gui *Gui) resetMainView() {
|
||||
gui.State.Panels.Main.ObjectKey = ""
|
||||
gui.getMainView().Wrap = true
|
||||
}
|
||||
|
||||
func (gui *Gui) newLineFocused(g *gocui.Gui, v *gocui.View) error {
|
||||
switch v.Name() {
|
||||
case "menu":
|
||||
|
|
@ -307,7 +312,7 @@ func (gui *Gui) refreshSelectedLine(line *int, total int) {
|
|||
func (gui *Gui) renderListPanel(v *gocui.View, items interface{}) error {
|
||||
gui.g.Update(func(g *gocui.Gui) error {
|
||||
isFocused := gui.g.CurrentView().Name() == v.Name()
|
||||
list, err := utils.RenderList(items, isFocused)
|
||||
list, err := utils.RenderList(items, utils.IsFocused(isFocused))
|
||||
if err != nil {
|
||||
return gui.createErrorPanel(gui.g, err.Error())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package utils
|
|||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
|
@ -95,10 +96,32 @@ type Displayable interface {
|
|||
GetDisplayStrings(bool) []string
|
||||
}
|
||||
|
||||
type RenderListConfig struct {
|
||||
IsFocused bool
|
||||
Header []string
|
||||
}
|
||||
|
||||
func IsFocused(isFocused bool) func(c *RenderListConfig) {
|
||||
return func(c *RenderListConfig) {
|
||||
c.IsFocused = isFocused
|
||||
}
|
||||
}
|
||||
|
||||
func WithHeader(header []string) func(c *RenderListConfig) {
|
||||
return func(c *RenderListConfig) {
|
||||
c.Header = header
|
||||
}
|
||||
}
|
||||
|
||||
// RenderList takes a slice of items, confirms they implement the Displayable
|
||||
// interface, then generates a list of their displaystrings to write to a panel's
|
||||
// buffer
|
||||
func RenderList(slice interface{}, isFocused bool) (string, error) {
|
||||
func RenderList(slice interface{}, options ...func(*RenderListConfig)) (string, error) {
|
||||
config := &RenderListConfig{}
|
||||
for _, option := range options {
|
||||
option(config)
|
||||
}
|
||||
|
||||
s := reflect.ValueOf(slice)
|
||||
if s.Kind() != reflect.Slice {
|
||||
return "", errors.New("RenderList given a non-slice type")
|
||||
|
|
@ -114,19 +137,22 @@ func RenderList(slice interface{}, isFocused bool) (string, error) {
|
|||
displayables[i] = value
|
||||
}
|
||||
|
||||
return renderDisplayableList(displayables, isFocused)
|
||||
return renderDisplayableList(displayables, *config)
|
||||
}
|
||||
|
||||
// renderDisplayableList takes a list of displayable items, obtains their display
|
||||
// strings via GetDisplayStrings() and then returns a single string containing
|
||||
// each item's string representation on its own line, with appropriate horizontal
|
||||
// padding between the item's own strings
|
||||
func renderDisplayableList(items []Displayable, isFocused bool) (string, error) {
|
||||
func renderDisplayableList(items []Displayable, config RenderListConfig) (string, error) {
|
||||
if len(items) == 0 {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
stringArrays := getDisplayStringArrays(items, isFocused)
|
||||
stringArrays := getDisplayStringArrays(items, config.IsFocused)
|
||||
if len(config.Header) > 0 {
|
||||
stringArrays = append([][]string{config.Header}, stringArrays...)
|
||||
}
|
||||
|
||||
if !displayArraysAligned(stringArrays) {
|
||||
return "", errors.New("Each item must return the same number of strings to display")
|
||||
|
|
@ -228,3 +254,37 @@ func AsJson(i interface{}) string {
|
|||
bytes, _ := json.MarshalIndent(i, "", " ")
|
||||
return string(bytes)
|
||||
}
|
||||
|
||||
func FormatBinaryBytes(b int) string {
|
||||
n := float64(b)
|
||||
units := []string{"B", "kiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB"}
|
||||
for _, unit := range units {
|
||||
if n > math.Pow(2, 10) {
|
||||
n = n / math.Pow(2, 10)
|
||||
} else {
|
||||
val := fmt.Sprintf("%.2f%s", n, unit)
|
||||
if val == "0.00B" {
|
||||
return "0B"
|
||||
}
|
||||
return val
|
||||
}
|
||||
}
|
||||
return "a lot"
|
||||
}
|
||||
|
||||
func FormatDecimalBytes(b int) string {
|
||||
n := float64(b)
|
||||
units := []string{"B", "kB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"}
|
||||
for _, unit := range units {
|
||||
if n > math.Pow(10, 3) {
|
||||
n = n / math.Pow(10, 3)
|
||||
} else {
|
||||
val := fmt.Sprintf("%.2f%s", n, unit)
|
||||
if val == "0.00B" {
|
||||
return "0B"
|
||||
}
|
||||
return val
|
||||
}
|
||||
}
|
||||
return "a lot"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -246,7 +246,7 @@ func TestGetDisplayStringArrays(t *testing.T) {
|
|||
func TestRenderDisplayableList(t *testing.T) {
|
||||
type scenario struct {
|
||||
input []Displayable
|
||||
isFocused bool
|
||||
config RenderListConfig
|
||||
expectedString string
|
||||
expectedErrorMessage string
|
||||
}
|
||||
|
|
@ -257,7 +257,7 @@ func TestRenderDisplayableList(t *testing.T) {
|
|||
Displayable(&myDisplayable{[]string{}}),
|
||||
Displayable(&myDisplayable{[]string{}}),
|
||||
},
|
||||
false,
|
||||
RenderListConfig{},
|
||||
"\n",
|
||||
"",
|
||||
},
|
||||
|
|
@ -266,7 +266,7 @@ func TestRenderDisplayableList(t *testing.T) {
|
|||
Displayable(&myDisplayable{[]string{"aa", "b"}}),
|
||||
Displayable(&myDisplayable{[]string{"c", "d"}}),
|
||||
},
|
||||
false,
|
||||
RenderListConfig{},
|
||||
"aa b\nc d",
|
||||
"",
|
||||
},
|
||||
|
|
@ -275,7 +275,7 @@ func TestRenderDisplayableList(t *testing.T) {
|
|||
Displayable(&myDisplayable{[]string{"a"}}),
|
||||
Displayable(&myDisplayable{[]string{"b", "c"}}),
|
||||
},
|
||||
false,
|
||||
RenderListConfig{},
|
||||
"",
|
||||
"Each item must return the same number of strings to display",
|
||||
},
|
||||
|
|
@ -284,14 +284,14 @@ func TestRenderDisplayableList(t *testing.T) {
|
|||
Displayable(&myDisplayable{[]string{"a"}}),
|
||||
Displayable(&myDisplayable{[]string{"b"}}),
|
||||
},
|
||||
true,
|
||||
RenderListConfig{IsFocused: true},
|
||||
"a blah\nb blah",
|
||||
"",
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
str, err := renderDisplayableList(s.input, s.isFocused)
|
||||
str, err := renderDisplayableList(s.input, s.config)
|
||||
assert.EqualValues(t, s.expectedString, str)
|
||||
if s.expectedErrorMessage != "" {
|
||||
assert.EqualError(t, err, s.expectedErrorMessage)
|
||||
|
|
@ -305,7 +305,7 @@ func TestRenderDisplayableList(t *testing.T) {
|
|||
func TestRenderList(t *testing.T) {
|
||||
type scenario struct {
|
||||
input interface{}
|
||||
isFocused bool
|
||||
options []func(*RenderListConfig)
|
||||
expectedString string
|
||||
expectedErrorMessage string
|
||||
}
|
||||
|
|
@ -316,7 +316,7 @@ func TestRenderList(t *testing.T) {
|
|||
{[]string{"aa", "b"}},
|
||||
{[]string{"c", "d"}},
|
||||
},
|
||||
false,
|
||||
nil,
|
||||
"aa b\nc d",
|
||||
"",
|
||||
},
|
||||
|
|
@ -325,13 +325,13 @@ func TestRenderList(t *testing.T) {
|
|||
{},
|
||||
{},
|
||||
},
|
||||
false,
|
||||
nil,
|
||||
"",
|
||||
"item does not implement the Displayable interface",
|
||||
},
|
||||
{
|
||||
&myStruct{},
|
||||
false,
|
||||
nil,
|
||||
"",
|
||||
"RenderList given a non-slice type",
|
||||
},
|
||||
|
|
@ -339,14 +339,14 @@ func TestRenderList(t *testing.T) {
|
|||
[]*myDisplayable{
|
||||
{[]string{"a"}},
|
||||
},
|
||||
true,
|
||||
[]func(*RenderListConfig){IsFocused(true)},
|
||||
"a blah",
|
||||
"",
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
str, err := RenderList(s.input, s.isFocused)
|
||||
str, err := RenderList(s.input, s.options...)
|
||||
assert.EqualValues(t, s.expectedString, str)
|
||||
if s.expectedErrorMessage != "" {
|
||||
assert.EqualError(t, err, s.expectedErrorMessage)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue