feat: pull progress
This commit is contained in:
parent
b231e8736c
commit
797ef57417
@ -167,6 +167,7 @@ func (a *Client) RunContainer(ctx context.Context, params RunContainerParams) (<
|
|||||||
defer close(errC)
|
defer close(errC)
|
||||||
|
|
||||||
if err := a.pullImageIfNeeded(ctx, params.ContainerConfig.Image, containerStateC); err != nil {
|
if err := a.pullImageIfNeeded(ctx, params.ContainerConfig.Image, containerStateC); err != nil {
|
||||||
|
a.logger.Error("Error pulling image", "err", err)
|
||||||
sendError(fmt.Errorf("image pull: %w", err))
|
sendError(fmt.Errorf("image pull: %w", err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -209,12 +210,30 @@ func (a *Client) RunContainer(ctx context.Context, params RunContainerParams) (<
|
|||||||
|
|
||||||
containerStateC <- domain.Container{ID: createResp.ID, Status: domain.ContainerStatusRunning}
|
containerStateC <- domain.Container{ID: createResp.ID, Status: domain.ContainerStatusRunning}
|
||||||
|
|
||||||
a.runContainerLoop(ctx, createResp.ID, params.NetworkCountConfig, containerStateC, errC)
|
a.runContainerLoop(
|
||||||
|
ctx,
|
||||||
|
createResp.ID,
|
||||||
|
params.ContainerConfig.Image,
|
||||||
|
params.NetworkCountConfig,
|
||||||
|
containerStateC,
|
||||||
|
errC,
|
||||||
|
)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
return containerStateC, errC
|
return containerStateC, errC
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type pullProgressDetail struct {
|
||||||
|
Curr int64 `json:"current"`
|
||||||
|
Total int64 `json:"total"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type pullProgress struct {
|
||||||
|
Status string `json:"status"`
|
||||||
|
Detail pullProgressDetail `json:"progressDetail"`
|
||||||
|
Progress string `json:"progress"`
|
||||||
|
}
|
||||||
|
|
||||||
// pullImageIfNeeded pulls the image if it has not already been pulled.
|
// pullImageIfNeeded pulls the image if it has not already been pulled.
|
||||||
func (a *Client) pullImageIfNeeded(ctx context.Context, imageName string, containerStateC chan<- domain.Container) error {
|
func (a *Client) pullImageIfNeeded(ctx context.Context, imageName string, containerStateC chan<- domain.Container) error {
|
||||||
a.mu.Lock()
|
a.mu.Lock()
|
||||||
@ -225,14 +244,9 @@ func (a *Client) pullImageIfNeeded(ctx context.Context, imageName string, contai
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
containerStateC <- domain.Container{Status: domain.ContainerStatusPulling}
|
if err := handleImagePull(ctx, imageName, a.apiClient, containerStateC); err != nil {
|
||||||
|
|
||||||
pullReader, err := a.apiClient.ImagePull(ctx, imageName, image.PullOptions{})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
_, _ = io.Copy(io.Discard, pullReader)
|
|
||||||
_ = pullReader.Close()
|
|
||||||
|
|
||||||
a.mu.Lock()
|
a.mu.Lock()
|
||||||
a.pulledImages[imageName] = struct{}{}
|
a.pulledImages[imageName] = struct{}{}
|
||||||
@ -246,6 +260,7 @@ func (a *Client) pullImageIfNeeded(ctx context.Context, imageName string, contai
|
|||||||
func (a *Client) runContainerLoop(
|
func (a *Client) runContainerLoop(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
containerID string,
|
containerID string,
|
||||||
|
imageName string,
|
||||||
networkCountConfig NetworkCountConfig,
|
networkCountConfig NetworkCountConfig,
|
||||||
stateC chan<- domain.Container,
|
stateC chan<- domain.Container,
|
||||||
errC chan<- error,
|
errC chan<- error,
|
||||||
@ -297,7 +312,11 @@ func (a *Client) runContainerLoop(
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
state := &domain.Container{ID: containerID, Status: domain.ContainerStatusRunning}
|
state := &domain.Container{
|
||||||
|
ID: containerID,
|
||||||
|
Status: domain.ContainerStatusRunning,
|
||||||
|
ImageName: imageName,
|
||||||
|
}
|
||||||
sendState := func() { stateC <- *state }
|
sendState := func() { stateC <- *state }
|
||||||
sendState()
|
sendState()
|
||||||
|
|
||||||
|
53
internal/container/pull.go
Normal file
53
internal/container/pull.go
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
package container
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"git.netflux.io/rob/octoplex/internal/domain"
|
||||||
|
"github.com/docker/docker/api/types/image"
|
||||||
|
)
|
||||||
|
|
||||||
|
func handleImagePull(
|
||||||
|
ctx context.Context,
|
||||||
|
imageName string,
|
||||||
|
dockerClient DockerClient,
|
||||||
|
containerStateC chan<- domain.Container,
|
||||||
|
) error {
|
||||||
|
containerStateC <- domain.Container{
|
||||||
|
Status: domain.ContainerStatusPulling,
|
||||||
|
ImageName: imageName,
|
||||||
|
PullStatus: "Waiting",
|
||||||
|
}
|
||||||
|
|
||||||
|
pullReader, err := dockerClient.ImagePull(ctx, imageName, image.PullOptions{})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
pullDecoder := json.NewDecoder(pullReader)
|
||||||
|
var pp pullProgress
|
||||||
|
for {
|
||||||
|
if err := pullDecoder.Decode(&pp); err != nil {
|
||||||
|
if err == io.EOF {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
return fmt.Errorf("image pull: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if pp.Progress != "" {
|
||||||
|
containerStateC <- domain.Container{
|
||||||
|
Status: domain.ContainerStatusPulling,
|
||||||
|
ImageName: imageName,
|
||||||
|
PullStatus: pp.Status,
|
||||||
|
PullProgress: pp.Progress,
|
||||||
|
PullPercent: int(pp.Detail.Curr * 100 / pp.Detail.Total),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ = pullReader.Close()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
0
internal/container/pull.json
Normal file
0
internal/container/pull.json
Normal file
50
internal/container/pull_test.go
Normal file
50
internal/container/pull_test.go
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
package container
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
_ "embed"
|
||||||
|
"io"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"git.netflux.io/rob/octoplex/internal/container/mocks"
|
||||||
|
"git.netflux.io/rob/octoplex/internal/domain"
|
||||||
|
"github.com/docker/docker/api/types/image"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/mock"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:embed testdata/pull_progress.json
|
||||||
|
var pullProgressJSON []byte
|
||||||
|
|
||||||
|
func TestHandleImagePull(t *testing.T) {
|
||||||
|
const imageName = "alpine"
|
||||||
|
containerStateC := make(chan domain.Container)
|
||||||
|
|
||||||
|
var dockerClient mocks.DockerClient
|
||||||
|
defer dockerClient.AssertExpectations(t)
|
||||||
|
|
||||||
|
dockerClient.
|
||||||
|
EXPECT().
|
||||||
|
ImagePull(mock.Anything, imageName, image.PullOptions{}).
|
||||||
|
Return(io.NopCloser(bytes.NewReader(pullProgressJSON)), nil)
|
||||||
|
|
||||||
|
var containerStates []domain.Container
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
require.NoError(t, handleImagePull(t.Context(), imageName, &dockerClient, containerStateC))
|
||||||
|
}()
|
||||||
|
|
||||||
|
const expectedContainerStates = 46
|
||||||
|
for range expectedContainerStates {
|
||||||
|
containerStates = append(containerStates, <-containerStateC)
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Len(t, containerStates, expectedContainerStates)
|
||||||
|
|
||||||
|
for _, containerState := range containerStates {
|
||||||
|
assert.Equal(t, domain.ContainerStatusPulling, containerState.Status)
|
||||||
|
assert.Equal(t, imageName, containerState.ImageName)
|
||||||
|
assert.NotZero(t, containerState.PullStatus)
|
||||||
|
}
|
||||||
|
}
|
51
internal/container/testdata/pull_progress.json
vendored
Normal file
51
internal/container/testdata/pull_progress.json
vendored
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
{"status":"Pulling from netfluxio/mediamtx-alpine","id":"latest"}
|
||||||
|
{"status":"Pulling fs layer","progressDetail":{},"id":"1f3e46996e29"}
|
||||||
|
{"status":"Pulling fs layer","progressDetail":{},"id":"6e814715152b"}
|
||||||
|
{"status":"Pulling fs layer","progressDetail":{},"id":"f6273376c644"}
|
||||||
|
{"status":"Pulling fs layer","progressDetail":{},"id":"3c07878de45f"}
|
||||||
|
{"status":"Waiting","progressDetail":{},"id":"3c07878de45f"}
|
||||||
|
{"status":"Downloading","progressDetail":{"current":31487,"total":3069027},"progress":"[\u003e ] 31.49kB/3.069MB","id":"6e814715152b"}
|
||||||
|
{"status":"Downloading","progressDetail":{"current":37415,"total":3641715},"progress":"[\u003e ] 37.41kB/3.642MB","id":"1f3e46996e29"}
|
||||||
|
{"status":"Downloading","progressDetail":{"current":163098,"total":16182831},"progress":"[\u003e ] 163.1kB/16.18MB","id":"f6273376c644"}
|
||||||
|
{"status":"Downloading","progressDetail":{"current":927991,"total":3069027},"progress":"[===============\u003e ] 928kB/3.069MB","id":"6e814715152b"}
|
||||||
|
{"status":"Downloading","progressDetail":{"current":330958,"total":3641715},"progress":"[====\u003e ] 331kB/3.642MB","id":"1f3e46996e29"}
|
||||||
|
{"status":"Downloading","progressDetail":{"current":1652998,"total":16182831},"progress":"[=====\u003e ] 1.653MB/16.18MB","id":"f6273376c644"}
|
||||||
|
{"status":"Downloading","progressDetail":{"current":915681,"total":3641715},"progress":"[============\u003e ] 915.7kB/3.642MB","id":"1f3e46996e29"}
|
||||||
|
{"status":"Downloading","progressDetail":{"current":1956087,"total":3069027},"progress":"[===============================\u003e ] 1.956MB/3.069MB","id":"6e814715152b"}
|
||||||
|
{"status":"Downloading","progressDetail":{"current":2324742,"total":16182831},"progress":"[=======\u003e ] 2.325MB/16.18MB","id":"f6273376c644"}
|
||||||
|
{"status":"Verifying Checksum","progressDetail":{},"id":"6e814715152b"}
|
||||||
|
{"status":"Download complete","progressDetail":{},"id":"6e814715152b"}
|
||||||
|
{"status":"Downloading","progressDetail":{"current":1603809,"total":3641715},"progress":"[======================\u003e ] 1.604MB/3.642MB","id":"1f3e46996e29"}
|
||||||
|
{"status":"Downloading","progressDetail":{"current":5654790,"total":16182831},"progress":"[=================\u003e ] 5.655MB/16.18MB","id":"f6273376c644"}
|
||||||
|
{"status":"Downloading","progressDetail":{"current":2291937,"total":3641715},"progress":"[===============================\u003e ] 2.292MB/3.642MB","id":"1f3e46996e29"}
|
||||||
|
{"status":"Downloading","progressDetail":{"current":8653062,"total":16182831},"progress":"[==========================\u003e ] 8.653MB/16.18MB","id":"f6273376c644"}
|
||||||
|
{"status":"Downloading","progressDetail":{"current":3025121,"total":3641715},"progress":"[=========================================\u003e ] 3.025MB/3.642MB","id":"1f3e46996e29"}
|
||||||
|
{"status":"Downloading","progressDetail":{"current":11663622,"total":16182831},"progress":"[====================================\u003e ] 11.66MB/16.18MB","id":"f6273376c644"}
|
||||||
|
{"status":"Verifying Checksum","progressDetail":{},"id":"1f3e46996e29"}
|
||||||
|
{"status":"Download complete","progressDetail":{},"id":"1f3e46996e29"}
|
||||||
|
{"status":"Extracting","progressDetail":{"current":65536,"total":3641715},"progress":"[\u003e ] 65.54kB/3.642MB","id":"1f3e46996e29"}
|
||||||
|
{"status":"Downloading","progressDetail":{"current":14141702,"total":16182831},"progress":"[===========================================\u003e ] 14.14MB/16.18MB","id":"f6273376c644"}
|
||||||
|
{"status":"Verifying Checksum","progressDetail":{},"id":"f6273376c644"}
|
||||||
|
{"status":"Download complete","progressDetail":{},"id":"f6273376c644"}
|
||||||
|
{"status":"Extracting","progressDetail":{"current":2424832,"total":3641715},"progress":"[=================================\u003e ] 2.425MB/3.642MB","id":"1f3e46996e29"}
|
||||||
|
{"status":"Extracting","progressDetail":{"current":3641715,"total":3641715},"progress":"[==================================================\u003e] 3.642MB/3.642MB","id":"1f3e46996e29"}
|
||||||
|
{"status":"Extracting","progressDetail":{"current":3641715,"total":3641715},"progress":"[==================================================\u003e] 3.642MB/3.642MB","id":"1f3e46996e29"}
|
||||||
|
{"status":"Pull complete","progressDetail":{},"id":"1f3e46996e29"}
|
||||||
|
{"status":"Extracting","progressDetail":{"current":32768,"total":3069027},"progress":"[\u003e ] 32.77kB/3.069MB","id":"6e814715152b"}
|
||||||
|
{"status":"Downloading","progressDetail":{"current":1369,"total":7593},"progress":"[=========\u003e ] 1.369kB/7.593kB","id":"3c07878de45f"}
|
||||||
|
{"status":"Downloading","progressDetail":{"current":7593,"total":7593},"progress":"[==================================================\u003e] 7.593kB/7.593kB","id":"3c07878de45f"}
|
||||||
|
{"status":"Download complete","progressDetail":{},"id":"3c07878de45f"}
|
||||||
|
{"status":"Extracting","progressDetail":{"current":2031616,"total":3069027},"progress":"[=================================\u003e ] 2.032MB/3.069MB","id":"6e814715152b"}
|
||||||
|
{"status":"Extracting","progressDetail":{"current":3069027,"total":3069027},"progress":"[==================================================\u003e] 3.069MB/3.069MB","id":"6e814715152b"}
|
||||||
|
{"status":"Pull complete","progressDetail":{},"id":"6e814715152b"}
|
||||||
|
{"status":"Extracting","progressDetail":{"current":163840,"total":16182831},"progress":"[\u003e ] 163.8kB/16.18MB","id":"f6273376c644"}
|
||||||
|
{"status":"Extracting","progressDetail":{"current":3112960,"total":16182831},"progress":"[=========\u003e ] 3.113MB/16.18MB","id":"f6273376c644"}
|
||||||
|
{"status":"Extracting","progressDetail":{"current":5734400,"total":16182831},"progress":"[=================\u003e ] 5.734MB/16.18MB","id":"f6273376c644"}
|
||||||
|
{"status":"Extracting","progressDetail":{"current":11632640,"total":16182831},"progress":"[===================================\u003e ] 11.63MB/16.18MB","id":"f6273376c644"}
|
||||||
|
{"status":"Extracting","progressDetail":{"current":16182831,"total":16182831},"progress":"[==================================================\u003e] 16.18MB/16.18MB","id":"f6273376c644"}
|
||||||
|
{"status":"Pull complete","progressDetail":{},"id":"f6273376c644"}
|
||||||
|
{"status":"Extracting","progressDetail":{"current":7593,"total":7593},"progress":"[==================================================\u003e] 7.593kB/7.593kB","id":"3c07878de45f"}
|
||||||
|
{"status":"Extracting","progressDetail":{"current":7593,"total":7593},"progress":"[==================================================\u003e] 7.593kB/7.593kB","id":"3c07878de45f"}
|
||||||
|
{"status":"Pull complete","progressDetail":{},"id":"3c07878de45f"}
|
||||||
|
{"status":"Digest: sha256:6edf95a345caa2c173bf24125164500283d10814cc441f2d8861054b4f755ac4"}
|
||||||
|
{"status":"Status: Downloaded newer image for netfluxio/mediamtx-alpine:latest"}
|
@ -80,6 +80,10 @@ type Container struct {
|
|||||||
RxRate int
|
RxRate int
|
||||||
TxRate int
|
TxRate int
|
||||||
RxSince time.Time
|
RxSince time.Time
|
||||||
|
ImageName string
|
||||||
|
PullStatus string // PullStatus is the status of the image pull.
|
||||||
|
PullProgress string // PullProgress is the "progress string" of the image pull.
|
||||||
|
PullPercent int // PullPercent is the percentage of the image that has been pulled.
|
||||||
RestartCount int
|
RestartCount int
|
||||||
ExitCode *int
|
ExitCode *int
|
||||||
Err error // Err is set if any error was received from the container client.
|
Err error // Err is set if any error was received from the container client.
|
||||||
|
@ -5,6 +5,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
|
"maps"
|
||||||
"slices"
|
"slices"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@ -45,12 +46,13 @@ type UI struct {
|
|||||||
|
|
||||||
// tview state
|
// tview state
|
||||||
|
|
||||||
app *tview.Application
|
app *tview.Application
|
||||||
screen tcell.Screen
|
screen tcell.Screen
|
||||||
screenCaptureC chan<- ScreenCapture
|
screenCaptureC chan<- ScreenCapture
|
||||||
pages *tview.Pages
|
pages *tview.Pages
|
||||||
sourceViews sourceViews
|
sourceViews sourceViews
|
||||||
destView *tview.Table
|
destView *tview.Table
|
||||||
|
pullProgressModal *tview.Modal
|
||||||
|
|
||||||
// other mutable state
|
// other mutable state
|
||||||
|
|
||||||
@ -175,6 +177,12 @@ func StartUI(ctx context.Context, params StartParams) (*UI, error) {
|
|||||||
destView.SetWrapSelection(true, false)
|
destView.SetWrapSelection(true, false)
|
||||||
destView.SetSelectedStyle(tcell.StyleDefault.Foreground(tcell.ColorWhite).Background(tcell.ColorDarkSlateGrey))
|
destView.SetSelectedStyle(tcell.StyleDefault.Foreground(tcell.ColorWhite).Background(tcell.ColorDarkSlateGrey))
|
||||||
|
|
||||||
|
pullProgressModal := tview.NewModal()
|
||||||
|
pullProgressModal.
|
||||||
|
SetBackgroundColor(tcell.ColorBlack).
|
||||||
|
SetTextColor(tcell.ColorWhite).
|
||||||
|
SetBorderStyle(tcell.StyleDefault.Background(tcell.ColorBlack).Foreground(tcell.ColorWhite))
|
||||||
|
|
||||||
flex := tview.NewFlex().
|
flex := tview.NewFlex().
|
||||||
SetDirection(tview.FlexColumn).
|
SetDirection(tview.FlexColumn).
|
||||||
AddItem(sidebar, 40, 0, false).
|
AddItem(sidebar, 40, 0, false).
|
||||||
@ -204,8 +212,9 @@ func StartUI(ctx context.Context, params StartParams) (*UI, error) {
|
|||||||
mem: memTextView,
|
mem: memTextView,
|
||||||
rx: rxTextView,
|
rx: rxTextView,
|
||||||
},
|
},
|
||||||
destView: destView,
|
destView: destView,
|
||||||
urlsToStartState: make(map[string]startState),
|
pullProgressModal: pullProgressModal,
|
||||||
|
urlsToStartState: make(map[string]startState),
|
||||||
}
|
}
|
||||||
|
|
||||||
app.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
|
app.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
|
||||||
@ -354,6 +363,8 @@ func (ui *UI) SetState(state domain.AppState) {
|
|||||||
ui.handleMediaServerClosed(state.Source.ExitReason)
|
ui.handleMediaServerClosed(state.Source.ExitReason)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ui.updatePullProgress(state)
|
||||||
|
|
||||||
ui.mu.Lock()
|
ui.mu.Lock()
|
||||||
for _, dest := range state.Destinations {
|
for _, dest := range state.Destinations {
|
||||||
ui.urlsToStartState[dest.URL] = containerStateToStartState(dest.Container.Status)
|
ui.urlsToStartState[dest.URL] = containerStateToStartState(dest.Container.Status)
|
||||||
@ -366,6 +377,63 @@ func (ui *UI) SetState(state domain.AppState) {
|
|||||||
ui.app.QueueUpdateDraw(func() { ui.redrawFromState(stateClone) })
|
ui.app.QueueUpdateDraw(func() { ui.redrawFromState(stateClone) })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ui *UI) updatePullProgress(state domain.AppState) {
|
||||||
|
pullingContainers := make(map[string]domain.Container)
|
||||||
|
|
||||||
|
isPulling := func(containerState string, pullPercent int) bool {
|
||||||
|
return containerState == domain.ContainerStatusPulling && pullPercent > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if isPulling(state.Source.Container.Status, state.Source.Container.PullPercent) {
|
||||||
|
pullingContainers[state.Source.Container.ImageName] = state.Source.Container
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, dest := range state.Destinations {
|
||||||
|
if isPulling(dest.Container.Status, dest.Container.PullPercent) {
|
||||||
|
pullingContainers[dest.Container.ImageName] = dest.Container
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(pullingContainers) == 0 {
|
||||||
|
ui.hideModal(modalGroupPullProgress)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// We don't really expect two images to be pulling simultaneously, but it's
|
||||||
|
// easy enough to handle.
|
||||||
|
imageNames := slices.Collect(maps.Keys(pullingContainers))
|
||||||
|
slices.Sort(imageNames)
|
||||||
|
container := pullingContainers[imageNames[0]]
|
||||||
|
|
||||||
|
ui.updateProgressModal(container)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ui *UI) updateProgressModal(container domain.Container) {
|
||||||
|
ui.app.QueueUpdateDraw(func() {
|
||||||
|
modalName := "modal-" + string(modalGroupPullProgress)
|
||||||
|
|
||||||
|
var status string
|
||||||
|
// Avoid showing the long Docker pull status in the modal content.
|
||||||
|
if len(container.PullStatus) < 30 {
|
||||||
|
status = container.PullStatus
|
||||||
|
}
|
||||||
|
|
||||||
|
modalContent := fmt.Sprintf(
|
||||||
|
"Pulling %s:\n%s (%d%%)\n\n%s",
|
||||||
|
container.ImageName,
|
||||||
|
status,
|
||||||
|
container.PullPercent,
|
||||||
|
container.PullProgress,
|
||||||
|
)
|
||||||
|
|
||||||
|
if ui.pages.HasPage(modalName) {
|
||||||
|
ui.pullProgressModal.SetText(modalContent)
|
||||||
|
} else {
|
||||||
|
ui.pages.AddPage(modalName, ui.pullProgressModal, true, true)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// modalGroup represents a specific modal of which only one may be shown
|
// modalGroup represents a specific modal of which only one may be shown
|
||||||
// simultaneously.
|
// simultaneously.
|
||||||
type modalGroup string
|
type modalGroup string
|
||||||
@ -375,6 +443,7 @@ const (
|
|||||||
modalGroupQuit modalGroup = "quit"
|
modalGroupQuit modalGroup = "quit"
|
||||||
modalGroupStartupCheck modalGroup = "startup-check"
|
modalGroupStartupCheck modalGroup = "startup-check"
|
||||||
modalGroupClipboard modalGroup = "clipboard"
|
modalGroupClipboard modalGroup = "clipboard"
|
||||||
|
modalGroupPullProgress modalGroup = "pull-progress"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (ui *UI) showModal(group modalGroup, text string, buttons []string, doneFunc func(int, string)) {
|
func (ui *UI) showModal(group modalGroup, text string, buttons []string, doneFunc func(int, string)) {
|
||||||
@ -404,6 +473,16 @@ func (ui *UI) showModal(group modalGroup, text string, buttons []string, doneFun
|
|||||||
ui.pages.AddPage(modalName, modal, true, true)
|
ui.pages.AddPage(modalName, modal, true, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ui *UI) hideModal(group modalGroup) {
|
||||||
|
modalName := "modal-" + string(group)
|
||||||
|
if !ui.pages.HasPage(modalName) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.pages.RemovePage(modalName)
|
||||||
|
ui.app.SetFocus(ui.destView)
|
||||||
|
}
|
||||||
|
|
||||||
func (ui *UI) handleMediaServerClosed(exitReason string) {
|
func (ui *UI) handleMediaServerClosed(exitReason string) {
|
||||||
done := make(chan struct{})
|
done := make(chan struct{})
|
||||||
|
|
||||||
@ -414,7 +493,6 @@ func (ui *UI) handleMediaServerClosed(exitReason string) {
|
|||||||
SetBackgroundColor(tcell.ColorBlack).
|
SetBackgroundColor(tcell.ColorBlack).
|
||||||
SetTextColor(tcell.ColorWhite).
|
SetTextColor(tcell.ColorWhite).
|
||||||
SetDoneFunc(func(int, string) {
|
SetDoneFunc(func(int, string) {
|
||||||
ui.logger.Info("closing app")
|
|
||||||
// TODO: improve app cleanup
|
// TODO: improve app cleanup
|
||||||
done <- struct{}{}
|
done <- struct{}{}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user