mirror of
				https://github.com/docker/compose.git
				synced 2025-11-03 21:25:21 +01:00 
			
		
		
		
	Add Navigation Menu to compose up
Signed-off-by: Joana Hrotko <joana.hrotko@docker.com>
This commit is contained in:
		
							parent
							
								
									3950460703
								
							
						
					
					
						commit
						e9dc82011f
					
				@ -101,6 +101,7 @@ RUN --mount=type=bind,target=. \
 | 
			
		||||
FROM build-base AS test
 | 
			
		||||
ARG CGO_ENABLED=0
 | 
			
		||||
ARG BUILD_TAGS
 | 
			
		||||
ENV COMPOSE_MENU=FALSE
 | 
			
		||||
RUN --mount=type=bind,target=. \
 | 
			
		||||
    --mount=type=cache,target=/root/.cache \
 | 
			
		||||
    --mount=type=cache,target=/go/pkg/mod \
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										1
									
								
								Makefile
									
									
									
									
									
								
							
							
						
						
									
										1
									
								
								Makefile
									
									
									
									
									
								
							@ -13,6 +13,7 @@
 | 
			
		||||
#   limitations under the License.
 | 
			
		||||
 | 
			
		||||
PKG := github.com/docker/compose/v2
 | 
			
		||||
export COMPOSE_MENU = FALSE
 | 
			
		||||
VERSION ?= $(shell git describe --match 'v[0-9]*' --dirty='.m' --always --tags)
 | 
			
		||||
 | 
			
		||||
GO_LDFLAGS ?= -w -X ${PKG}/internal.Version=${VERSION}
 | 
			
		||||
 | 
			
		||||
