mirror of
https://github.com/yarlson/lnk.git
synced 2025-09-02 18:12:33 +02:00
feat(output): implement configurable color and emoji output
Add new output formatting system with flags for color and emoji control: - Introduce OutputConfig and Writer structs for flexible output handling - Add --colors and --emoji/--no-emoji global flags - Refactor commands to use new Writer for consistent formatting - Separate error content from presentation for better flexibility
This commit is contained in:
230
cmd/output.go
Normal file
230
cmd/output.go
Normal file
@@ -0,0 +1,230 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// OutputConfig controls formatting behavior
|
||||
type OutputConfig struct {
|
||||
Colors bool
|
||||
Emoji bool
|
||||
}
|
||||
|
||||
// Writer provides formatted output with configurable styling
|
||||
type Writer struct {
|
||||
out io.Writer
|
||||
config OutputConfig
|
||||
err error // first error encountered
|
||||
}
|
||||
|
||||
// NewWriter creates a new Writer with the given configuration
|
||||
func NewWriter(out io.Writer, config OutputConfig) *Writer {
|
||||
return &Writer{
|
||||
out: out,
|
||||
config: config,
|
||||
}
|
||||
}
|
||||
|
||||
// Message represents a structured message with optional formatting
|
||||
type Message struct {
|
||||
Text string
|
||||
Color string
|
||||
Emoji string
|
||||
Bold bool
|
||||
}
|
||||
|
||||
// Write outputs a message according to the writer's configuration
|
||||
func (w *Writer) Write(msg Message) *Writer {
|
||||
if w.err != nil {
|
||||
return w
|
||||
}
|
||||
|
||||
var output string
|
||||
|
||||
// Add emoji if enabled
|
||||
if w.config.Emoji && msg.Emoji != "" {
|
||||
output = msg.Emoji + " "
|
||||
}
|
||||
|
||||
// Add color/bold if enabled
|
||||
if w.config.Colors {
|
||||
if msg.Bold {
|
||||
output += "\033[1m"
|
||||
}
|
||||
if msg.Color != "" {
|
||||
output += msg.Color
|
||||
}
|
||||
}
|
||||
|
||||
output += msg.Text
|
||||
|
||||
// Close formatting if enabled
|
||||
if w.config.Colors && (msg.Bold || msg.Color != "") {
|
||||
output += "\033[0m"
|
||||
}
|
||||
|
||||
_, w.err = fmt.Fprint(w.out, output)
|
||||
return w
|
||||
}
|
||||
|
||||
// Printf is like Write but with format string
|
||||
func (w *Writer) Printf(msg Message, args ...any) *Writer {
|
||||
newMsg := msg
|
||||
newMsg.Text = fmt.Sprintf(msg.Text, args...)
|
||||
return w.Write(newMsg)
|
||||
}
|
||||
|
||||
// Writeln writes a message followed by a newline
|
||||
func (w *Writer) Writeln(msg Message) *Writer {
|
||||
return w.Write(msg).WriteString("\n")
|
||||
}
|
||||
|
||||
// WriteString outputs plain text (no formatting)
|
||||
func (w *Writer) WriteString(text string) *Writer {
|
||||
if w.err != nil {
|
||||
return w
|
||||
}
|
||||
_, w.err = fmt.Fprint(w.out, text)
|
||||
return w
|
||||
}
|
||||
|
||||
// WritelnString outputs plain text followed by a newline
|
||||
func (w *Writer) WritelnString(text string) *Writer {
|
||||
if w.err != nil {
|
||||
return w
|
||||
}
|
||||
|
||||
_, w.err = fmt.Fprintln(w.out, text)
|
||||
return w
|
||||
}
|
||||
|
||||
// ANSI color codes
|
||||
const (
|
||||
ColorRed = "\033[31m"
|
||||
ColorYellow = "\033[33m"
|
||||
ColorCyan = "\033[36m"
|
||||
ColorGray = "\033[90m"
|
||||
ColorBrightGreen = "\033[1;32m"
|
||||
ColorBrightYellow = "\033[1;33m"
|
||||
ColorBrightRed = "\033[1;31m"
|
||||
)
|
||||
|
||||
// Predefined message constructors for common patterns
|
||||
|
||||
func Success(text string) Message {
|
||||
return Message{Text: text, Color: ColorBrightGreen, Emoji: "✅", Bold: true}
|
||||
}
|
||||
|
||||
func Error(text string) Message {
|
||||
return Message{Text: text, Emoji: "❌"}
|
||||
}
|
||||
|
||||
func Warning(text string) Message {
|
||||
return Message{Text: text, Color: ColorBrightYellow, Emoji: "⚠️", Bold: true}
|
||||
}
|
||||
|
||||
func Info(text string) Message {
|
||||
return Message{Text: text, Color: ColorYellow, Emoji: "💡"}
|
||||
}
|
||||
|
||||
func Target(text string) Message {
|
||||
return Message{Text: text, Emoji: "🎯", Bold: true}
|
||||
}
|
||||
|
||||
func Rocket(text string) Message {
|
||||
return Message{Text: text, Emoji: "🚀", Bold: true}
|
||||
}
|
||||
|
||||
func Sparkles(text string) Message {
|
||||
return Message{Text: text, Emoji: "✨", Bold: true}
|
||||
}
|
||||
|
||||
func Link(text string) Message {
|
||||
return Message{Text: text, Color: ColorCyan, Emoji: "🔗"}
|
||||
}
|
||||
|
||||
func Plain(text string) Message {
|
||||
return Message{Text: text}
|
||||
}
|
||||
|
||||
func Bold(text string) Message {
|
||||
return Message{Text: text, Bold: true}
|
||||
}
|
||||
|
||||
func Colored(text, color string) Message {
|
||||
return Message{Text: text, Color: color}
|
||||
}
|
||||
|
||||
// Global output configuration
|
||||
var (
|
||||
globalConfig = OutputConfig{
|
||||
Colors: true, // auto-detect on first use
|
||||
Emoji: true,
|
||||
}
|
||||
autoDetected bool
|
||||
)
|
||||
|
||||
// SetGlobalConfig updates the global output configuration
|
||||
func SetGlobalConfig(colors string, emoji bool) error {
|
||||
switch colors {
|
||||
case "auto":
|
||||
globalConfig.Colors = isTerminal()
|
||||
case "always":
|
||||
globalConfig.Colors = true
|
||||
case "never":
|
||||
globalConfig.Colors = false
|
||||
default:
|
||||
return fmt.Errorf("invalid color mode: %s (valid: auto, always, never)", colors)
|
||||
}
|
||||
|
||||
// Check NO_COLOR environment variable (explicit flag takes precedence)
|
||||
if os.Getenv("NO_COLOR") != "" && colors == "auto" {
|
||||
globalConfig.Colors = false
|
||||
}
|
||||
|
||||
globalConfig.Emoji = emoji
|
||||
autoDetected = true
|
||||
return nil
|
||||
}
|
||||
|
||||
// isTerminal checks if stdout is a terminal
|
||||
func isTerminal() bool {
|
||||
fileInfo, err := os.Stdout.Stat()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return (fileInfo.Mode() & os.ModeCharDevice) != 0
|
||||
}
|
||||
|
||||
// autoDetectConfig performs one-time auto-detection if not explicitly configured
|
||||
func autoDetectConfig() {
|
||||
if !autoDetected {
|
||||
if os.Getenv("NO_COLOR") != "" {
|
||||
globalConfig.Colors = false
|
||||
} else {
|
||||
globalConfig.Colors = isTerminal()
|
||||
}
|
||||
autoDetected = true
|
||||
}
|
||||
}
|
||||
|
||||
// GetWriter returns a writer for the given cobra command
|
||||
func GetWriter(cmd *cobra.Command) *Writer {
|
||||
autoDetectConfig()
|
||||
return NewWriter(cmd.OutOrStdout(), globalConfig)
|
||||
}
|
||||
|
||||
// GetErrorWriter returns a writer for stderr
|
||||
func GetErrorWriter() *Writer {
|
||||
autoDetectConfig()
|
||||
return NewWriter(os.Stderr, globalConfig)
|
||||
}
|
||||
|
||||
// Err returns the first error encountered during writing
|
||||
func (w *Writer) Err() error {
|
||||
return w.err
|
||||
}
|
Reference in New Issue
Block a user