From 48369a6a9054b0e606eb41575e385d0c02768e18 Mon Sep 17 00:00:00 2001 From: "m.kindritskiy" Date: Sun, 14 Jun 2026 14:25:37 +0300 Subject: [PATCH] Style pre-execute errors via logrus formatter --- internal/cli/cli.go | 2 +- internal/logging/formatter.go | 78 ++++++++++++++++++++++++++++++++++- internal/logging/log.go | 8 ++-- internal/logging/log_test.go | 2 +- 4 files changed, 84 insertions(+), 6 deletions(-) diff --git a/internal/cli/cli.go b/internal/cli/cli.go index 4d680cee..6ef9b0fc 100644 --- a/internal/cli/cli.go +++ b/internal/cli/cli.go @@ -46,7 +46,7 @@ func Main(version string, buildDate string) int { appSettings.Apply() - logging.InitLogging(os.Stdout, os.Stderr) + logging.InitLogging(os.Stdout, os.Stderr, theme.ColorSchemeByName(appSettings.Theme)) rootCmd := cmd.CreateRootCommand(version, buildDate) rootCmd.InitDefaultHelpFlag() diff --git a/internal/logging/formatter.go b/internal/logging/formatter.go index 93e910fa..0b4a8322 100644 --- a/internal/logging/formatter.go +++ b/internal/logging/formatter.go @@ -3,9 +3,15 @@ package logging import ( "bytes" "fmt" + "io" + "os" "strings" + "unicode" + "charm.land/lipgloss/v2" + "github.com/charmbracelet/x/term" "github.com/fatih/color" + "github.com/lets-cli/fang" log "github.com/sirupsen/logrus" ) @@ -15,11 +21,63 @@ type LogRepresenter interface { Repr() string } +type errorStyles struct { + header lipgloss.Style + text lipgloss.Style +} + // Formatter formats a log entry in a human readable way. -type Formatter struct{} +type Formatter struct { + errorStyles *errorStyles // nil when output is not a TTY or no scheme given +} + +// newFormatter builds a Formatter, enabling lipgloss error styling when +// errWriter is a terminal and cs is non-nil. +func newFormatter(errWriter io.Writer, cs fang.ColorSchemeFunc) *Formatter { + f := &Formatter{} + if cs == nil { + return f + } + + file, ok := errWriter.(term.File) + if !ok || !term.IsTerminal(file.Fd()) { + return f + } + + isDark := lipgloss.HasDarkBackground(os.Stdin, file) + scheme := cs(lipgloss.LightDark(isDark)) + + w, _, err := term.GetSize(file.Fd()) + if err != nil || w == 0 { + w = 160 + } + if w > 160 { + w = 160 + } + + f.errorStyles = &errorStyles{ + header: lipgloss.NewStyle(). + Foreground(scheme.ErrorHeader[0]). + Background(scheme.ErrorHeader[1]). + Bold(true). + Padding(0, 1). + Margin(1). + MarginLeft(2). + SetString("ERROR"), + text: lipgloss.NewStyle(). + MarginLeft(2). + Width(w - 2), + } + + return f +} // Format implements the log.Formatter interface. func (f *Formatter) Format(entry *log.Entry) ([]byte, error) { + if entry.Level == log.ErrorLevel && f.errorStyles != nil { + return f.formatStyledError(entry), nil + } + buff := &bytes.Buffer{} parts := []string{formatPrefix(entry)} @@ -35,6 +93,15 @@ func (f *Formatter) Format(entry *log.Entry) ([]byte, error) { return buff.Bytes(), nil } +func (f *Formatter) formatStyledError(entry *log.Entry) []byte { + var buf bytes.Buffer + buf.WriteString(f.errorStyles.header.String()) + buf.WriteString("\n") + buf.WriteString(f.errorStyles.text.Render(capitalizeFirst(entry.Message) + ".")) + buf.WriteString("\n\n") + return buf.Bytes() +} + func formatPrefix(entry *log.Entry) string { if entry.Level == log.DebugLevel { return color.BlueString("lets:") @@ -65,3 +132,12 @@ func writeData(fields log.Fields) string { return strings.Join(buff, " ") } + +func capitalizeFirst(s string) string { + if s == "" { + return s + } + runes := []rune(s) + runes[0] = unicode.ToUpper(runes[0]) + return string(runes) +} diff --git a/internal/logging/log.go b/internal/logging/log.go index 31ce660e..7304db4d 100644 --- a/internal/logging/log.go +++ b/internal/logging/log.go @@ -5,14 +5,16 @@ import ( "io" "github.com/fatih/color" + "github.com/lets-cli/fang" log "github.com/sirupsen/logrus" ) -// Log is the main application logger. -// InitLogging for logrus. +// InitLogging configures the global logrus logger. Pass a non-nil cs to enable +// lipgloss-styled error output when errWriter is a terminal. func InitLogging( stdWriter io.Writer, errWriter io.Writer, + cs fang.ColorSchemeFunc, ) { log.SetOutput(io.Discard) @@ -36,7 +38,7 @@ func InitLogging( }, }) - log.SetFormatter(&Formatter{}) + log.SetFormatter(newFormatter(errWriter, cs)) } // ExecLogger is used in Executor. diff --git a/internal/logging/log_test.go b/internal/logging/log_test.go index 48824636..0ef85c2e 100644 --- a/internal/logging/log_test.go +++ b/internal/logging/log_test.go @@ -36,7 +36,7 @@ func TestLoggingToStd(t *testing.T) { setNoColorForTest(t, true) - InitLogging(&stdBuff, &errBuff) + InitLogging(&stdBuff, &errBuff, nil) log.Info(stdOutMsg) log.Error(stdErrMsg)