Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
153 changes: 135 additions & 18 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ func rootCmdRun(cmd *cobra.Command, _ []string) {
printLogo()
}
log.Debug("running in debug mode")
log.WithField("path", filepath.Join(config.Get().System.LogDirectory, "wings.log")).Info("writing log files to disk")
log.WithField("config_file", configPath).Info("loading configuration from file")

if isDockerSnap() {
Expand Down Expand Up @@ -472,25 +473,141 @@ func initLogging() {
log.WithField("path", p).Info("writing log files to disk")
}

// Prints the wings logo, nothing special here!
// bannerColor is how much color stdout can render for the startup banner.
type bannerColor int

const (
bannerPlain bannerColor = iota // not a TTY: emit no escape codes at all
banner256 // TTY without truecolor: 256-color codes
bannerTrue // TTY with truecolor: 24-bit color
)

// detectBannerColor picks the richest color mode stdout supports: plain when
// output is not a terminal, truecolor when COLORTERM advertises it, otherwise
// 256-color.
func detectBannerColor() bannerColor {
fi, err := os.Stdout.Stat()
if err != nil || fi.Mode()&os.ModeCharDevice == 0 {
return bannerPlain
}
switch os.Getenv("COLORTERM") {
case "truecolor", "24bit":
return bannerTrue
}
return banner256
Comment on lines +488 to +497

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Return plain output when color is disabled.

A character device does not guarantee 256-color support. TERM=dumb terminals receive literal ANSI escape sequences from this branch. Users who set NO_COLOR also receive color output.

Return bannerPlain before the color-mode switch when TERM is "dumb" or NO_COLOR is set.

Proposed fix
 func detectBannerColor() bannerColor {
 	fi, err := os.Stdout.Stat()
 	if err != nil || fi.Mode()&os.ModeCharDevice == 0 {
 		return bannerPlain
 	}
+	if os.Getenv("TERM") == "dumb" || os.Getenv("NO_COLOR") != "" {
+		return bannerPlain
+	}
 	switch os.Getenv("COLORTERM") {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func detectBannerColor() bannerColor {
fi, err := os.Stdout.Stat()
if err != nil || fi.Mode()&os.ModeCharDevice == 0 {
return bannerPlain
}
switch os.Getenv("COLORTERM") {
case "truecolor", "24bit":
return bannerTrue
}
return banner256
func detectBannerColor() bannerColor {
fi, err := os.Stdout.Stat()
if err != nil || fi.Mode()&os.ModeCharDevice == 0 {
return bannerPlain
}
if os.Getenv("TERM") == "dumb" || os.Getenv("NO_COLOR") != "" {
return bannerPlain
}
switch os.Getenv("COLORTERM") {
case "truecolor", "24bit":
return bannerTrue
}
return banner256
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmd/root.go` around lines 487 - 496, Update detectBannerColor to return
bannerPlain before the COLORTERM switch when TERM is "dumb" or the NO_COLOR
environment variable is set. Preserve the existing stdout character-device check
and color selection for other terminals.

}

// hue is a single semantic banner color: a 24-bit value plus a 256-color
// fallback, resolved to an SGR foreground escape for the active color mode.
type hue struct {
r, g, b, c256 int
}

func (h hue) fg(m bannerColor) string {
switch m {
case bannerTrue:
return fmt.Sprintf("\x1b[38;2;%d;%d;%dm", h.r, h.g, h.b)
case banner256:
return fmt.Sprintf("\x1b[38;5;%dm", h.c256)
default:
return ""
}
}

// ANSI Shadow "PELICAN" wordmark, six rows painted top→bottom cyan→blue.
var pelicanRows = [6]string{
`██████╗ ███████╗██╗ ██╗ ██████╗ █████╗ ███╗ ██╗`,
`██╔══██╗██╔════╝██║ ██║██╔════╝██╔══██╗████╗ ██║`,
`██████╔╝█████╗ ██║ ██║██║ ███████║██╔██╗ ██║`,
`██╔═══╝ ██╔══╝ ██║ ██║██║ ██╔══██║██║╚██╗██║`,
`██║ ███████╗███████╗██║╚██████╗██║ ██║██║ ╚████║`,
`╚═╝ ╚══════╝╚══════╝╚═╝ ╚═════╝╚═╝ ╚═╝╚═╝ ╚═══╝`,
}

var pelicanRowHues = [6]hue{
{103, 232, 249, 123}, // #67E8F9
{56, 189, 248, 117}, // #38BDF8
{14, 165, 233, 39}, // #0EA5E9
{59, 130, 246, 33}, // #3B82F6
{37, 99, 235, 27}, // #2563EB
{30, 64, 175, 25}, // #1E40AF
}

var (
hueLightCyan = hue{103, 232, 249, 123} // #67E8F9 — W I N G S letters
hueTeal = hue{22, 78, 99, 23} // #164E63 — rule end caps
hueCyan = hue{34, 211, 238, 51} // #22D3EE — ▸ accents / URLs
hueDim = hue{107, 114, 128, 243} // #6B7280 — version / descriptions
hueGray = hue{156, 163, 175, 247} // #9CA3AF — tagline
hueWhite = hue{244, 244, 245, 255} // #F4F4F5 — link labels
hueYellow = hue{250, 204, 21, 226} // #FACC15 — star CTA
)

// printLogo renders the wings startup banner once, before log output begins:
// an ANSI Shadow "PELICAN" wordmark with a vertical cyan→blue gradient, the
// WINGS rule + version, a tagline, link rows, and a star call-to-action. Color
// depth adapts to the terminal (truecolor / 256-color / none).
func printLogo() {
fmt.Printf(colorstring.Color(`
____
__ [blue][bold]Pelican[reset] _____/___/_______ _______ ______
\_____\ \/\/ / / / __ / ___/
\___\ / / / / /_/ /___ /
\___/\___/___/___/___/___ /______/
/_______/ [bold]%s[reset]

Copyright © 2018 - %d Dane Everitt & Contributors

Website: https://pelican.dev
Source: https://github.com/pelican-dev/wings
License: https://github.com/pelican-dev/wings/blob/main/LICENSE

This software is made available under the terms of the MIT license.
The above copyright notice and this permission notice shall be included
in all copies or substantial portions of the Software.%s`), system.Version, time.Now().Year(), "\n\n")
m := detectBannerColor()
bold, underline, reset := "", "", ""
if m != bannerPlain {
bold, underline, reset = "\x1b[1m", "\x1b[4m", "\x1b[0m"
}

var b strings.Builder
line := func(parts ...string) {
for _, p := range parts {
b.WriteString(p)
}
b.WriteString(reset + "\n")
}

b.WriteByte('\n')

// 1. Wordmark — one color per row.
for i, row := range pelicanRows {
line(pelicanRowHues[i].fg(m), row)
}

// 2. WINGS rule (indent 6) with the version centered beneath "W I N G S",
// which begins at column 14 and is 9 columns wide.
b.WriteByte('\n')
line(
" ",
hueTeal.fg(m), "▰▰▰▰▰▰", reset,
" ", hueLightCyan.fg(m), bold, "W I N G S", reset,
" ", hueTeal.fg(m), "▰▰▰▰▰▰",
)
vIndent := 14 + (9-len(system.Version))/2
if vIndent < 0 {
vIndent = 0
}
line(strings.Repeat(" ", vIndent), hueDim.fg(m), system.Version)

// 3. Tagline.
b.WriteByte('\n')
line(hueGray.fg(m), "Where your servers take flight.")

// 4. Link rows — "▸ <label:8> ─ <desc:19> → <url>".
b.WriteByte('\n')
linkRow := func(label, desc, url string) {
line(
hueCyan.fg(m), "▸ ", reset,
hueWhite.fg(m), bold, fmt.Sprintf("%-8s", label), reset,
hueDim.fg(m), fmt.Sprintf("─ %-19s→ ", desc), reset,
hueCyan.fg(m), url,
)
}
linkRow("Source", "Star us on GitHub", "github.com/pelican/wings")
linkRow("Docs", "Get started", "pelican.dev/docs")

// 5. Star call-to-action.
b.WriteByte('\n')
line(hueYellow.fg(m), bold, "★ Help Pelican soar: Star the project on GitHub")
line(hueCyan.fg(m), underline, "https://github.com/pelican/panel")

b.WriteByte('\n')
fmt.Print(b.String())
}

func exitWithConfigurationNotice() {
Expand Down
Loading