@ -65,6 +65,8 @@ const (
 | 
			
		||||
	ComposeIgnoreOrphans = "COMPOSE_IGNORE_ORPHANS"
 | 
			
		||||
	// ComposeEnvFiles defines the env files to use if --env-file isn't used
 | 
			
		||||
	ComposeEnvFiles = "COMPOSE_ENV_FILES"
 | 
			
		||||
	// ComposeMenu defines if the navigation menu should be rendered. Can be also set via --menu
 | 
			
		||||
	ComposeMenu = "COMPOSE_MENU"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type Backend interface {
 | 
			
		||||
@ -620,3 +622,15 @@ var printerModes = []string{
 | 
			
		||||
	ui.ModePlain,
 | 
			
		||||
	ui.ModeQuiet,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func SetUnchangedOption(name string, experimentalFlag bool) bool {
 | 
			
		||||
	var value bool
 | 
			
		||||
	// If the var is defined we use that value first
 | 
			
		||||
	if envVar, ok := os.LookupEnv(name); ok {
 | 
			
		||||
		value = utils.StringToBool(envVar)
 | 
			
		||||
	} else {
 | 
			
		||||
		// if not, we try to get it from experimental feature flag
 | 
			
		||||
		value = experimentalFlag
 | 
			
		||||
	}
 | 
			
		||||
	return value
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -56,6 +56,8 @@ type upOptions struct {
 | 
			
		||||
	wait                  bool
 | 
			
		||||
	waitTimeout           int
 | 
			
		||||
	watch                 bool
 | 
			
		||||
	navigationMenu        bool
 | 
			
		||||
	navigationMenuChanged bool
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (opts upOptions) apply(project *types.Project, services []string) (*types.Project, error) {
 | 
			
		||||
@ -87,6 +89,7 @@ func upCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service, ex
 | 
			
		||||
		PreRunE: AdaptCmd(func(ctx context.Context, cmd *cobra.Command, args []string) error {
 | 
			
		||||
			create.pullChanged = cmd.Flags().Changed("pull")
 | 
			
		||||
			create.timeChanged = cmd.Flags().Changed("timeout")
 | 
			
		||||
			up.navigationMenuChanged = cmd.Flags().Changed("menu")
 | 
			
		||||
			return validateFlags(&up, &create)
 | 
			
		||||
		}),
 | 
			
		||||
		RunE: p.WithServices(dockerCli, func(ctx context.Context, project *types.Project, services []string) error {
 | 
			
		||||
@ -128,6 +131,8 @@ func upCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service, ex
 | 
			
		||||
	flags.BoolVar(&up.wait, "wait", false, "Wait for services to be running|healthy. Implies detached mode.")
 | 
			
		||||
	flags.IntVar(&up.waitTimeout, "wait-timeout", 0, "Maximum duration to wait for the project to be running|healthy")
 | 
			
		||||
	flags.BoolVarP(&up.watch, "watch", "w", false, "Watch source code and rebuild/refresh containers when files are updated.")
 | 
			
		||||
	flags.BoolVar(&up.navigationMenu, "menu", false, "Enable interactive shortcuts when running attached (Experimental). Incompatible with --detach.")
 | 
			
		||||
	flags.MarkHidden("menu") //nolint:errcheck
 | 
			
		||||
 | 
			
		||||
	return upCmd
 | 
			
		||||
}
 | 
			
		||||
@ -161,7 +166,7 @@ func runUp(
 | 
			
		||||
	ctx context.Context,
 | 
			
		||||
	dockerCli command.Cli,
 | 
			
		||||
	backend api.Service,
 | 
			
		||||
	_ *experimental.State,
 | 
			
		||||
	experimentals *experimental.State,
 | 
			
		||||
	createOptions createOptions,
 | 
			
		||||
	upOptions upOptions,
 | 
			
		||||
	buildOptions buildOptions,
 | 
			
		||||
@ -181,6 +186,9 @@ func runUp(
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	if !upOptions.navigationMenuChanged {
 | 
			
		||||
		upOptions.navigationMenu = SetUnchangedOption(ComposeMenu, experimentals.NavBar())
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var build *api.BuildOptions
 | 
			
		||||
	if !createOptions.noBuild {
 | 
			
		||||
@ -262,6 +270,7 @@ func runUp(
 | 
			
		||||
			WaitTimeout:    timeout,
 | 
			
		||||
			Watch:          upOptions.watch,
 | 
			
		||||
			Services:       services,
 | 
			
		||||
			NavigationMenu: upOptions.navigationMenu,
 | 
			
		||||
		},
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										67
									
								
								cmd/formatter/ansi.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										67
									
								
								cmd/formatter/ansi.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,67 @@
 | 
			
		||||
/*
 | 
			
		||||
   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 (
 | 
			
		||||
	"fmt"
 | 
			
		||||
 | 
			
		||||
	"github.com/acarl005/stripansi"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func ansi(code string) string {
 | 
			
		||||
	return fmt.Sprintf("\033%s", code)
 | 
			
		||||
}
 | 
			
		||||
func SaveCursor() {
 | 
			
		||||
	fmt.Print(ansi("7"))
 | 
			
		||||
}
 | 
			
		||||
func RestoreCursor() {
 | 
			
		||||
	fmt.Print(ansi("8"))
 | 
			
		||||
}
 | 
			
		||||
func HideCursor() {
 | 
			
		||||
	fmt.Print(ansi("[?25l"))
 | 
			
		||||
}
 | 
			
		||||
func ShowCursor() {
 | 
			
		||||
	fmt.Print(ansi("[?25h"))
 | 
			
		||||
}
 | 
			
		||||
func MoveCursor(y, x int) {
 | 
			
		||||
	fmt.Print(ansi(fmt.Sprintf("[%d;%dH", y, x)))
 | 
			
		||||
}
 | 
			
		||||
func MoveCursorX(pos int) {
 | 
			
		||||
	fmt.Print(ansi(fmt.Sprintf("[%dG", pos)))
 | 
			
		||||
}
 | 
			
		||||
func ClearLine() {
 | 
			
		||||
	// Does not move cursor from its current position
 | 
			
		||||
	fmt.Print(ansi("[2K"))
 | 
			
		||||
}
 | 
			
		||||
func MoveCursorUp(lines int) {
 | 
			
		||||
	// Does not add new lines
 | 
			
		||||
	fmt.Print(ansi(fmt.Sprintf("[%dA", lines)))
 | 
			
		||||
}
 | 
			
		||||
func MoveCursorDown(lines int) {
 | 
			
		||||
	// Does not add new lines
 | 
			
		||||
	fmt.Print(ansi(fmt.Sprintf("[%dB", lines)))
 | 
			
		||||
}
 | 
			
		||||
func NewLine() {
 | 
			
		||||
	// Like \n
 | 
			
		||||
	fmt.Print("\012")
 | 
			
		||||
}
 | 
			
		||||
func lenAnsi(s string) int {
 | 
			
		||||
	// len has into consideration ansi codes, if we want
 | 
			
		||||
	// the len of the actual len(string) we need to strip
 | 
			
		||||
	// all ansi codes
 | 
			
		||||
	return len(stripansi.Strip(s))
 | 
			
		||||
}
 | 
			
		||||
@ -35,6 +35,18 @@ var names = []string{
 | 
			
		||||
	"white",
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
	BOLD      = "1"
 | 
			
		||||
	FAINT     = "2"
 | 
			
		||||
	ITALIC    = "3"
 | 
			
		||||
	UNDERLINE = "4"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
	RESET = "0"
 | 
			
		||||
	CYAN  = "36"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
	// Never use ANSI codes
 | 
			
		||||
	Never = "never"
 | 
			
		||||
@ -72,12 +84,17 @@ var monochrome = func(s string) string {
 | 
			
		||||
	return s
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func ansiColor(code, s string) string {
 | 
			
		||||
	return fmt.Sprintf("%s%s%s", ansi(code), s, ansi("0"))
 | 
			
		||||
func ansiColor(code, s string, formatOpts ...string) string {
 | 
			
		||||
	return fmt.Sprintf("%s%s%s", ansiColorCode(code, formatOpts...), s, ansiColorCode("0"))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func ansi(code string) string {
 | 
			
		||||
	return fmt.Sprintf("\033[%sm", code)
 | 
			
		||||
// Everything about ansiColorCode color https://hyperskill.org/learn/step/18193
 | 
			
		||||
func ansiColorCode(code string, formatOpts ...string) string {
 | 
			
		||||
	res := "\033["
 | 
			
		||||
	for _, c := range formatOpts {
 | 
			
		||||
		res = fmt.Sprintf("%s%s;", res, c)
 | 
			
		||||
	}
 | 
			
		||||
	return fmt.Sprintf("%s%sm", res, code)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func makeColorFunc(code string) colorFunc {
 | 
			
		||||
 | 
			
		||||
@ -102,13 +102,17 @@ func (l *logConsumer) Err(container, message string) {
 | 
			
		||||
	l.write(l.stderr, container, message)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var navColor = makeColorFunc("90")
 | 
			
		||||
 | 
			
		||||
func (l *logConsumer) write(w io.Writer, container, message string) {
 | 
			
		||||
	if l.ctx.Err() != nil {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	printFn := func() {
 | 
			
		||||
		p := l.getPresenter(container)
 | 
			
		||||
		timestamp := time.Now().Format(jsonmessage.RFC3339NanoFixed)
 | 
			
		||||
		for _, line := range strings.Split(message, "\n") {
 | 
			
		||||
			ClearLine()
 | 
			
		||||
			if l.timestamp {
 | 
			
		||||
				fmt.Fprintf(w, "%s%s%s\n", p.prefix, timestamp, line)
 | 
			
		||||
			} else {
 | 
			
		||||
@ -116,6 +120,12 @@ func (l *logConsumer) write(w io.Writer, container, message string) {
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	if KeyboardManager != nil {
 | 
			
		||||
		KeyboardManager.PrintKeyboardInfo(printFn)
 | 
			
		||||
	} else {
 | 
			
		||||
		printFn()
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (l *logConsumer) Status(container, msg string) {
 | 
			
		||||
	p := l.getPresenter(container)
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										321
									
								
								cmd/formatter/shortcut.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										321
									
								
								cmd/formatter/shortcut.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,321 @@
 | 
			
		||||
/*
 | 
			
		||||
   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 options 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 options + 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 {
 | 
			
		||||
				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) KeyboardClose() {
 | 
			
		||||
	_ = keyboard.Close()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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:
 | 
			
		||||
		lk.KeyboardClose()
 | 
			
		||||
 | 
			
		||||
		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)
 | 
			
		||||
}
 | 
			
		||||
@ -108,6 +108,17 @@ options:
 | 
			
		||||
      experimentalcli: false
 | 
			
		||||
      kubernetes: false
 | 
			
		||||
      swarm: false
 | 
			
		||||
    - option: menu
 | 
			
		||||
      value_type: bool
 | 
			
		||||
      default_value: "false"
 | 
			
		||||
      description: |
 | 
			
		||||
        Enable interactive shortcuts when running attached (Experimental). Incompatible with --detach.
 | 
			
		||||
      deprecated: false
 | 
			
		||||
      hidden: true
 | 
			
		||||
      experimental: false
 | 
			
		||||
      experimentalcli: false
 | 
			
		||||
      kubernetes: false
 | 
			
		||||
      swarm: false
 | 
			
		||||
    - option: no-attach
 | 
			
		||||
      value_type: stringArray
 | 
			
		||||
      default_value: '[]'
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										3
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										3
									
								
								go.mod
									
									
									
									
									
								
							@ -5,6 +5,7 @@ go 1.21
 | 
			
		||||
require (
 | 
			
		||||
	github.com/AlecAivazis/survey/v2 v2.3.7
 | 
			
		||||
	github.com/Microsoft/go-winio v0.6.1
 | 
			
		||||
	github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d
 | 
			
		||||
	github.com/buger/goterm v1.0.4
 | 
			
		||||
	github.com/compose-spec/compose-go/v2 v2.0.2
 | 
			
		||||
	github.com/containerd/console v1.0.4
 | 
			
		||||
@ -34,6 +35,7 @@ require (
 | 
			
		||||
	github.com/otiai10/copy v1.14.0
 | 
			
		||||
	github.com/r3labs/sse v0.0.0-20210224172625-26fe804710bc
 | 
			
		||||
	github.com/sirupsen/logrus v1.9.3
 | 
			
		||||
	github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966
 | 
			
		||||
	github.com/spf13/cobra v1.8.0
 | 
			
		||||
	github.com/spf13/pflag v1.0.5
 | 
			
		||||
	github.com/stretchr/testify v1.8.4
 | 
			
		||||
@ -85,6 +87,7 @@ require (
 | 
			
		||||
	github.com/docker/docker-credential-helpers v0.8.0 // indirect
 | 
			
		||||
	github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c // indirect
 | 
			
		||||
	github.com/docker/go-metrics v0.0.1 // indirect
 | 
			
		||||
	github.com/eiannone/keyboard v0.0.0-20220611211555-0d226195f203
 | 
			
		||||
	github.com/emicklei/go-restful/v3 v3.11.0 // indirect
 | 
			
		||||
	github.com/felixge/httpsnoop v1.0.4 // indirect
 | 
			
		||||
	github.com/fvbommel/sortorder v1.0.2 // indirect
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										6
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						
									
										6
									
								
								go.sum
									
									
									
									
									
								
							@ -27,6 +27,8 @@ github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDe
 | 
			
		||||
github.com/Shopify/logrus-bugsnag v0.0.0-20170309145241-6dbc35f2c30d/go.mod h1:HI8ITrYtUY+O+ZhtlqUnD8+KwNPOyugEhfP9fdUIaEQ=
 | 
			
		||||
github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d h1:UrqY+r/OJnIp5u0s1SbQ8dVfLCZJsnvazdBP5hS4iRs=
 | 
			
		||||
github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d/go.mod h1:HI8ITrYtUY+O+ZhtlqUnD8+KwNPOyugEhfP9fdUIaEQ=
 | 
			
		||||
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d h1:licZJFw2RwpHMqeKTCYkitsPqHNxTmd4SNR5r94FGM8=
 | 
			
		||||
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d/go.mod h1:asat636LX7Bqt5lYEZ27JNDcqxfjdBQuJ/MM4CN/Lzo=
 | 
			
		||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
 | 
			
		||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
 | 
			
		||||
github.com/anchore/go-struct-converter v0.0.0-20221118182256-c68fdcfa2092 h1:aM1rlcoLz8y5B2r4tTLMiVTrMtpfY0O8EScKJxaSaEc=
 | 
			
		||||
@ -150,6 +152,8 @@ github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDD
 | 
			
		||||
github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7 h1:UhxFibDNY/bfvqU5CAUmr9zpesgbU6SWc8/B4mflAE4=
 | 
			
		||||
github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE=
 | 
			
		||||
github.com/dvsekhvalnov/jose2go v0.0.0-20170216131308-f21a8cedbbae/go.mod h1:7BvyPhdbLxMXIYTFPLsyJRFMsKmOZnQmzh6Gb+uquuM=
 | 
			
		||||
github.com/eiannone/keyboard v0.0.0-20220611211555-0d226195f203 h1:XBBHcIb256gUJtLmY22n99HaZTz+r2Z51xUPi01m3wg=
 | 
			
		||||
github.com/eiannone/keyboard v0.0.0-20220611211555-0d226195f203/go.mod h1:E1jcSv8FaEny+OP/5k9UxZVw9YFWGj7eI4KR/iOBqCg=
 | 
			
		||||
github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g=
 | 
			
		||||
github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
 | 
			
		||||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
 | 
			
		||||
@ -440,6 +444,8 @@ github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6Mwd
 | 
			
		||||
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
 | 
			
		||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
 | 
			
		||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
 | 
			
		||||
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 h1:JIAuq3EEf9cgbU6AtGPK4CTG3Zf6CKMNqf0MHTggAUA=
 | 
			
		||||
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog=
 | 
			
		||||
github.com/spdx/tools-golang v0.5.3 h1:ialnHeEYUC4+hkm5vJm4qz2x+oEJbS0mAMFrNXdQraY=
 | 
			
		||||
github.com/spdx/tools-golang v0.5.3/go.mod h1:/ETOahiAo96Ob0/RAIBmFZw6XN0yTnyr/uFZm2NTMhI=
 | 
			
		||||
github.com/spf13/cast v0.0.0-20150508191742-4d07383ffe94 h1:JmfC365KywYwHB946TTiQWEb8kqPY+pybPLoGE9GgVk=
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										36
									
								
								internal/tracing/keyboard_metrics.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								internal/tracing/keyboard_metrics.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,36 @@
 | 
			
		||||
/*
 | 
			
		||||
   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 tracing
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
 | 
			
		||||
	"go.opentelemetry.io/otel/attribute"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func KeyboardMetrics(ctx context.Context, enabled, isDockerDesktopActive, isWatchConfigured bool) {
 | 
			
		||||
	commandAvailable := []string{}
 | 
			
		||||
	if isDockerDesktopActive {
 | 
			
		||||
		commandAvailable = append(commandAvailable, "gui")
 | 
			
		||||
	}
 | 
			
		||||
	if isWatchConfigured {
 | 
			
		||||
		commandAvailable = append(commandAvailable, "watch")
 | 
			
		||||
	}
 | 
			
		||||
	AddAttributeToSpan(ctx,
 | 
			
		||||
		attribute.Bool("navmenu.enabled", enabled),
 | 
			
		||||
		attribute.StringSlice("navmenu.command_available", commandAvailable))
 | 
			
		||||
}
 | 
			
		||||
@ -19,6 +19,8 @@ package tracing
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
 | 
			
		||||
	"github.com/acarl005/stripansi"
 | 
			
		||||
	"go.opentelemetry.io/otel/attribute"
 | 
			
		||||
	"go.opentelemetry.io/otel/codes"
 | 
			
		||||
	semconv "go.opentelemetry.io/otel/semconv/v1.19.0"
 | 
			
		||||
	"go.opentelemetry.io/otel/trace"
 | 
			
		||||
@ -80,12 +82,16 @@ func EventWrapFuncForErrGroup(ctx context.Context, eventName string, opts SpanOp
 | 
			
		||||
		eventOpts := opts.EventOptions()
 | 
			
		||||
 | 
			
		||||
		err := fn(ctx)
 | 
			
		||||
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			eventOpts = append(eventOpts, trace.WithAttributes(semconv.ExceptionMessage(err.Error())))
 | 
			
		||||
			eventOpts = append(eventOpts, trace.WithAttributes(semconv.ExceptionMessage(stripansi.Strip(err.Error()))))
 | 
			
		||||
		}
 | 
			
		||||
		span.AddEvent(eventName, eventOpts...)
 | 
			
		||||
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func AddAttributeToSpan(ctx context.Context, attr ...attribute.KeyValue) {
 | 
			
		||||
	span := trace.SpanFromContext(ctx)
 | 
			
		||||
	span.SetAttributes(attr...)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -219,6 +219,7 @@ type StartOptions struct {
 | 
			
		||||
	// Services passed in the command line to be started
 | 
			
		||||
	Services       []string
 | 
			
		||||
	Watch          bool
 | 
			
		||||
	NavigationMenu bool
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// RestartOptions group options of the Restart API
 | 
			
		||||
 | 
			
		||||
@ -81,7 +81,7 @@ func (s *composeService) Close() error {
 | 
			
		||||
	if s.dockerCli != nil {
 | 
			
		||||
		errs = append(errs, s.dockerCli.Client().Close())
 | 
			
		||||
	}
 | 
			
		||||
	if s.desktopCli != nil {
 | 
			
		||||
	if s.isDesktopIntegrationActive() {
 | 
			
		||||
		errs = append(errs, s.desktopCli.Close())
 | 
			
		||||
	}
 | 
			
		||||
	return errors.Join(errs...)
 | 
			
		||||
@ -320,3 +320,7 @@ func (s *composeService) RuntimeVersion(ctx context.Context) (string, error) {
 | 
			
		||||
	return runtimeVersion.val, runtimeVersion.err
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *composeService) isDesktopIntegrationActive() bool {
 | 
			
		||||
	return s.desktopCli != nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -152,7 +152,7 @@ func (s *composeService) ensureProjectVolumes(ctx context.Context, project *type
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err := func() error {
 | 
			
		||||
		if s.experiments.AutoFileShares() && s.desktopCli != nil {
 | 
			
		||||
		if s.experiments.AutoFileShares() && s.isDesktopIntegrationActive() {
 | 
			
		||||
			// collect all the bind mount paths and try to set up file shares in
 | 
			
		||||
			// Docker Desktop for them
 | 
			
		||||
			var paths []string
 | 
			
		||||
 | 
			
		||||
@ -145,7 +145,7 @@ func (s *composeService) ensureVolumesDown(ctx context.Context, project *types.P
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if s.experiments.AutoFileShares() && s.desktopCli != nil {
 | 
			
		||||
	if s.experiments.AutoFileShares() && s.isDesktopIntegrationActive() {
 | 
			
		||||
		ops = append(ops, func() error {
 | 
			
		||||
			desktop.RemoveFileSharesForProject(ctx, s.desktopCli, project.Name)
 | 
			
		||||
			return nil
 | 
			
		||||
 | 
			
		||||
@ -25,9 +25,11 @@ import (
 | 
			
		||||
 | 
			
		||||
	"github.com/compose-spec/compose-go/v2/types"
 | 
			
		||||
	"github.com/docker/cli/cli"
 | 
			
		||||
	"github.com/docker/compose/v2/cmd/formatter"
 | 
			
		||||
	"github.com/docker/compose/v2/internal/tracing"
 | 
			
		||||
	"github.com/docker/compose/v2/pkg/api"
 | 
			
		||||
	"github.com/docker/compose/v2/pkg/progress"
 | 
			
		||||
	"github.com/eiannone/keyboard"
 | 
			
		||||
	"github.com/hashicorp/go-multierror"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
@ -73,6 +75,7 @@ func (s *composeService) Up(ctx context.Context, project *types.Project, options
 | 
			
		||||
		first := true
 | 
			
		||||
		gracefulTeardown := func() {
 | 
			
		||||
			printer.Cancel()
 | 
			
		||||
			formatter.ClearLine()
 | 
			
		||||
			fmt.Fprintln(s.stdinfo(), "Gracefully stopping... (press Ctrl+C again to force)")
 | 
			
		||||
			eg.Go(func() error {
 | 
			
		||||
				err := s.Stop(context.Background(), project.Name, api.StopOptions{
 | 
			
		||||
@ -85,6 +88,23 @@ func (s *composeService) Up(ctx context.Context, project *types.Project, options
 | 
			
		||||
			})
 | 
			
		||||
			first = false
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		var kEvents <-chan keyboard.KeyEvent
 | 
			
		||||
		isWatchConfigured := s.shouldWatch(project)
 | 
			
		||||
		isDockerDesktopActive := s.isDesktopIntegrationActive()
 | 
			
		||||
 | 
			
		||||
		tracing.KeyboardMetrics(ctx, options.Start.NavigationMenu, isDockerDesktopActive, isWatchConfigured)
 | 
			
		||||
		if options.Start.NavigationMenu {
 | 
			
		||||
			kEvents, err = keyboard.GetKeys(100)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				panic(err)
 | 
			
		||||
			}
 | 
			
		||||
			formatter.NewKeyboardManager(ctx, isDockerDesktopActive, isWatchConfigured, signalChan, s.Watch)
 | 
			
		||||
			if options.Start.Watch {
 | 
			
		||||
				formatter.KeyboardManager.StartWatch(ctx, project, options)
 | 
			
		||||
			}
 | 
			
		||||
			defer formatter.KeyboardManager.KeyboardClose()
 | 
			
		||||
		}
 | 
			
		||||
		for {
 | 
			
		||||
			select {
 | 
			
		||||
			case <-doneCh:
 | 
			
		||||
@ -105,6 +125,8 @@ func (s *composeService) Up(ctx context.Context, project *types.Project, options
 | 
			
		||||
					})
 | 
			
		||||
					return nil
 | 
			
		||||
				}
 | 
			
		||||
			case event := <-kEvents:
 | 
			
		||||
				formatter.KeyboardManager.HandleKeyEvents(event, ctx, project, options)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	})
 | 
			
		||||
@ -124,7 +146,7 @@ func (s *composeService) Up(ctx context.Context, project *types.Project, options
 | 
			
		||||
		return err
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	if options.Start.Watch {
 | 
			
		||||
	if options.Start.Watch && !options.Start.NavigationMenu {
 | 
			
		||||
		eg.Go(func() error {
 | 
			
		||||
			buildOpts := *options.Create.Build
 | 
			
		||||
			buildOpts.Quiet = true
 | 
			
		||||
 | 
			
		||||
@ -65,6 +65,17 @@ func (s *composeService) getSyncImplementation(project *types.Project) (sync.Syn
 | 
			
		||||
 | 
			
		||||
	return sync.NewTar(project.Name, tarDockerClient{s: s}), nil
 | 
			
		||||
}
 | 
			
		||||
func (s *composeService) shouldWatch(project *types.Project) bool {
 | 
			
		||||
	var shouldWatch bool
 | 
			
		||||
	for i := range project.Services {
 | 
			
		||||
		service := project.Services[i]
 | 
			
		||||
 | 
			
		||||
		if service.Develop != nil && service.Develop.Watch != nil {
 | 
			
		||||
			shouldWatch = true
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return shouldWatch
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *composeService) Watch(ctx context.Context, project *types.Project, services []string, options api.WatchOptions) error { //nolint: gocyclo
 | 
			
		||||
	var err error
 | 
			
		||||
@ -159,17 +170,15 @@ func (s *composeService) Watch(ctx context.Context, project *types.Project, serv
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
		watching = true
 | 
			
		||||
 | 
			
		||||
		eg.Go(func() error {
 | 
			
		||||
			defer watcher.Close() //nolint:errcheck
 | 
			
		||||
			return s.watch(ctx, project, service.Name, options, watcher, syncer, config.Watch)
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if !watching {
 | 
			
		||||
		return fmt.Errorf("none of the selected services is configured for watch, consider setting an 'develop' section")
 | 
			
		||||
	}
 | 
			
		||||
	options.LogTo.Log(api.WatchLogger, "watch enabled")
 | 
			
		||||
	options.LogTo.Log(api.WatchLogger, "Watch enabled")
 | 
			
		||||
 | 
			
		||||
	return eg.Wait()
 | 
			
		||||
}
 | 
			
		||||
@ -189,10 +198,12 @@ func (s *composeService) watch(ctx context.Context, project *types.Project, name
 | 
			
		||||
 | 
			
		||||
	events := make(chan fileEvent)
 | 
			
		||||
	batchEvents := batchDebounceEvents(ctx, s.clock, quietPeriod, events)
 | 
			
		||||
	quit := make(chan bool)
 | 
			
		||||
	go func() {
 | 
			
		||||
		for {
 | 
			
		||||
			select {
 | 
			
		||||
			case <-ctx.Done():
 | 
			
		||||
				quit <- true
 | 
			
		||||
				return
 | 
			
		||||
			case batch := <-batchEvents:
 | 
			
		||||
				start := time.Now()
 | 
			
		||||
@ -208,9 +219,11 @@ func (s *composeService) watch(ctx context.Context, project *types.Project, name
 | 
			
		||||
 | 
			
		||||
	for {
 | 
			
		||||
		select {
 | 
			
		||||
		case <-ctx.Done():
 | 
			
		||||
		case <-quit:
 | 
			
		||||
			options.LogTo.Log(api.WatchLogger, "Watch disabled")
 | 
			
		||||
			return nil
 | 
			
		||||
		case err := <-watcher.Errors():
 | 
			
		||||
			options.LogTo.Err(api.WatchLogger, "Watch disabled with errors")
 | 
			
		||||
			return err
 | 
			
		||||
		case event := <-watcher.Events():
 | 
			
		||||
			hostPath := event.Path()
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user