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
|
FROM build-base AS test
|
||||||
ARG CGO_ENABLED=0
|
ARG CGO_ENABLED=0
|
||||||
ARG BUILD_TAGS
|
ARG BUILD_TAGS
|
||||||
|
ENV COMPOSE_MENU=FALSE
|
||||||
RUN --mount=type=bind,target=. \
|
RUN --mount=type=bind,target=. \
|
||||||
--mount=type=cache,target=/root/.cache \
|
--mount=type=cache,target=/root/.cache \
|
||||||
--mount=type=cache,target=/go/pkg/mod \
|
--mount=type=cache,target=/go/pkg/mod \
|
||||||
|
1
Makefile
1
Makefile
@ -13,6 +13,7 @@
|
|||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
PKG := github.com/docker/compose/v2
|
PKG := github.com/docker/compose/v2
|
||||||
|
export COMPOSE_MENU = FALSE
|
||||||
VERSION ?= $(shell git describe --match 'v[0-9]*' --dirty='.m' --always --tags)
|
VERSION ?= $(shell git describe --match 'v[0-9]*' --dirty='.m' --always --tags)
|
||||||
|
|
||||||
GO_LDFLAGS ?= -w -X ${PKG}/internal.Version=${VERSION}
|
GO_LDFLAGS ?= -w -X ${PKG}/internal.Version=${VERSION}
|
||||||
|
@ -65,6 +65,8 @@ const (
|
|||||||
ComposeIgnoreOrphans = "COMPOSE_IGNORE_ORPHANS"
|
ComposeIgnoreOrphans = "COMPOSE_IGNORE_ORPHANS"
|
||||||
// ComposeEnvFiles defines the env files to use if --env-file isn't used
|
// ComposeEnvFiles defines the env files to use if --env-file isn't used
|
||||||
ComposeEnvFiles = "COMPOSE_ENV_FILES"
|
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 {
|
type Backend interface {
|
||||||
@ -620,3 +622,15 @@ var printerModes = []string{
|
|||||||
ui.ModePlain,
|
ui.ModePlain,
|
||||||
ui.ModeQuiet,
|
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
|
wait bool
|
||||||
waitTimeout int
|
waitTimeout int
|
||||||
watch bool
|
watch bool
|
||||||
|
navigationMenu bool
|
||||||
|
navigationMenuChanged bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (opts upOptions) apply(project *types.Project, services []string) (*types.Project, error) {
|
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 {
|
PreRunE: AdaptCmd(func(ctx context.Context, cmd *cobra.Command, args []string) error {
|
||||||
create.pullChanged = cmd.Flags().Changed("pull")
|
create.pullChanged = cmd.Flags().Changed("pull")
|
||||||
create.timeChanged = cmd.Flags().Changed("timeout")
|
create.timeChanged = cmd.Flags().Changed("timeout")
|
||||||
|
up.navigationMenuChanged = cmd.Flags().Changed("menu")
|
||||||
return validateFlags(&up, &create)
|
return validateFlags(&up, &create)
|
||||||
}),
|
}),
|
||||||
RunE: p.WithServices(dockerCli, func(ctx context.Context, project *types.Project, services []string) error {
|
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.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.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.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
|
return upCmd
|
||||||
}
|
}
|
||||||
@ -161,7 +166,7 @@ func runUp(
|
|||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
dockerCli command.Cli,
|
dockerCli command.Cli,
|
||||||
backend api.Service,
|
backend api.Service,
|
||||||
_ *experimental.State,
|
experimentals *experimental.State,
|
||||||
createOptions createOptions,
|
createOptions createOptions,
|
||||||
upOptions upOptions,
|
upOptions upOptions,
|
||||||
buildOptions buildOptions,
|
buildOptions buildOptions,
|
||||||
@ -181,6 +186,9 @@ func runUp(
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if !upOptions.navigationMenuChanged {
|
||||||
|
upOptions.navigationMenu = SetUnchangedOption(ComposeMenu, experimentals.NavBar())
|
||||||
|
}
|
||||||
|
|
||||||
var build *api.BuildOptions
|
var build *api.BuildOptions
|
||||||
if !createOptions.noBuild {
|
if !createOptions.noBuild {
|
||||||
@ -262,6 +270,7 @@ func runUp(
|
|||||||
WaitTimeout: timeout,
|
WaitTimeout: timeout,
|
||||||
Watch: upOptions.watch,
|
Watch: upOptions.watch,
|
||||||
Services: services,
|
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",
|
"white",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
BOLD = "1"
|
||||||
|
FAINT = "2"
|
||||||
|
ITALIC = "3"
|
||||||
|
UNDERLINE = "4"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
RESET = "0"
|
||||||
|
CYAN = "36"
|
||||||
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// Never use ANSI codes
|
// Never use ANSI codes
|
||||||
Never = "never"
|
Never = "never"
|
||||||
@ -72,12 +84,17 @@ var monochrome = func(s string) string {
|
|||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
func ansiColor(code, s string) string {
|
func ansiColor(code, s string, formatOpts ...string) string {
|
||||||
return fmt.Sprintf("%s%s%s", ansi(code), s, ansi("0"))
|
return fmt.Sprintf("%s%s%s", ansiColorCode(code, formatOpts...), s, ansiColorCode("0"))
|
||||||
}
|
}
|
||||||
|
|
||||||
func ansi(code string) string {
|
// Everything about ansiColorCode color https://hyperskill.org/learn/step/18193
|
||||||
return fmt.Sprintf("\033[%sm", code)
|
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 {
|
func makeColorFunc(code string) colorFunc {
|
||||||
|
@ -102,13 +102,17 @@ func (l *logConsumer) Err(container, message string) {
|
|||||||
l.write(l.stderr, container, message)
|
l.write(l.stderr, container, message)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var navColor = makeColorFunc("90")
|
||||||
|
|
||||||
func (l *logConsumer) write(w io.Writer, container, message string) {
|
func (l *logConsumer) write(w io.Writer, container, message string) {
|
||||||
if l.ctx.Err() != nil {
|
if l.ctx.Err() != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
printFn := func() {
|
||||||
p := l.getPresenter(container)
|
p := l.getPresenter(container)
|
||||||
timestamp := time.Now().Format(jsonmessage.RFC3339NanoFixed)
|
timestamp := time.Now().Format(jsonmessage.RFC3339NanoFixed)
|
||||||
for _, line := range strings.Split(message, "\n") {
|
for _, line := range strings.Split(message, "\n") {
|
||||||
|
ClearLine()
|
||||||
if l.timestamp {
|
if l.timestamp {
|
||||||
fmt.Fprintf(w, "%s%s%s\n", p.prefix, timestamp, line)
|
fmt.Fprintf(w, "%s%s%s\n", p.prefix, timestamp, line)
|
||||||
} else {
|
} 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) {
|
func (l *logConsumer) Status(container, msg string) {
|
||||||
p := l.getPresenter(container)
|
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
|
experimentalcli: false
|
||||||
kubernetes: false
|
kubernetes: false
|
||||||
swarm: 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
|
- option: no-attach
|
||||||
value_type: stringArray
|
value_type: stringArray
|
||||||
default_value: '[]'
|
default_value: '[]'
|
||||||
|
3
go.mod
3
go.mod
@ -5,6 +5,7 @@ go 1.21
|
|||||||
require (
|
require (
|
||||||
github.com/AlecAivazis/survey/v2 v2.3.7
|
github.com/AlecAivazis/survey/v2 v2.3.7
|
||||||
github.com/Microsoft/go-winio v0.6.1
|
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/buger/goterm v1.0.4
|
||||||
github.com/compose-spec/compose-go/v2 v2.0.2
|
github.com/compose-spec/compose-go/v2 v2.0.2
|
||||||
github.com/containerd/console v1.0.4
|
github.com/containerd/console v1.0.4
|
||||||
@ -34,6 +35,7 @@ require (
|
|||||||
github.com/otiai10/copy v1.14.0
|
github.com/otiai10/copy v1.14.0
|
||||||
github.com/r3labs/sse v0.0.0-20210224172625-26fe804710bc
|
github.com/r3labs/sse v0.0.0-20210224172625-26fe804710bc
|
||||||
github.com/sirupsen/logrus v1.9.3
|
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/cobra v1.8.0
|
||||||
github.com/spf13/pflag v1.0.5
|
github.com/spf13/pflag v1.0.5
|
||||||
github.com/stretchr/testify v1.8.4
|
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/docker-credential-helpers v0.8.0 // indirect
|
||||||
github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c // indirect
|
github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c // indirect
|
||||||
github.com/docker/go-metrics v0.0.1 // 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/emicklei/go-restful/v3 v3.11.0 // indirect
|
||||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||||
github.com/fvbommel/sortorder v1.0.2 // 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-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 h1:UrqY+r/OJnIp5u0s1SbQ8dVfLCZJsnvazdBP5hS4iRs=
|
||||||
github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d/go.mod h1:HI8ITrYtUY+O+ZhtlqUnD8+KwNPOyugEhfP9fdUIaEQ=
|
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/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/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=
|
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 h1:UhxFibDNY/bfvqU5CAUmr9zpesgbU6SWc8/B4mflAE4=
|
||||||
github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE=
|
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/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 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g=
|
||||||
github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
|
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=
|
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.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
||||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
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 h1:ialnHeEYUC4+hkm5vJm4qz2x+oEJbS0mAMFrNXdQraY=
|
||||||
github.com/spdx/tools-golang v0.5.3/go.mod h1:/ETOahiAo96Ob0/RAIBmFZw6XN0yTnyr/uFZm2NTMhI=
|
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=
|
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 (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
|
"github.com/acarl005/stripansi"
|
||||||
|
"go.opentelemetry.io/otel/attribute"
|
||||||
"go.opentelemetry.io/otel/codes"
|
"go.opentelemetry.io/otel/codes"
|
||||||
semconv "go.opentelemetry.io/otel/semconv/v1.19.0"
|
semconv "go.opentelemetry.io/otel/semconv/v1.19.0"
|
||||||
"go.opentelemetry.io/otel/trace"
|
"go.opentelemetry.io/otel/trace"
|
||||||
@ -80,12 +82,16 @@ func EventWrapFuncForErrGroup(ctx context.Context, eventName string, opts SpanOp
|
|||||||
eventOpts := opts.EventOptions()
|
eventOpts := opts.EventOptions()
|
||||||
|
|
||||||
err := fn(ctx)
|
err := fn(ctx)
|
||||||
|
|
||||||
if err != nil {
|
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...)
|
span.AddEvent(eventName, eventOpts...)
|
||||||
|
|
||||||
return err
|
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 passed in the command line to be started
|
||||||
Services []string
|
Services []string
|
||||||
Watch bool
|
Watch bool
|
||||||
|
NavigationMenu bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// RestartOptions group options of the Restart API
|
// RestartOptions group options of the Restart API
|
||||||
|
@ -81,7 +81,7 @@ func (s *composeService) Close() error {
|
|||||||
if s.dockerCli != nil {
|
if s.dockerCli != nil {
|
||||||
errs = append(errs, s.dockerCli.Client().Close())
|
errs = append(errs, s.dockerCli.Client().Close())
|
||||||
}
|
}
|
||||||
if s.desktopCli != nil {
|
if s.isDesktopIntegrationActive() {
|
||||||
errs = append(errs, s.desktopCli.Close())
|
errs = append(errs, s.desktopCli.Close())
|
||||||
}
|
}
|
||||||
return errors.Join(errs...)
|
return errors.Join(errs...)
|
||||||
@ -320,3 +320,7 @@ func (s *composeService) RuntimeVersion(ctx context.Context) (string, error) {
|
|||||||
return runtimeVersion.val, runtimeVersion.err
|
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 {
|
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
|
// collect all the bind mount paths and try to set up file shares in
|
||||||
// Docker Desktop for them
|
// Docker Desktop for them
|
||||||
var paths []string
|
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 {
|
ops = append(ops, func() error {
|
||||||
desktop.RemoveFileSharesForProject(ctx, s.desktopCli, project.Name)
|
desktop.RemoveFileSharesForProject(ctx, s.desktopCli, project.Name)
|
||||||
return nil
|
return nil
|
||||||
|
@ -25,9 +25,11 @@ import (
|
|||||||
|
|
||||||
"github.com/compose-spec/compose-go/v2/types"
|
"github.com/compose-spec/compose-go/v2/types"
|
||||||
"github.com/docker/cli/cli"
|
"github.com/docker/cli/cli"
|
||||||
|
"github.com/docker/compose/v2/cmd/formatter"
|
||||||
"github.com/docker/compose/v2/internal/tracing"
|
"github.com/docker/compose/v2/internal/tracing"
|
||||||
"github.com/docker/compose/v2/pkg/api"
|
"github.com/docker/compose/v2/pkg/api"
|
||||||
"github.com/docker/compose/v2/pkg/progress"
|
"github.com/docker/compose/v2/pkg/progress"
|
||||||
|
"github.com/eiannone/keyboard"
|
||||||
"github.com/hashicorp/go-multierror"
|
"github.com/hashicorp/go-multierror"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -73,6 +75,7 @@ func (s *composeService) Up(ctx context.Context, project *types.Project, options
|
|||||||
first := true
|
first := true
|
||||||
gracefulTeardown := func() {
|
gracefulTeardown := func() {
|
||||||
printer.Cancel()
|
printer.Cancel()
|
||||||
|
formatter.ClearLine()
|
||||||
fmt.Fprintln(s.stdinfo(), "Gracefully stopping... (press Ctrl+C again to force)")
|
fmt.Fprintln(s.stdinfo(), "Gracefully stopping... (press Ctrl+C again to force)")
|
||||||
eg.Go(func() error {
|
eg.Go(func() error {
|
||||||
err := s.Stop(context.Background(), project.Name, api.StopOptions{
|
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
|
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 {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-doneCh:
|
case <-doneCh:
|
||||||
@ -105,6 +125,8 @@ func (s *composeService) Up(ctx context.Context, project *types.Project, options
|
|||||||
})
|
})
|
||||||
return nil
|
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
|
return err
|
||||||
})
|
})
|
||||||
|
|
||||||
if options.Start.Watch {
|
if options.Start.Watch && !options.Start.NavigationMenu {
|
||||||
eg.Go(func() error {
|
eg.Go(func() error {
|
||||||
buildOpts := *options.Create.Build
|
buildOpts := *options.Create.Build
|
||||||
buildOpts.Quiet = true
|
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
|
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
|
func (s *composeService) Watch(ctx context.Context, project *types.Project, services []string, options api.WatchOptions) error { //nolint: gocyclo
|
||||||
var err error
|
var err error
|
||||||
@ -159,17 +170,15 @@ func (s *composeService) Watch(ctx context.Context, project *types.Project, serv
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
watching = true
|
watching = true
|
||||||
|
|
||||||
eg.Go(func() error {
|
eg.Go(func() error {
|
||||||
defer watcher.Close() //nolint:errcheck
|
defer watcher.Close() //nolint:errcheck
|
||||||
return s.watch(ctx, project, service.Name, options, watcher, syncer, config.Watch)
|
return s.watch(ctx, project, service.Name, options, watcher, syncer, config.Watch)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if !watching {
|
if !watching {
|
||||||
return fmt.Errorf("none of the selected services is configured for watch, consider setting an 'develop' section")
|
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()
|
return eg.Wait()
|
||||||
}
|
}
|
||||||
@ -189,10 +198,12 @@ func (s *composeService) watch(ctx context.Context, project *types.Project, name
|
|||||||
|
|
||||||
events := make(chan fileEvent)
|
events := make(chan fileEvent)
|
||||||
batchEvents := batchDebounceEvents(ctx, s.clock, quietPeriod, events)
|
batchEvents := batchDebounceEvents(ctx, s.clock, quietPeriod, events)
|
||||||
|
quit := make(chan bool)
|
||||||
go func() {
|
go func() {
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
|
quit <- true
|
||||||
return
|
return
|
||||||
case batch := <-batchEvents:
|
case batch := <-batchEvents:
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
@ -208,9 +219,11 @@ func (s *composeService) watch(ctx context.Context, project *types.Project, name
|
|||||||
|
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-quit:
|
||||||
|
options.LogTo.Log(api.WatchLogger, "Watch disabled")
|
||||||
return nil
|
return nil
|
||||||
case err := <-watcher.Errors():
|
case err := <-watcher.Errors():
|
||||||
|
options.LogTo.Err(api.WatchLogger, "Watch disabled with errors")
|
||||||
return err
|
return err
|
||||||
case event := <-watcher.Events():
|
case event := <-watcher.Events():
|
||||||
hostPath := event.Path()
|
hostPath := event.Path()
|
||||||
|
Loading…
x
Reference in New Issue
Block a user