| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358 |
- /*
- 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"
- "reflect"
- "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/eiannone/keyboard"
- "github.com/skratchdot/open-golang/open"
- )
- const DISPLAY_ERROR_TIME = 10
- type KeyboardError struct {
- err error
- timeStart time.Time
- }
- func (ke *KeyboardError) shouldDisplay() bool {
- return ke.err != nil && int(time.Since(ke.timeStart).Seconds()) < DISPLAY_ERROR_TIME
- }
- func (ke *KeyboardError) printError(height int, info string) {
- if ke.shouldDisplay() {
- errMessage := ke.err.Error()
- MoveCursor(height-1-extraLines(info)-extraLines(errMessage), 0)
- ClearLine()
- fmt.Print(errMessage)
- }
- }
- func (ke *KeyboardError) addError(prefix string, err error) {
- ke.timeStart = time.Now()
- prefix = ansiColor(CYAN, fmt.Sprintf("%s →", prefix), BOLD)
- errorString := fmt.Sprintf("%s %s", prefix, err.Error())
- ke.err = errors.New(errorString)
- }
- func (ke *KeyboardError) error() string {
- return ke.err.Error()
- }
- type KeyboardWatch struct {
- Watching bool
- Watcher Toggle
- IsConfigured bool
- }
- type Toggle interface {
- Start(context.Context) error
- Stop() error
- }
- 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
- logLevel KEYBOARD_LOG_LEVEL
- signalChannel chan<- os.Signal
- }
- // FIXME(ndeloof) we should avoid use of such a global reference. see use in logConsumer
- var KeyboardManager *LogKeyboard
- func NewKeyboardManager(isDockerDesktopActive bool, sc chan<- os.Signal, w bool, watcher Toggle) *LogKeyboard {
- KeyboardManager = &LogKeyboard{
- Watch: KeyboardWatch{
- Watching: w,
- Watcher: watcher,
- IsConfigured: !reflect.ValueOf(watcher).IsNil(),
- },
- IsDockerDesktopActive: isDockerDesktopActive,
- logLevel: INFO,
- signalChannel: sc,
- }
- return KeyboardManager
- }
- func (lk *LogKeyboard) ClearKeyboardInfo() {
- lk.clearNavigationMenu()
- }
- func (lk *LogKeyboard) PrintKeyboardInfo() {
- if lk.logLevel == INFO {
- lk.printNavigationMenu()
- }
- }
- // Creates space to print error and menu string
- func (lk *LogKeyboard) createBuffer(lines int) {
- if lk.kError.shouldDisplay() {
- extraLines := extraLines(lk.kError.error()) + 1
- lines += extraLines
- }
- // get the string
- infoMessage := lk.navigationMenu()
- // calculate how many lines we need to display the menu info
- // might be needed a line break
- extraLines := extraLines(infoMessage) + 1
- lines += extraLines
- if lines > 0 {
- allocateSpace(lines)
- MoveCursorUp(lines)
- }
- }
- func (lk *LogKeyboard) printNavigationMenu() {
- offset := 1
- lk.clearNavigationMenu()
- lk.createBuffer(offset)
- if lk.logLevel == INFO {
- height := goterm.Height()
- menu := lk.navigationMenu()
- MoveCursorX(0)
- SaveCursor()
- lk.kError.printError(height, menu)
- MoveCursor(height-extraLines(menu), 0)
- ClearLine()
- fmt.Print(menu)
- MoveCursorX(0)
- RestoreCursor()
- }
- }
- func (lk *LogKeyboard) navigationMenu() string {
- var openDDInfo string
- if lk.IsDockerDesktopActive {
- openDDInfo = shortcutKeyColor("v") + navColor(" View in Docker Desktop")
- }
- var openDDUI string
- if openDDInfo != "" {
- openDDUI = navColor(" ")
- }
- if lk.IsDockerDesktopActive {
- openDDUI = openDDUI + shortcutKeyColor("o") + navColor(" View Config")
- }
- var watchInfo string
- if openDDInfo != "" || openDDUI != "" {
- watchInfo = navColor(" ")
- }
- isEnabled := " Enable"
- if lk.Watch.Watching {
- isEnabled = " Disable"
- }
- watchInfo = watchInfo + shortcutKeyColor("w") + navColor(isEnabled+" Watch")
- return openDDInfo + openDDUI + watchInfo
- }
- func (lk *LogKeyboard) clearNavigationMenu() {
- height := goterm.Height()
- MoveCursorX(0)
- SaveCursor()
- // ClearLine()
- for i := 0; i < height; i++ {
- MoveCursorDown(1)
- ClearLine()
- }
- RestoreCursor()
- }
- func (lk *LogKeyboard) openDockerDesktop(ctx context.Context, project *types.Project) {
- if !lk.IsDockerDesktopActive {
- return
- }
- go func() {
- _ = tracing.EventWrapFuncForErrGroup(ctx, "menu/gui", tracing.SpanOptions{},
- func(ctx context.Context) error {
- link := fmt.Sprintf("docker-desktop://dashboard/apps/%s", project.Name)
- err := open.Run(link)
- if err != nil {
- err = fmt.Errorf("could not open Docker Desktop")
- lk.keyboardError("View", err)
- }
- return err
- })()
- }()
- }
- func (lk *LogKeyboard) openDDComposeUI(ctx context.Context, project *types.Project) {
- if !lk.IsDockerDesktopActive {
- return
- }
- go func() {
- _ = tracing.EventWrapFuncForErrGroup(ctx, "menu/gui/composeview", tracing.SpanOptions{},
- func(ctx context.Context) error {
- link := fmt.Sprintf("docker-desktop://dashboard/docker-compose/%s", project.Name)
- err := open.Run(link)
- if err != nil {
- err = fmt.Errorf("could not open Docker Desktop Compose UI")
- lk.keyboardError("View Config", err)
- }
- return err
- })()
- }()
- }
- func (lk *LogKeyboard) openDDWatchDocs(ctx context.Context, project *types.Project) {
- go func() {
- _ = tracing.EventWrapFuncForErrGroup(ctx, "menu/gui/watch", tracing.SpanOptions{},
- func(ctx context.Context) error {
- link := fmt.Sprintf("docker-desktop://dashboard/docker-compose/%s/watch", project.Name)
- err := open.Run(link)
- if err != nil {
- err = fmt.Errorf("could not open Docker Desktop Compose UI")
- lk.keyboardError("Watch Docs", err)
- }
- return err
- })()
- }()
- }
- func (lk *LogKeyboard) keyboardError(prefix string, err error) {
- lk.kError.addError(prefix, err)
- lk.printNavigationMenu()
- timer1 := time.NewTimer((DISPLAY_ERROR_TIME + 1) * time.Second)
- go func() {
- <-timer1.C
- lk.printNavigationMenu()
- }()
- }
- func (lk *LogKeyboard) ToggleWatch(ctx context.Context, options api.UpOptions) {
- if !lk.Watch.IsConfigured {
- return
- }
- if lk.Watch.Watching {
- err := lk.Watch.Watcher.Stop()
- if err != nil {
- options.Start.Attach.Err(api.WatchLogger, err.Error())
- } else {
- lk.Watch.Watching = false
- }
- } else {
- go func() {
- _ = tracing.EventWrapFuncForErrGroup(ctx, "menu/watch", tracing.SpanOptions{},
- func(ctx context.Context) error {
- err := lk.Watch.Watcher.Start(ctx)
- if err != nil {
- options.Start.Attach.Err(api.WatchLogger, err.Error())
- } else {
- lk.Watch.Watching = true
- }
- return err
- })()
- }()
- }
- }
- func (lk *LogKeyboard) HandleKeyEvents(ctx context.Context, event keyboard.KeyEvent, project *types.Project, options api.UpOptions) {
- switch kRune := event.Rune; kRune {
- case 'v':
- lk.openDockerDesktop(ctx, project)
- case 'w':
- if !lk.Watch.IsConfigured {
- // we try to open watch docs if DD is installed
- if lk.IsDockerDesktopActive {
- lk.openDDWatchDocs(ctx, project)
- }
- // either way we mark menu/watch as an error
- go func() {
- _ = 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
- })()
- }()
- }
- lk.ToggleWatch(ctx, options)
- case 'o':
- lk.openDDComposeUI(ctx, project)
- }
- switch key := event.Key; key {
- case keyboard.KeyCtrlC:
- _ = keyboard.Close()
- lk.clearNavigationMenu()
- ShowCursor()
- lk.logLevel = NONE
- // will notify main thread to kill and will handle gracefully
- lk.signalChannel <- syscall.SIGINT
- case keyboard.KeyEnter:
- NewLine()
- lk.printNavigationMenu()
- }
- }
- func allocateSpace(lines int) {
- for i := 0; i < lines; i++ {
- ClearLine()
- NewLine()
- MoveCursorX(0)
- }
- }
- func extraLines(s string) int {
- return int(math.Floor(float64(lenAnsi(s)) / float64(goterm.Width())))
- }
- func shortcutKeyColor(key string) string {
- foreground := "38;2"
- black := "0;0;0"
- background := "48;2"
- white := "255;255;255"
- return ansiColor(foreground+";"+black+";"+background+";"+white, key, BOLD)
- }
- func navColor(key string) string {
- return ansiColor(FAINT, key)
- }
|