From 3c32ab3445f080e586f92f92b30ec1ba43c831d0 Mon Sep 17 00:00:00 2001 From: David Schmitt Date: Thu, 13 Jun 2024 17:14:39 +0200 Subject: [PATCH] Render ASCII-only spinner and icons on conhost.exe Fixes #338 --- cmd/root.go | 5 ++-- cmd/tea_initialisesources.go | 7 +++-- cmd/tea_plan.go | 3 +-- cmd/tea_submitplan.go | 6 ++--- cmd/tea_taskmodel.go | 6 ++--- cmd/terraform_apply.go | 5 ++-- cmd/terraform_plan.go | 4 +-- cmd/theme.go | 23 ++++++++++++++++ cmd/theme_darwin.go | 8 ++++++ cmd/theme_linux.go | 51 ++++++++++++++++++++++++++++++++++++ cmd/theme_windows.go | 10 +++++++ 11 files changed, 107 insertions(+), 21 deletions(-) create mode 100644 cmd/theme_darwin.go create mode 100644 cmd/theme_linux.go create mode 100644 cmd/theme_windows.go diff --git a/cmd/root.go b/cmd/root.go index bb154902..e419c1f6 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -14,7 +14,6 @@ import ( "connectrpc.com/connect" tea "github.com/charmbracelet/bubbletea" - "github.com/charmbracelet/lipgloss" "github.com/google/uuid" "github.com/overmindtech/cli/tracing" "github.com/overmindtech/sdp-go" @@ -238,9 +237,9 @@ func (m authenticateModel) View() string { output = markdownToString(m.width, prompt) case Authenticated: - output = wrap(lipgloss.NewStyle().Foreground(ColorPalette.BgSuccess).Render("✔︎")+" Authenticated successfully. Press any key to continue.", m.width-4, 2) + output = wrap(RenderOk()+" Authenticated successfully. Press any key to continue.", m.width-4, 2) case ErrorAuthenticating: - output = wrap(lipgloss.NewStyle().Foreground(ColorPalette.BgDanger).Render("✗")+" Unable to authenticate. Please try again.", m.width-4, 2) + output = wrap(RenderErr()+" Unable to authenticate. Please try again.", m.width-4, 2) } return containerStyle.Render(output) diff --git a/cmd/tea_initialisesources.go b/cmd/tea_initialisesources.go index e39d3abe..b2af7c75 100644 --- a/cmd/tea_initialisesources.go +++ b/cmd/tea_initialisesources.go @@ -12,7 +12,6 @@ import ( "connectrpc.com/connect" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/huh" - "github.com/charmbracelet/lipgloss" "github.com/overmindtech/sdp-go" log "github.com/sirupsen/logrus" "github.com/spf13/viper" @@ -271,7 +270,7 @@ func (m initialiseSourcesModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { func (m initialiseSourcesModel) View() string { bits := []string{m.taskModel.View()} for _, hint := range m.errorHints { - bits = append(bits, wrap(fmt.Sprintf(" %v %v", lipgloss.NewStyle().Foreground(ColorPalette.BgDanger).Render("✗"), hint), m.width, 2)) + bits = append(bits, wrap(fmt.Sprintf(" %v %v", RenderErr(), hint), m.width, 2)) } if m.awsConfigForm != nil && !m.awsConfigFormDone { bits = append(bits, m.awsConfigForm.View()) @@ -280,10 +279,10 @@ func (m initialiseSourcesModel) View() string { bits = append(bits, m.profileInputForm.View()) } if m.awsSourceRunning { - bits = append(bits, wrap(fmt.Sprintf(" %v AWS Source: running", lipgloss.NewStyle().Foreground(ColorPalette.BgSuccess).Render("✔︎")), m.width, 4)) + bits = append(bits, wrap(fmt.Sprintf(" %v AWS Source: running", RenderOk()), m.width, 4)) } if m.stdlibSourceRunning { - bits = append(bits, wrap(fmt.Sprintf(" %v stdlib Source: running", lipgloss.NewStyle().Foreground(ColorPalette.BgSuccess).Render("✔︎")), m.width, 4)) + bits = append(bits, wrap(fmt.Sprintf(" %v stdlib Source: running", RenderOk()), m.width, 4)) } return strings.Join(bits, "\n") } diff --git a/cmd/tea_plan.go b/cmd/tea_plan.go index 6c95cd3d..a103af64 100644 --- a/cmd/tea_plan.go +++ b/cmd/tea_plan.go @@ -8,7 +8,6 @@ import ( "strings" tea "github.com/charmbracelet/bubbletea" - "github.com/charmbracelet/lipgloss" "github.com/overmindtech/cli/tracing" "github.com/spf13/viper" ) @@ -131,7 +130,7 @@ func (m runPlanModel) View() string { case taskStatusPending, taskStatusRunning: bits = append(bits, wrap(fmt.Sprintf("%v Running 'terraform %v'", - lipgloss.NewStyle().Foreground(ColorPalette.BgSuccess).Render("✔︎"), + RenderOk(), strings.Join(m.args, " "), ), m.width, 2)) case taskStatusDone: diff --git a/cmd/tea_submitplan.go b/cmd/tea_submitplan.go index f81073f8..a66f4b5b 100644 --- a/cmd/tea_submitplan.go +++ b/cmd/tea_submitplan.go @@ -211,18 +211,16 @@ func (m submitPlanModel) View() string { if m.resourceExtractionTask.status != taskStatusPending { bits = append(bits, m.resourceExtractionTask.View()) if m.mappedItemDiffs.numTotalChanges > 0 { - greenTick := lipgloss.NewStyle().Foreground(ColorPalette.BgSuccess).Render("✔︎") supportedTypes := maps.Keys(m.mappedItemDiffs.supported) slices.Sort[[]string](supportedTypes) for _, typ := range supportedTypes { - bits = append(bits, fmt.Sprintf(" %v %v (%v)", greenTick, typ, len(m.mappedItemDiffs.supported[typ]))) + bits = append(bits, fmt.Sprintf(" %v %v (%v)", RenderOk(), typ, len(m.mappedItemDiffs.supported[typ]))) } - yellowCross := lipgloss.NewStyle().Foreground(ColorPalette.BgWarning).Render("✗") unsupportedTypes := maps.Keys(m.mappedItemDiffs.unsupported) slices.Sort[[]string](unsupportedTypes) for _, typ := range unsupportedTypes { - bits = append(bits, fmt.Sprintf(" %v %v (%v)", yellowCross, typ, len(m.mappedItemDiffs.unsupported[typ]))) + bits = append(bits, fmt.Sprintf(" %v %v (%v)", RenderErr(), typ, len(m.mappedItemDiffs.unsupported[typ]))) } } } diff --git a/cmd/tea_taskmodel.go b/cmd/tea_taskmodel.go index e44678cc..7a6ed685 100644 --- a/cmd/tea_taskmodel.go +++ b/cmd/tea_taskmodel.go @@ -68,7 +68,7 @@ func NewTaskModel(title string, width int) taskModel { status: taskStatusPending, title: title, spinner: spinner.New( - spinner.WithSpinner(DotsSpinner), + spinner.WithSpinner(PlatformSpinner()), spinner.WithStyle(lipgloss.NewStyle().Foreground(ColorPalette.BgMain)), ), width: width, @@ -118,9 +118,9 @@ func (m taskModel) View() string { case taskStatusRunning: label = m.spinner.View() case taskStatusDone: - label = lipgloss.NewStyle().Foreground(ColorPalette.BgSuccess).Render("✔︎") + label = RenderOk() case taskStatusError: - label = lipgloss.NewStyle().Foreground(ColorPalette.BgDanger).Render("✗") + label = RenderErr() case taskStatusSkipped: label = lipgloss.NewStyle().Foreground(ColorPalette.LabelFaint).Render("-") default: diff --git a/cmd/terraform_apply.go b/cmd/terraform_apply.go index 29ff70e6..e7c6cf06 100644 --- a/cmd/terraform_apply.go +++ b/cmd/terraform_apply.go @@ -11,7 +11,6 @@ import ( "connectrpc.com/connect" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/huh" - "github.com/charmbracelet/lipgloss" "github.com/google/uuid" "github.com/overmindtech/cli/tracing" "github.com/overmindtech/sdp-go" @@ -404,7 +403,7 @@ func (m tfApplyModel) View() string { if m.runTfApply { bits = append(bits, wrap(fmt.Sprintf("%v Running 'terraform %v'", - lipgloss.NewStyle().Foreground(ColorPalette.BgSuccess).Render("✔︎"), + RenderOk(), strings.Join(m.args, " "), ), m.width, 2)) } @@ -412,7 +411,7 @@ func (m tfApplyModel) View() string { if m.isEnding && m.endingChangeSnapshot.overall.status != taskStatusPending { bits = append(bits, wrap(fmt.Sprintf("%v Ran 'terraform %v'", - lipgloss.NewStyle().Foreground(ColorPalette.BgSuccess).Render("✔︎"), + RenderOk(), strings.Join(m.args, " "), ), m.width, 2)) bits = append(bits, m.endingChangeSnapshot.View()) diff --git a/cmd/terraform_plan.go b/cmd/terraform_plan.go index dbe7afff..fedc9f5f 100644 --- a/cmd/terraform_plan.go +++ b/cmd/terraform_plan.go @@ -402,10 +402,10 @@ func mappedItemDiffsFromPlan(ctx context.Context, planJson []byte, fileName stri // Log the types for typ, plannedChanges := range plannedChangeGroupsVar.supported { - log.WithContext(ctx).Infof(Green.Color(" ✓ %v (%v)"), typ, len(plannedChanges)) + log.WithContext(ctx).Infof(" %v %v (%v)", RenderOk(), typ, len(plannedChanges)) } for typ, plannedChanges := range plannedChangeGroupsVar.unsupported { - log.WithContext(ctx).Infof(Yellow.Color(" ✗ %v (%v)"), typ, len(plannedChanges)) + log.WithContext(ctx).Infof(" %v %v (%v)", RenderErr(), typ, len(plannedChanges)) } return removedSecrets, plannedChangeGroupsVar.MappedItemDiffs(), mappedItemDiffsMsg{ diff --git a/cmd/theme.go b/cmd/theme.go index 8118123b..80d0c55a 100644 --- a/cmd/theme.go +++ b/cmd/theme.go @@ -375,3 +375,26 @@ func wrap(s string, width, indent int) string { return strings.ReplaceAll(wordwrap.String(s, width-indent), "\n", "\n"+strings.Repeat(" ", indent)) } + +func RenderOk() string { + checkMark := "✔︎" + if IsConhost() { + checkMark = "OK" + } + return lipgloss.NewStyle().Foreground(ColorPalette.BgSuccess).Render(checkMark) +} + +func RenderErr() string { + checkMark := "✗" + if IsConhost() { + checkMark = "ERR" + } + return lipgloss.NewStyle().Foreground(ColorPalette.BgDanger).Render(checkMark) +} + +func PlatformSpinner() spinner.Spinner { + if IsConhost() { + return spinner.Line + } + return DotsSpinner +} diff --git a/cmd/theme_darwin.go b/cmd/theme_darwin.go new file mode 100644 index 00000000..c0cdaea1 --- /dev/null +++ b/cmd/theme_darwin.go @@ -0,0 +1,8 @@ +package cmd + +// IsConhost returns true if the current terminal is conhost. This indicates +// that it can't deal with multi-byte characters and requires special treatment. +// See https://github.com/overmindtech/cli/issues/388 for detailed analysis. +func IsConhost() bool { + return false +} diff --git a/cmd/theme_linux.go b/cmd/theme_linux.go new file mode 100644 index 00000000..692ba9c8 --- /dev/null +++ b/cmd/theme_linux.go @@ -0,0 +1,51 @@ +package cmd + +import ( + "bytes" + "os" + "sync" +) + +var isWslCache int // 0 = unset; 1 = WSL; 2 = not WSL +var isWslCacheMu sync.RWMutex + +// IsConhost returns true if the current terminal is conhost. This indicates +// that it can't deal with multi-byte characters and requires special treatment. +// See https://github.com/overmindtech/cli/issues/388 for detailed analysis. +func IsConhost() bool { + // shortcut this if we (probably) run in Windows Terminal (through WSL) or + // on something that smells like a regular Linux terminal + if os.Getenv("WT_SESSION") != "" { + return false + } + + isWslCacheMu.RLock() + w := isWslCache + isWslCacheMu.RUnlock() + + if w == 1 { + return true + } else if w == 2 { + return false + } + + // isWslCache has not yet been initialised, so we need to check if we are in WSL + // since we don't know if we are in WSL, we need to check now + isWslCacheMu.Lock() + defer isWslCacheMu.Unlock() + if w != 0 { + // someone else raced the lock and has already decided + return isWslCache == 1 + } + + // check if we run in WSL + ver, err := os.ReadFile("/proc/version") + if err == nil && bytes.Contains(ver, []byte("Microsoft")) { + isWslCache = 1 + return true + } + + // we can't access /proc/version or it does not contain Microsoft, we are _probably_ not in WSL + isWslCache = 2 + return false +} diff --git a/cmd/theme_windows.go b/cmd/theme_windows.go new file mode 100644 index 00000000..4b1c7352 --- /dev/null +++ b/cmd/theme_windows.go @@ -0,0 +1,10 @@ +package cmd + +import "os" + +// IsConhost returns true if the current terminal is conhost. This indicates +// that it can't deal with multi-byte characters and requires special treatment. +// See https://github.com/overmindtech/cli/issues/388 for detailed analysis. +func IsConhost() bool { + return os.Getenv("WT_SESSION") == "" +}