mirror of https://github.com/docker/compose.git
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
|
||||
}
|
||||
|
|
|
@ -42,20 +42,22 @@ type composeOptions struct {
|
|||
|
||||
type upOptions struct {
|
||||
*composeOptions
|
||||
Detach bool
|
||||
noStart bool
|
||||
noDeps bool
|
||||
cascadeStop bool
|
||||
exitCodeFrom string
|
||||
noColor bool
|
||||
noPrefix bool
|
||||
attachDependencies bool
|
||||
attach []string
|
||||
noAttach []string
|
||||
timestamp bool
|
||||
wait bool
|
||||
waitTimeout int
|
||||
watch bool
|
||||
Detach bool
|
||||
noStart bool
|
||||
noDeps bool
|
||||
cascadeStop bool
|
||||
exitCodeFrom string
|
||||
noColor bool
|
||||
noPrefix bool
|
||||
attachDependencies bool
|
||||
attach []string
|
||||
noAttach []string
|
||||
timestamp bool
|
||||
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 {
|
||||
|
@ -253,15 +261,16 @@ func runUp(
|
|||
return backend.Up(ctx, project, api.UpOptions{
|
||||
Create: create,
|
||||
Start: api.StartOptions{
|
||||
Project: project,
|
||||
Attach: consumer,
|
||||
AttachTo: attach,
|
||||
ExitCodeFrom: upOptions.exitCodeFrom,
|
||||
CascadeStop: upOptions.cascadeStop,
|
||||
Wait: upOptions.wait,
|
||||
WaitTimeout: timeout,
|
||||
Watch: upOptions.watch,
|
||||
Services: services,
|
||||
Project: project,
|
||||
Attach: consumer,
|
||||
AttachTo: attach,
|
||||
ExitCodeFrom: upOptions.exitCodeFrom,
|
||||
CascadeStop: upOptions.cascadeStop,
|
||||
Wait: upOptions.wait,
|
||||
WaitTimeout: timeout,
|
||||
Watch: upOptions.watch,
|
||||
Services: services,
|
||||
NavigationMenu: upOptions.navigationMenu,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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,19 +102,29 @@ 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
|
||||
}
|
||||
p := l.getPresenter(container)
|
||||
timestamp := time.Now().Format(jsonmessage.RFC3339NanoFixed)
|
||||
for _, line := range strings.Split(message, "\n") {
|
||||
if l.timestamp {
|
||||
fmt.Fprintf(w, "%s%s%s\n", p.prefix, timestamp, line)
|
||||
} else {
|
||||
fmt.Fprintf(w, "%s%s\n", p.prefix, line)
|
||||
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 {
|
||||
fmt.Fprintf(w, "%s%s\n", p.prefix, line)
|
||||
}
|
||||
}
|
||||
}
|
||||
if KeyboardManager != nil {
|
||||
KeyboardManager.PrintKeyboardInfo(printFn)
|
||||
} else {
|
||||
printFn()
|
||||
}
|
||||
}
|
||||
|
||||
func (l *logConsumer) Status(container, msg string) {
|
||||
|
|
|
@ -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=
|
||||
|
|
|
@ -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...)
|
||||
}
|
||||
|
|
|
@ -217,8 +217,9 @@ type StartOptions struct {
|
|||
Wait bool
|
||||
WaitTimeout time.Duration
|
||||
// Services passed in the command line to be started
|
||||
Services []string
|
||||
Watch bool
|
||||
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…
Reference in New Issue