diff --git a/cmd/manifest/validate.go b/cmd/manifest/validate.go index c98ee063..bbc65698 100644 --- a/cmd/manifest/validate.go +++ b/cmd/manifest/validate.go @@ -93,14 +93,14 @@ func NewValidateCommand(clients *shared.ClientFactory) *cobra.Command { cmd.Printf( "\n%s: %s\n", style.Bold("App Manifest Validation Result"), - style.Styler().Green("Valid"), + style.Green("Valid"), ) clients.IO.PrintTrace(ctx, slacktrace.ManifestValidateSuccess) } else { cmd.Printf( "\n%s: %s\n", style.Bold("App Manifest Validation Result"), - style.Styler().Red("InValid"), + style.Red("InValid"), ) } } diff --git a/cmd/root.go b/cmd/root.go index 7b78e246..6c5e264c 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -44,6 +44,7 @@ import ( "github.com/slackapi/slack-cli/cmd/upgrade" versioncmd "github.com/slackapi/slack-cli/cmd/version" "github.com/slackapi/slack-cli/internal/cmdutil" + "github.com/slackapi/slack-cli/internal/experiment" "github.com/slackapi/slack-cli/internal/iostreams" "github.com/slackapi/slack-cli/internal/pkg/version" "github.com/slackapi/slack-cli/internal/shared" @@ -297,6 +298,7 @@ func InitConfig(ctx context.Context, clients *shared.ClientFactory, rootCmd *cob // Init configurations clients.Config.LoadExperiments(ctx, clients.IO.PrintDebug) + style.ToggleCharm(clients.Config.WithExperimentOn(experiment.Charm)) // TODO(slackcontext) Consolidate storing CLI version to slackcontext clients.Config.Version = clients.CLIVersion diff --git a/cmd/upgrade/upgrade.go b/cmd/upgrade/upgrade.go index 75488fe5..8cc58cae 100644 --- a/cmd/upgrade/upgrade.go +++ b/cmd/upgrade/upgrade.go @@ -74,9 +74,9 @@ func checkForUpdates(clients *shared.ClientFactory, cmd *cobra.Command) error { } if clients.SDKConfig.Hooks.CheckUpdate.IsAvailable() { - cmd.Printf("%s You are using the latest Slack CLI and SDK versions\n", style.Styler().Green("✔").String()) + cmd.Printf("%s You are using the latest Slack CLI and SDK versions\n", style.Green("✔")) } else { - cmd.Printf("%s You are using the latest Slack CLI version\n", style.Styler().Green("✔").String()) + cmd.Printf("%s You are using the latest Slack CLI version\n", style.Green("✔")) } return nil diff --git a/internal/iostreams/printer.go b/internal/iostreams/printer.go index 3510765e..d79836cf 100644 --- a/internal/iostreams/printer.go +++ b/internal/iostreams/printer.go @@ -89,7 +89,7 @@ func (io *IOStreams) PrintInfo(ctx context.Context, shouldTrace bool, format str span, _ := opentracing.StartSpanFromContext(ctx, "printInfo", opentracing.Tag{Key: "printInfo", Value: message}) defer span.Finish() } - io.Stdout.Println(style.Styler().Reset(message)) + io.Stdout.Println(message) } // PrintTrace prints traceID and values to stdout if SLACK_TEST_TRACE=true diff --git a/internal/pkg/apps/install.go b/internal/pkg/apps/install.go index 7bfe922a..135f1ab6 100644 --- a/internal/pkg/apps/install.go +++ b/internal/pkg/apps/install.go @@ -869,12 +869,12 @@ func continueDespiteWarning(ctx context.Context, clients *shared.ClientFactory, clients.IO.PrintInfo(ctx, false, "\n%s: %s", style.Bold("Changes confirmed"), - style.Styler().Green("Continuing with install."), + style.Green("Continuing with install."), ) return true, nil } - clients.IO.PrintInfo(ctx, false, "\n%s", style.Styler().Red("App install canceled.")) + clients.IO.PrintInfo(ctx, false, "\n%s", style.Red("App install canceled.")) return false, nil } diff --git a/internal/pkg/platform/activity.go b/internal/pkg/platform/activity.go index b155c225..458d95cd 100644 --- a/internal/pkg/platform/activity.go +++ b/internal/pkg/platform/activity.go @@ -220,9 +220,9 @@ func prettifyActivity(activity api.Activity) (log string) { switch activity.Level { case types.WARN: - return style.Styler().Yellow(msg).String() + return style.Yellow(msg) case types.ERROR, types.FATAL: - return style.Styler().Red(msg).String() + return style.Red(msg) } return msg @@ -282,7 +282,7 @@ func externalAuthResultToString(activity api.Activity) (result string) { msg = msg + "\n\t\t" + strings.ReplaceAll(activity.Payload["extra_message"].(string), "\n", "\n\t\t") } - return style.Styler().Gray(13, msg).String() + return style.Gray(msg) } func externalAuthStartedToString(activity api.Activity) (result string) { @@ -300,7 +300,7 @@ func externalAuthStartedToString(activity api.Activity) (result string) { msg = msg + "\n\t" + strings.ReplaceAll(activity.Payload["code"].(string), "\n", "\n\t") } - return style.Styler().Gray(13, msg).String() + return style.Gray(msg) } func externalAuthTokenFetchResult(activity api.Activity) (result string) { @@ -318,13 +318,13 @@ func externalAuthTokenFetchResult(activity api.Activity) (result string) { msg = msg + "\n\t" + strings.ReplaceAll(activity.Payload["code"].(string), "\n", "\n\t") } - return style.Styler().Gray(13, msg).String() + return style.Gray(msg) } func functionDeploymentToString(activity api.Activity) (result string) { msg := fmt.Sprintf("Application %sd by user '%s' on team '%s'", activity.Payload["action"], activity.Payload["user_id"], activity.Payload["team_id"]) msg = fmt.Sprintf("%s %s [%s] %s", style.Emoji("cloud"), activity.CreatedPretty(), activity.Level, msg) - return style.Styler().Gray(13, msg).String() + return style.Gray(msg) } func functionExecutionOutputToString(activity api.Activity) (result string) { diff --git a/internal/style/charm_theme.go b/internal/style/charm_theme.go index cfdc72c1..dbc646e7 100644 --- a/internal/style/charm_theme.go +++ b/internal/style/charm_theme.go @@ -15,27 +15,13 @@ package style // Slack brand theme for charmbracelet/huh prompts. -// Uses official Slack brand colors to give the CLI a fun, playful feel. +// Uses official Slack brand colors defined in colors.go. import ( "github.com/charmbracelet/huh" "github.com/charmbracelet/lipgloss" ) -// Slack brand colors according to https://a.slack-edge.com/4d5bb/marketing/img/media-kit/slack_brand_guidelines_september2020.pdf -var ( - slackAubergine = lipgloss.Color("#7C2852") - slackBlue = lipgloss.Color("#36c5f0") - slackGreen = lipgloss.Color("#2eb67d") - slackYellow = lipgloss.Color("#ecb22e") - slackRed = lipgloss.Color("#e01e5a") - slackPool = lipgloss.Color("#78d7dd") - slackLegalGray = lipgloss.Color("#5e5d60") - slackOptionText = lipgloss.AdaptiveColor{Light: "#1d1c1d", Dark: "#f4ede4"} - slackDescriptionText = lipgloss.AdaptiveColor{Light: "#454447", Dark: "#b9b5b0"} - slackPlaceholderText = lipgloss.AdaptiveColor{Light: "#5e5d60", Dark: "#868380"} -) - // ThemeSlack returns a huh theme styled with Slack brand colors. func ThemeSlack() *huh.Theme { t := huh.ThemeBase() diff --git a/internal/style/colors.go b/internal/style/colors.go new file mode 100644 index 00000000..dbe9ae7c --- /dev/null +++ b/internal/style/colors.go @@ -0,0 +1,45 @@ +// Copyright 2022-2026 Salesforce, Inc. +// +// 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 style + +// Slack brand color palette. +// Single source of truth for all styling: lipgloss, huh themes, and bubbletea components. +// +// Colors from https://a.slack-edge.com/4d5bb/marketing/img/media-kit/slack_brand_guidelines_september2020.pdf + +import "github.com/charmbracelet/lipgloss" + +// Brand colors +var ( + slackAubergine = lipgloss.Color("#7C2852") + slackBlue = lipgloss.Color("#36c5f0") + slackGreen = lipgloss.Color("#2eb67d") + slackYellow = lipgloss.Color("#ecb22e") + slackRed = lipgloss.Color("#e01e5a") + slackRedDark = lipgloss.Color("#a01040") +) + +// Supplementary colors +var ( + slackPool = lipgloss.Color("#78d7dd") + slackLegalGray = lipgloss.Color("#5e5d60") +) + +// Adaptive colors that adjust for light/dark terminal backgrounds +var ( + slackOptionText = lipgloss.AdaptiveColor{Light: "#1d1c1d", Dark: "#f4ede4"} + slackDescriptionText = lipgloss.AdaptiveColor{Light: "#454447", Dark: "#b9b5b0"} + slackPlaceholderText = lipgloss.AdaptiveColor{Light: "#5e5d60", Dark: "#868380"} +) diff --git a/internal/style/style.go b/internal/style/style.go index dcf3bb30..407c7fd3 100644 --- a/internal/style/style.go +++ b/internal/style/style.go @@ -19,6 +19,7 @@ import ( "runtime" "strings" + "github.com/charmbracelet/lipgloss" "github.com/kyokomi/emoji/v2" "github.com/logrusorgru/aurora/v4" ) @@ -32,28 +33,8 @@ var isColorShown = isStyleEnabled // isLinkShown specifies if hyperlinks should be formatted var isLinkShown = isStyleEnabled -// ANSI escape sequence color code -// -// Non-grayscale codes are selected from -// https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit -// -// Grayscale codes might be ANSI or selected from -// https://github.com/logrusorgru/aurora#grayscale -// -// TODO: check whether tty supports 256; if not, simplify to top 8 colors -// https://unix.stackexchange.com/questions/9957/how-to-check-if-bash-can-print-colors -const ( - blueDark = 32 - blue = 39 - grayDark = 236 - gray = 246 - grayLight = 12 - green = 29 - red = 196 - redDark = 1 - whiteOffset = 21 // 235 in ANSI - yellow = 178 -) +// isCharmEnabled specifies if lipgloss/charm styling should be used instead of aurora +var isCharmEnabled = false // RemoveANSI uses regex to strip ANSI colour codes // @@ -71,12 +52,17 @@ func ToggleStyles(active bool) { isLinkShown = active } -func Styler() *aurora.Aurora { - config := aurora.NewConfig() - config.Colors = isColorShown - config.Hyperlinks = isLinkShown +// ToggleCharm enables lipgloss-based styling when set to true +func ToggleCharm(active bool) { + isCharmEnabled = active +} - return aurora.New(config.Options()...) +// render applies a lipgloss style to text, returning plain text when colors are disabled. +func render(s lipgloss.Style, text string) string { + if !isColorShown { + return text + } + return s.Render(text) } func Emoji(alias string) string { @@ -122,37 +108,93 @@ Color styles // Secondary dims the displayed text func Secondary(text string) string { - return Styler().Gray(grayLight, text).String() + if !isCharmEnabled { + return legacySecondary(text) + } + return render(lipgloss.NewStyle().Foreground(slackDescriptionText), text) } // CommandText emphasizes command text func CommandText(text string) string { - return Styler().Index(blue, text).Bold().String() + if !isCharmEnabled { + return legacyCommandText(text) + } + return render(lipgloss.NewStyle().Foreground(slackBlue).Bold(true), text) } // LinkText underlines and formats the provided path func LinkText(path string) string { - return Styler().Gray(grayLight, path).Underline().String() + if !isCharmEnabled { + return legacyLinkText(path) + } + return render(lipgloss.NewStyle().Foreground(slackDescriptionText).Underline(true), path) } func Selector(text string) string { - return Styler().Index(green, text).Bold().String() + if !isCharmEnabled { + return legacySelector(text) + } + return render(lipgloss.NewStyle().Foreground(slackGreen).Bold(true), text) } func Error(text string) string { - return Styler().Index(red, text).Bold().String() + if !isCharmEnabled { + return legacyError(text) + } + return render(lipgloss.NewStyle().Foreground(slackRed).Bold(true), text) } func Warning(text string) string { - return Styler().Index(yellow, text).Bold().String() + if !isCharmEnabled { + return legacyWarning(text) + } + return render(lipgloss.NewStyle().Foreground(slackYellow).Bold(true), text) } func Header(text string) string { - return Styler().Bold(strings.ToUpper(text)).String() + if !isCharmEnabled { + return legacyHeader(text) + } + return render(lipgloss.NewStyle().Foreground(slackAubergine).Bold(true), strings.ToUpper(text)) } func Input(text string) string { - return Styler().Index(blue, text).String() + if !isCharmEnabled { + return legacyInput(text) + } + return render(lipgloss.NewStyle().Foreground(slackBlue), text) +} + +// Green applies green color to text without bold +func Green(text string) string { + if !isCharmEnabled { + return legacyGreen(text) + } + return render(lipgloss.NewStyle().Foreground(slackGreen), text) +} + +// Red applies red color to text without bold +func Red(text string) string { + if !isCharmEnabled { + return legacyRed(text) + } + return render(lipgloss.NewStyle().Foreground(slackRedDark), text) +} + +// Yellow applies yellow color to text without bold +func Yellow(text string) string { + if !isCharmEnabled { + return legacyYellow(text) + } + return render(lipgloss.NewStyle().Foreground(slackYellow), text) +} + +// Gray applies a subdued gray color to text +func Gray(text string) string { + if !isCharmEnabled { + return legacyGray(text) + } + return render(lipgloss.NewStyle().Foreground(slackLegalGray), text) } /* @@ -161,17 +203,26 @@ Text styles // Bright is a strong bold version of the text func Bright(text string) string { - return Styler().Bold(text).String() + if !isCharmEnabled { + return legacyBright(text) + } + return render(lipgloss.NewStyle().Bold(true), text) } // Bold brightly emboldens the provided text func Bold(text string) string { - return Styler().Gray(whiteOffset, text).Bold().String() + if !isCharmEnabled { + return legacyBold(text) + } + return render(lipgloss.NewStyle().Foreground(slackOptionText).Bold(true), text) } // Darken adds a bold gray shade to text func Darken(text string) string { - return Styler().Index(gray, text).Bold().String() + if !isCharmEnabled { + return legacyDarken(text) + } + return render(lipgloss.NewStyle().Foreground(slackPlaceholderText).Bold(true), text) } // Faint resets all effects then decreases text intensity @@ -179,17 +230,26 @@ func Faint(text string) string { if !isColorShown { return text } - return "\x1b[0;2m" + text + "\x1b[0m" + if !isCharmEnabled { + return legacyFaint(text) + } + return lipgloss.NewStyle().Faint(true).Render(text) } // Highlight adds emphasis to text func Highlight(text string) string { - return Styler().Bold(text).String() + if !isCharmEnabled { + return legacyHighlight(text) + } + return render(lipgloss.NewStyle().Bold(true), text) } // Underline underscores the given text func Underline(text string) string { - return Styler().Underline(text).String() + if !isCharmEnabled { + return legacyUnderline(text) + } + return render(lipgloss.NewStyle().Underline(true), text) } /* @@ -201,3 +261,51 @@ func Pluralize(singular string, plural string, count int) string { } return plural } + +// ════════════════════════════════════════════════════════════════════════════════ +// DEPRECATED: Legacy aurora styling +// +// Delete this entire section, the aurora import, and the ANSI color constants +// when the charm experiment is permanently enabled. +// ════════════════════════════════════════════════════════════════════════════════ + +const ( + blueDark = 32 + blue = 39 + grayDark = 236 + gray = 246 + grayLight = 12 + green = 29 + red = 196 + redDark = 1 + whiteOffset = 21 // 235 in ANSI + yellow = 178 +) + +// DEPRECATED: Styler returns an aurora instance for legacy styling. +// Use the style functions (Secondary, CommandText, Error, etc.) instead. +func Styler() *aurora.Aurora { + config := aurora.NewConfig() + config.Colors = isColorShown + config.Hyperlinks = isLinkShown + return aurora.New(config.Options()...) +} + +func legacySecondary(text string) string { return Styler().Gray(grayLight, text).String() } +func legacyCommandText(text string) string { return Styler().Index(blue, text).Bold().String() } +func legacyLinkText(path string) string { return Styler().Gray(grayLight, path).Underline().String() } +func legacySelector(text string) string { return Styler().Index(green, text).Bold().String() } +func legacyError(text string) string { return Styler().Index(red, text).Bold().String() } +func legacyWarning(text string) string { return Styler().Index(yellow, text).Bold().String() } +func legacyHeader(text string) string { return Styler().Bold(strings.ToUpper(text)).String() } +func legacyInput(text string) string { return Styler().Index(blue, text).String() } +func legacyGreen(text string) string { return Styler().Index(green, text).String() } +func legacyRed(text string) string { return Styler().Index(redDark, text).String() } +func legacyYellow(text string) string { return Styler().Index(yellow, text).String() } +func legacyGray(text string) string { return Styler().Gray(13, text).String() } +func legacyBright(text string) string { return Styler().Bold(text).String() } +func legacyBold(text string) string { return Styler().Gray(whiteOffset, text).Bold().String() } +func legacyDarken(text string) string { return Styler().Index(gray, text).Bold().String() } +func legacyFaint(text string) string { return "\x1b[0;2m" + text + "\x1b[0m" } +func legacyHighlight(text string) string { return Styler().Bold(text).String() } +func legacyUnderline(text string) string { return Styler().Underline(text).String() } diff --git a/internal/style/template.go b/internal/style/template.go index 9a7766a1..6d8125c3 100644 --- a/internal/style/template.go +++ b/internal/style/template.go @@ -33,7 +33,7 @@ type TemplateData map[string]interface{} func getTemplateFuncs() template.FuncMap { return template.FuncMap{ "Title": func(s string) string { - return Styler().Bold(strings.ToUpper(s)).String() + return Header(s) }, "IsAlias": func(cmdName string, aliases map[string]string) bool { _, exists := aliases[cmdName] @@ -79,7 +79,7 @@ func getTemplateFuncs() template.FuncMap { "GetProcessName": processName, "Error": func(message string, code string) string { text := fmt.Sprintf("Error: %s (%s)", message, code) - return Styler().Index(redDark, text).String() + return Red(text) }, "Suggestion": func(remediation string) string { text := fmt.Sprintf("Suggestion: %s", remediation) @@ -92,7 +92,7 @@ func getTemplateFuncs() template.FuncMap { return Selector(text) }, "Red": func(text string) string { - return Styler().Index(redDark, text).String() + return Red(text) }, "rpad": func(s string, padding int) string { formattedString := fmt.Sprintf("%%-%ds", padding) diff --git a/internal/update/sdk.go b/internal/update/sdk.go index ecc3089d..c38d1435 100644 --- a/internal/update/sdk.go +++ b/internal/update/sdk.go @@ -356,10 +356,10 @@ func printInstallUpdateResponse(updateInfo SDKInstallUpdateResponse) { if update.PreviousVersion != update.InstalledVersion { fmt.Printf( style.Indent(" %s %s\n %s → %s\n\n"), - style.Styler().Green("✔"), + style.Green("✔"), style.Bold(update.Name), style.Secondary(update.PreviousVersion), - style.Styler().Green(update.InstalledVersion), + style.Green(update.InstalledVersion), ) } }