mirror of https://github.com/docker/compose.git
380 lines
9.3 KiB
Go
380 lines
9.3 KiB
Go
/*
|
|
Copyright 2024 Docker Compose CLI authors
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
|
|
package formatter
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"math"
|
|
"os"
|
|
"syscall"
|
|
"time"
|
|
|
|
"github.com/buger/goterm"
|
|
"github.com/compose-spec/compose-go/v2/types"
|
|
"github.com/docker/compose/v2/internal/tracing"
|
|
"github.com/docker/compose/v2/pkg/api"
|
|
"github.com/docker/compose/v2/pkg/watch"
|
|
"github.com/eiannone/keyboard"
|
|
"github.com/hashicorp/go-multierror"
|
|
"github.com/skratchdot/open-golang/open"
|
|
)
|
|
|
|
const DISPLAY_ERROR_TIME = 10
|
|
|
|
type KeyboardError struct {
|
|
err error
|
|
timeStart time.Time
|
|
}
|
|
|
|
func (ke *KeyboardError) shouldDisplay() bool {
|
|
return ke.err != nil && int(time.Since(ke.timeStart).Seconds()) < DISPLAY_ERROR_TIME
|
|
}
|
|
|
|
func (ke *KeyboardError) printError(height int, info string) {
|
|
if ke.shouldDisplay() {
|
|
errMessage := ke.err.Error()
|
|
|
|
MoveCursor(height-1-extraLines(info)-extraLines(errMessage), 0)
|
|
ClearLine()
|
|
|
|
fmt.Print(errMessage)
|
|
}
|
|
}
|
|
|
|
func (ke *KeyboardError) addError(prefix string, err error) {
|
|
ke.timeStart = time.Now()
|
|
|
|
prefix = ansiColor(CYAN, fmt.Sprintf("%s →", prefix), BOLD)
|
|
errorString := fmt.Sprintf("%s %s", prefix, err.Error())
|
|
|
|
ke.err = errors.New(errorString)
|
|
}
|
|
|
|
func (ke *KeyboardError) error() string {
|
|
return ke.err.Error()
|
|
}
|
|
|
|
type KeyboardWatch struct {
|
|
Watcher watch.Notify
|
|
Watching bool
|
|
WatchFn func(ctx context.Context, doneCh chan bool, project *types.Project, services []string, options api.WatchOptions) error
|
|
Ctx context.Context
|
|
Cancel context.CancelFunc
|
|
}
|
|
|
|
func (kw *KeyboardWatch) isWatching() bool {
|
|
return kw.Watching
|
|
}
|
|
|
|
func (kw *KeyboardWatch) switchWatching() {
|
|
kw.Watching = !kw.Watching
|
|
}
|
|
|
|
func (kw *KeyboardWatch) newContext(ctx context.Context) context.CancelFunc {
|
|
ctx, cancel := context.WithCancel(ctx)
|
|
kw.Ctx = ctx
|
|
kw.Cancel = cancel
|
|
return cancel
|
|
}
|
|
|
|
type KEYBOARD_LOG_LEVEL int
|
|
|
|
const (
|
|
NONE KEYBOARD_LOG_LEVEL = 0
|
|
INFO KEYBOARD_LOG_LEVEL = 1
|
|
DEBUG KEYBOARD_LOG_LEVEL = 2
|
|
)
|
|
|
|
type LogKeyboard struct {
|
|
kError KeyboardError
|
|
Watch KeyboardWatch
|
|
IsDockerDesktopActive bool
|
|
IsWatchConfigured bool
|
|
logLevel KEYBOARD_LOG_LEVEL
|
|
signalChannel chan<- os.Signal
|
|
}
|
|
|
|
var KeyboardManager *LogKeyboard
|
|
var eg multierror.Group
|
|
|
|
func NewKeyboardManager(ctx context.Context, isDockerDesktopActive, isWatchConfigured bool,
|
|
sc chan<- os.Signal,
|
|
watchFn func(ctx context.Context,
|
|
doneCh chan bool,
|
|
project *types.Project,
|
|
services []string,
|
|
options api.WatchOptions,
|
|
) error,
|
|
) {
|
|
km := LogKeyboard{}
|
|
km.IsDockerDesktopActive = isDockerDesktopActive
|
|
km.IsWatchConfigured = isWatchConfigured
|
|
km.logLevel = INFO
|
|
|
|
km.Watch.Watching = false
|
|
km.Watch.WatchFn = watchFn
|
|
|
|
km.signalChannel = sc
|
|
|
|
KeyboardManager = &km
|
|
}
|
|
|
|
func (lk *LogKeyboard) ClearKeyboardInfo() {
|
|
lk.clearNavigationMenu()
|
|
}
|
|
|
|
func (lk *LogKeyboard) PrintKeyboardInfo() {
|
|
if lk.logLevel == INFO {
|
|
lk.printNavigationMenu()
|
|
}
|
|
}
|
|
|
|
// Creates space to print error and menu string
|
|
func (lk *LogKeyboard) createBuffer(lines int) {
|
|
if lk.kError.shouldDisplay() {
|
|
extraLines := extraLines(lk.kError.error()) + 1
|
|
lines += extraLines
|
|
}
|
|
|
|
// get the string
|
|
infoMessage := lk.navigationMenu()
|
|
// calculate how many lines we need to display the menu info
|
|
// might be needed a line break
|
|
extraLines := extraLines(infoMessage) + 1
|
|
lines += extraLines
|
|
|
|
if lines > 0 {
|
|
allocateSpace(lines)
|
|
MoveCursorUp(lines)
|
|
}
|
|
}
|
|
|
|
func (lk *LogKeyboard) printNavigationMenu() {
|
|
offset := 1
|
|
lk.clearNavigationMenu()
|
|
lk.createBuffer(offset)
|
|
|
|
if lk.logLevel == INFO {
|
|
height := goterm.Height()
|
|
menu := lk.navigationMenu()
|
|
|
|
MoveCursorX(0)
|
|
SaveCursor()
|
|
|
|
lk.kError.printError(height, menu)
|
|
|
|
MoveCursor(height-extraLines(menu), 0)
|
|
ClearLine()
|
|
fmt.Print(menu)
|
|
|
|
MoveCursorX(0)
|
|
RestoreCursor()
|
|
}
|
|
}
|
|
|
|
func (lk *LogKeyboard) navigationMenu() string {
|
|
var openDDInfo string
|
|
if lk.IsDockerDesktopActive {
|
|
openDDInfo = shortcutKeyColor("v") + navColor(" View in Docker Desktop")
|
|
}
|
|
|
|
var openDDUI string
|
|
if openDDInfo != "" {
|
|
openDDUI = navColor(" ")
|
|
}
|
|
if lk.IsDockerDesktopActive {
|
|
openDDUI = openDDUI + shortcutKeyColor("o") + navColor(" View Config")
|
|
}
|
|
|
|
var watchInfo string
|
|
if openDDInfo != "" || openDDUI != "" {
|
|
watchInfo = navColor(" ")
|
|
}
|
|
var isEnabled = " Enable"
|
|
if lk.Watch.Watching {
|
|
isEnabled = " Disable"
|
|
}
|
|
watchInfo = watchInfo + shortcutKeyColor("w") + navColor(isEnabled+" Watch")
|
|
return openDDInfo + openDDUI + watchInfo
|
|
}
|
|
|
|
func (lk *LogKeyboard) clearNavigationMenu() {
|
|
height := goterm.Height()
|
|
MoveCursorX(0)
|
|
SaveCursor()
|
|
|
|
// ClearLine()
|
|
for i := 0; i < height; i++ {
|
|
MoveCursorDown(1)
|
|
ClearLine()
|
|
}
|
|
RestoreCursor()
|
|
}
|
|
|
|
func (lk *LogKeyboard) openDockerDesktop(ctx context.Context, project *types.Project) {
|
|
if !lk.IsDockerDesktopActive {
|
|
return
|
|
}
|
|
eg.Go(tracing.EventWrapFuncForErrGroup(ctx, "menu/gui", tracing.SpanOptions{},
|
|
func(ctx context.Context) error {
|
|
link := fmt.Sprintf("docker-desktop://dashboard/apps/%s", project.Name)
|
|
err := open.Run(link)
|
|
if err != nil {
|
|
err = fmt.Errorf("Could not open Docker Desktop")
|
|
lk.keyboardError("View", err)
|
|
}
|
|
return err
|
|
}),
|
|
)
|
|
}
|
|
|
|
func (lk *LogKeyboard) openDDComposeUI(ctx context.Context, project *types.Project) {
|
|
if !lk.IsDockerDesktopActive {
|
|
return
|
|
}
|
|
eg.Go(tracing.EventWrapFuncForErrGroup(ctx, "menu/gui/composeview", tracing.SpanOptions{},
|
|
func(ctx context.Context) error {
|
|
link := fmt.Sprintf("docker-desktop://dashboard/docker-compose/%s", project.Name)
|
|
err := open.Run(link)
|
|
if err != nil {
|
|
err = fmt.Errorf("Could not open Docker Desktop Compose UI")
|
|
lk.keyboardError("View Config", err)
|
|
}
|
|
return err
|
|
}),
|
|
)
|
|
}
|
|
func (lk *LogKeyboard) openDDWatchDocs(ctx context.Context, project *types.Project) {
|
|
eg.Go(tracing.EventWrapFuncForErrGroup(ctx, "menu/gui/watch", tracing.SpanOptions{},
|
|
func(ctx context.Context) error {
|
|
link := fmt.Sprintf("docker-desktop://dashboard/docker-compose/%s/watch", project.Name)
|
|
err := open.Run(link)
|
|
if err != nil {
|
|
err = fmt.Errorf("Could not open Docker Desktop Compose UI")
|
|
lk.keyboardError("Watch Docs", err)
|
|
}
|
|
return err
|
|
}),
|
|
)
|
|
}
|
|
|
|
func (lk *LogKeyboard) keyboardError(prefix string, err error) {
|
|
lk.kError.addError(prefix, err)
|
|
|
|
lk.printNavigationMenu()
|
|
timer1 := time.NewTimer((DISPLAY_ERROR_TIME + 1) * time.Second)
|
|
go func() {
|
|
<-timer1.C
|
|
lk.printNavigationMenu()
|
|
}()
|
|
}
|
|
|
|
func (lk *LogKeyboard) StartWatch(ctx context.Context, doneCh chan bool, project *types.Project, options api.UpOptions) {
|
|
if !lk.IsWatchConfigured {
|
|
return
|
|
}
|
|
lk.Watch.switchWatching()
|
|
if !lk.Watch.isWatching() {
|
|
lk.Watch.Cancel()
|
|
} else {
|
|
eg.Go(tracing.EventWrapFuncForErrGroup(ctx, "menu/watch", tracing.SpanOptions{},
|
|
func(ctx context.Context) error {
|
|
if options.Create.Build == nil {
|
|
err := fmt.Errorf("Cannot run watch mode with flag --no-build")
|
|
lk.keyboardError("Watch", err)
|
|
return err
|
|
}
|
|
|
|
lk.Watch.newContext(ctx)
|
|
buildOpts := *options.Create.Build
|
|
buildOpts.Quiet = true
|
|
return lk.Watch.WatchFn(lk.Watch.Ctx, doneCh, project, options.Start.Services, api.WatchOptions{
|
|
Build: &buildOpts,
|
|
LogTo: options.Start.Attach,
|
|
})
|
|
}))
|
|
}
|
|
}
|
|
|
|
func (lk *LogKeyboard) HandleKeyEvents(event keyboard.KeyEvent, ctx context.Context, doneCh chan bool, project *types.Project, options api.UpOptions) {
|
|
switch kRune := event.Rune; kRune {
|
|
case 'v':
|
|
lk.openDockerDesktop(ctx, project)
|
|
case 'w':
|
|
if !lk.IsWatchConfigured {
|
|
// we try to open watch docs if DD is installed
|
|
if lk.IsDockerDesktopActive {
|
|
lk.openDDWatchDocs(ctx, project)
|
|
}
|
|
// either way we mark menu/watch as an error
|
|
eg.Go(tracing.EventWrapFuncForErrGroup(ctx, "menu/watch", tracing.SpanOptions{},
|
|
func(ctx context.Context) error {
|
|
err := fmt.Errorf("watch is not yet configured. Learn more: %s", ansiColor(CYAN, "https://docs.docker.com/compose/file-watch/"))
|
|
lk.keyboardError("Watch", err)
|
|
return err
|
|
}))
|
|
return
|
|
}
|
|
lk.StartWatch(ctx, doneCh, project, options)
|
|
case 'o':
|
|
lk.openDDComposeUI(ctx, project)
|
|
}
|
|
switch key := event.Key; key {
|
|
case keyboard.KeyCtrlC:
|
|
_ = keyboard.Close()
|
|
lk.clearNavigationMenu()
|
|
ShowCursor()
|
|
|
|
lk.logLevel = NONE
|
|
if lk.Watch.Watching && lk.Watch.Cancel != nil {
|
|
lk.Watch.Cancel()
|
|
_ = eg.Wait().ErrorOrNil() // Need to print this ?
|
|
}
|
|
// will notify main thread to kill and will handle gracefully
|
|
lk.signalChannel <- syscall.SIGINT
|
|
case keyboard.KeyEnter:
|
|
NewLine()
|
|
lk.printNavigationMenu()
|
|
}
|
|
}
|
|
|
|
func allocateSpace(lines int) {
|
|
for i := 0; i < lines; i++ {
|
|
ClearLine()
|
|
NewLine()
|
|
MoveCursorX(0)
|
|
}
|
|
}
|
|
|
|
func extraLines(s string) int {
|
|
return int(math.Floor(float64(lenAnsi(s)) / float64(goterm.Width())))
|
|
}
|
|
|
|
func shortcutKeyColor(key string) string {
|
|
foreground := "38;2"
|
|
black := "0;0;0"
|
|
background := "48;2"
|
|
white := "255;255;255"
|
|
return ansiColor(foreground+";"+black+";"+background+";"+white, key, BOLD)
|
|
}
|
|
|
|
func navColor(key string) string {
|
|
return ansiColor(FAINT, key)
|
|
}
|