| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325 | 
							- /*
 
-    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-linesOffset(info)-linesOffset(errMessage)-1, 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, 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,
 
- 		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
 
- 	HideCursor()
 
- }
 
- func (lk *LogKeyboard) PrintKeyboardInfo(printFn func()) {
 
- 	printFn()
 
- 	if lk.logLevel == INFO {
 
- 		lk.printNavigationMenu()
 
- 	}
 
- }
 
- // Creates space to print error and menu string
 
- func (lk *LogKeyboard) createBuffer(lines int) {
 
- 	allocateSpace(lines)
 
- 	if lk.kError.shouldDisplay() {
 
- 		extraLines := linesOffset(lk.kError.error()) + 1
 
- 		allocateSpace(extraLines)
 
- 		lines += extraLines
 
- 	}
 
- 	infoMessage := lk.navigationMenu()
 
- 	extraLines := linesOffset(infoMessage) + 1
 
- 	allocateSpace(extraLines)
 
- 	lines += extraLines
 
- 	if lines > 0 {
 
- 		MoveCursorUp(lines)
 
- 	}
 
- }
 
- func (lk *LogKeyboard) printNavigationMenu() {
 
- 	lk.clearNavigationMenu()
 
- 	lk.createBuffer(0)
 
- 	if lk.logLevel == INFO {
 
- 		height := goterm.Height()
 
- 		menu := lk.navigationMenu()
 
- 		MoveCursorX(0)
 
- 		SaveCursor()
 
- 		lk.kError.printError(height, menu)
 
- 		MoveCursor(height-linesOffset(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 watchInfo string
 
- 	if openDDInfo != "" {
 
- 		watchInfo = navColor("   ")
 
- 	}
 
- 	var isEnabled = " Enable"
 
- 	if lk.Watch.Watching {
 
- 		isEnabled = " Disable"
 
- 	}
 
- 	watchInfo = watchInfo + shortcutKeyColor("w") + navColor(isEnabled+" Watch")
 
- 	return openDDInfo + watchInfo
 
- }
 
- func (lk *LogKeyboard) clearNavigationMenu() {
 
- 	height := goterm.Height()
 
- 	MoveCursorX(0)
 
- 	SaveCursor()
 
- 	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) 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, project *types.Project, options api.UpOptions) {
 
- 	if !lk.IsWatchConfigured {
 
- 		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.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, project, options.Start.Services, api.WatchOptions{
 
- 					Build: &buildOpts,
 
- 					LogTo: options.Start.Attach,
 
- 				})
 
- 			}))
 
- 	}
 
- }
 
- func (lk *LogKeyboard) HandleKeyEvents(event keyboard.KeyEvent, ctx context.Context, project *types.Project, options api.UpOptions) {
 
- 	switch kRune := event.Rune; kRune {
 
- 	case 'v':
 
- 		lk.openDockerDesktop(ctx, project)
 
- 	case 'w':
 
- 		lk.StartWatch(ctx, project, options)
 
- 	}
 
- 	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:
 
- 		lk.printNavigationMenu()
 
- 	}
 
- }
 
- func allocateSpace(lines int) {
 
- 	for i := 0; i < lines; i++ {
 
- 		ClearLine()
 
- 		NewLine()
 
- 		MoveCursorX(0)
 
- 	}
 
- }
 
- func linesOffset(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)
 
- }
 
 
  |