Skip to content
Merged
Show file tree
Hide file tree
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
2 changes: 1 addition & 1 deletion internal/cli/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
78 changes: 77 additions & 1 deletion internal/logging/formatter.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand All @@ -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())

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

issue: Handle very narrow terminal widths to avoid negative or zero lipgloss width.

For very small widths (e.g. w == 1), Width(w - 2) may be 0 or negative, which can cause broken or undefined lipgloss rendering. Clamp w to a sane minimum (e.g. w = max(20, min(w, 160))) or otherwise ensure the value passed to Width is always >= 1.

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)}

Expand All @@ -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")
Comment on lines +96 to +101

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

issue (bug_risk): Styled error formatting drops all structured fields attached to the entry.

The non-styled formatter includes entry.Data via writeData, but this styled error formatter omits it and only prints the message. That means any WithFields metadata is lost when styling is enabled. Please also render entry.Data (e.g., in a secondary style) so structured context is preserved.

return buf.Bytes()
}

func formatPrefix(entry *log.Entry) string {
if entry.Level == log.DebugLevel {
return color.BlueString("lets:")
Expand Down Expand Up @@ -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)
}
8 changes: 5 additions & 3 deletions internal/logging/log.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -36,7 +38,7 @@ func InitLogging(
},
})

log.SetFormatter(&Formatter{})
log.SetFormatter(newFormatter(errWriter, cs))
}

// ExecLogger is used in Executor.
Expand Down
2 changes: 1 addition & 1 deletion internal/logging/log_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading