github.com/Racer159/jackal@v0.32.7-0.20240401174413-0bd2339e4f2e/src/pkg/message/message.go (about)

     1  // SPDX-License-Identifier: Apache-2.0
     2  // SPDX-FileCopyrightText: 2021-Present The Jackal Authors
     3  
     4  // Package message provides a rich set of functions for displaying messages to the user.
     5  package message
     6  
     7  import (
     8  	"encoding/json"
     9  	"fmt"
    10  	"io"
    11  	"net/http"
    12  	"os"
    13  	"runtime/debug"
    14  	"strings"
    15  	"time"
    16  
    17  	"github.com/Racer159/jackal/src/config"
    18  	"github.com/fatih/color"
    19  	"github.com/pterm/pterm"
    20  	"github.com/sergi/go-diff/diffmatchpatch"
    21  )
    22  
    23  // LogLevel is the level of logging to display.
    24  type LogLevel int
    25  
    26  const (
    27  	// WarnLevel level. Non-critical entries that deserve eyes.
    28  	WarnLevel LogLevel = iota
    29  	// InfoLevel level. General operational entries about what's going on inside the
    30  	// application.
    31  	InfoLevel
    32  	// DebugLevel level. Usually only enabled when debugging. Very verbose logging.
    33  	DebugLevel
    34  	// TraceLevel level. Designates finer-grained informational events than the Debug.
    35  	TraceLevel
    36  
    37  	// TermWidth sets the width of full width elements like progressbars and headers
    38  	TermWidth = 100
    39  )
    40  
    41  // NoProgress tracks whether spinner/progress bars show updates.
    42  var NoProgress bool
    43  
    44  // RuleLine creates a line of ━ as wide as the terminal
    45  var RuleLine = strings.Repeat("━", TermWidth)
    46  
    47  // logLevel holds the pterm compatible log level integer
    48  var logLevel = InfoLevel
    49  
    50  // logFile acts as a buffer for logFile generation
    51  var logFile *pausableLogFile
    52  
    53  // DebugWriter represents a writer interface that writes to message.Debug
    54  type DebugWriter struct{}
    55  
    56  func (d *DebugWriter) Write(raw []byte) (int, error) {
    57  	debugPrinter(2, string(raw))
    58  	return len(raw), nil
    59  }
    60  
    61  func init() {
    62  	pterm.ThemeDefault.SuccessMessageStyle = *pterm.NewStyle(pterm.FgLightGreen)
    63  	// Customize default error.
    64  	pterm.Success.Prefix = pterm.Prefix{
    65  		Text:  " ✔",
    66  		Style: pterm.NewStyle(pterm.FgLightGreen),
    67  	}
    68  	pterm.Error.Prefix = pterm.Prefix{
    69  		Text:  "    ERROR:",
    70  		Style: pterm.NewStyle(pterm.BgLightRed, pterm.FgBlack),
    71  	}
    72  	pterm.Info.Prefix = pterm.Prefix{
    73  		Text: " •",
    74  	}
    75  
    76  	pterm.SetDefaultOutput(os.Stderr)
    77  }
    78  
    79  // UseLogFile writes output to stderr and a logFile.
    80  func UseLogFile(dir string) (io.Writer, error) {
    81  	// Prepend the log filename with a timestamp.
    82  	ts := time.Now().Format("2006-01-02-15-04-05")
    83  
    84  	f, err := os.CreateTemp(dir, fmt.Sprintf("jackal-%s-*.log", ts))
    85  	if err != nil {
    86  		return nil, err
    87  	}
    88  
    89  	logFile = &pausableLogFile{
    90  		wr: f,
    91  		f:  f,
    92  	}
    93  
    94  	return logFile, nil
    95  }
    96  
    97  // LogFileLocation returns the location of the log file.
    98  func LogFileLocation() string {
    99  	if logFile == nil {
   100  		return ""
   101  	}
   102  	return logFile.f.Name()
   103  }
   104  
   105  // SetLogLevel sets the log level.
   106  func SetLogLevel(lvl LogLevel) {
   107  	logLevel = lvl
   108  	if logLevel >= DebugLevel {
   109  		pterm.EnableDebugMessages()
   110  	}
   111  }
   112  
   113  // GetLogLevel returns the current log level.
   114  func GetLogLevel() LogLevel {
   115  	return logLevel
   116  }
   117  
   118  // DisableColor disables color in output
   119  func DisableColor() {
   120  	pterm.DisableColor()
   121  }
   122  
   123  // JackalCommand prints a jackal terminal command.
   124  func JackalCommand(format string, a ...any) {
   125  	Command("jackal "+format, a...)
   126  }
   127  
   128  // Command prints a jackal terminal command.
   129  func Command(format string, a ...any) {
   130  	style := pterm.NewStyle(pterm.FgWhite, pterm.BgBlack)
   131  	style.Printfln("$ "+format, a...)
   132  }
   133  
   134  // Debug prints a debug message.
   135  func Debug(payload ...any) {
   136  	debugPrinter(2, payload...)
   137  }
   138  
   139  // Debugf prints a debug message with a given format.
   140  func Debugf(format string, a ...any) {
   141  	message := fmt.Sprintf(format, a...)
   142  	debugPrinter(2, message)
   143  }
   144  
   145  // ErrorWebf prints an error message and returns a web response.
   146  func ErrorWebf(err any, w http.ResponseWriter, format string, a ...any) {
   147  	debugPrinter(2, err)
   148  	message := fmt.Sprintf(format, a...)
   149  	Warn(message)
   150  	http.Error(w, message, http.StatusInternalServerError)
   151  }
   152  
   153  // Warn prints a warning message.
   154  func Warn(message string) {
   155  	Warnf("%s", message)
   156  }
   157  
   158  // Warnf prints a warning message with a given format.
   159  func Warnf(format string, a ...any) {
   160  	message := Paragraphn(TermWidth-10, format, a...)
   161  	pterm.Println()
   162  	pterm.Warning.Println(message)
   163  }
   164  
   165  // WarnErr prints an error message as a warning.
   166  func WarnErr(err any, message string) {
   167  	debugPrinter(2, err)
   168  	Warnf(message)
   169  }
   170  
   171  // WarnErrf prints an error message as a warning with a given format.
   172  func WarnErrf(err any, format string, a ...any) {
   173  	debugPrinter(2, err)
   174  	Warnf(format, a...)
   175  }
   176  
   177  // Fatal prints a fatal error message and exits with a 1.
   178  func Fatal(err any, message string) {
   179  	debugPrinter(2, err)
   180  	errorPrinter(2).Println(message)
   181  	debugPrinter(2, string(debug.Stack()))
   182  	os.Exit(1)
   183  }
   184  
   185  // Fatalf prints a fatal error message and exits with a 1 with a given format.
   186  func Fatalf(err any, format string, a ...any) {
   187  	message := Paragraph(format, a...)
   188  	Fatal(err, message)
   189  }
   190  
   191  // Info prints an info message.
   192  func Info(message string) {
   193  	Infof("%s", message)
   194  }
   195  
   196  // Infof prints an info message with a given format.
   197  func Infof(format string, a ...any) {
   198  	if logLevel > 0 {
   199  		message := Paragraph(format, a...)
   200  		pterm.Info.Println(message)
   201  	}
   202  }
   203  
   204  // Success prints a success message.
   205  func Success(message string) {
   206  	Successf("%s", message)
   207  }
   208  
   209  // Successf prints a success message with a given format.
   210  func Successf(format string, a ...any) {
   211  	message := Paragraph(format, a...)
   212  	pterm.Success.Println(message)
   213  }
   214  
   215  // Question prints a user prompt description message.
   216  func Question(text string) {
   217  	Questionf("%s", text)
   218  }
   219  
   220  // Questionf prints a user prompt description message with a given format.
   221  func Questionf(format string, a ...any) {
   222  	message := Paragraph(format, a...)
   223  	pterm.Println()
   224  	pterm.FgLightGreen.Println(message)
   225  }
   226  
   227  // Note prints a note message.
   228  func Note(text string) {
   229  	Notef("%s", text)
   230  }
   231  
   232  // Notef prints a note message  with a given format.
   233  func Notef(format string, a ...any) {
   234  	message := Paragraphn(TermWidth-7, format, a...)
   235  	notePrefix := pterm.PrefixPrinter{
   236  		MessageStyle: &pterm.ThemeDefault.InfoMessageStyle,
   237  		Prefix: pterm.Prefix{
   238  			Style: &pterm.ThemeDefault.InfoPrefixStyle,
   239  			Text:  "NOTE",
   240  		},
   241  	}
   242  	pterm.Println()
   243  	notePrefix.Println(message)
   244  }
   245  
   246  // Title prints a title and an optional help description for that section
   247  func Title(title string, help string) {
   248  	titleFormatted := pterm.FgBlack.Sprint(pterm.BgWhite.Sprintf(" %s ", title))
   249  	helpFormatted := pterm.FgGray.Sprint(help)
   250  	pterm.Printfln("%s  %s", titleFormatted, helpFormatted)
   251  }
   252  
   253  // HeaderInfof prints a large header with a formatted message.
   254  func HeaderInfof(format string, a ...any) {
   255  	pterm.Println()
   256  	message := Truncate(fmt.Sprintf(format, a...), TermWidth, false)
   257  	// Ensure the text is consistent for the header width
   258  	padding := TermWidth - len(message)
   259  	pterm.DefaultHeader.
   260  		WithBackgroundStyle(pterm.NewStyle(pterm.BgDarkGray)).
   261  		WithTextStyle(pterm.NewStyle(pterm.FgLightWhite)).
   262  		WithMargin(2).
   263  		Printfln(message + strings.Repeat(" ", padding))
   264  }
   265  
   266  // HorizontalRule prints a white horizontal rule to separate the terminal
   267  func HorizontalRule() {
   268  	pterm.Println()
   269  	pterm.Println(RuleLine)
   270  }
   271  
   272  // JSONValue prints any value as JSON.
   273  func JSONValue(value any) string {
   274  	bytes, err := json.MarshalIndent(value, "", "  ")
   275  	if err != nil {
   276  		debugPrinter(2, fmt.Sprintf("ERROR marshalling json: %s", err.Error()))
   277  	}
   278  	return string(bytes)
   279  }
   280  
   281  // Paragraph formats text into a paragraph matching the TermWidth
   282  func Paragraph(format string, a ...any) string {
   283  	return Paragraphn(TermWidth, format, a...)
   284  }
   285  
   286  // Paragraphn formats text into an n column paragraph
   287  func Paragraphn(n int, format string, a ...any) string {
   288  	return pterm.DefaultParagraph.WithMaxWidth(n).Sprintf(format, a...)
   289  }
   290  
   291  // PrintDiff prints the differences between a and b with a as original and b as new
   292  func PrintDiff(textA, textB string) {
   293  	dmp := diffmatchpatch.New()
   294  
   295  	diffs := dmp.DiffMain(textA, textB, true)
   296  
   297  	diffs = dmp.DiffCleanupSemantic(diffs)
   298  
   299  	pterm.Println(dmp.DiffPrettyText(diffs))
   300  }
   301  
   302  // Truncate truncates provided text to the requested length
   303  func Truncate(text string, length int, invert bool) string {
   304  	// Remove newlines and replace with semicolons
   305  	textEscaped := strings.ReplaceAll(text, "\n", "; ")
   306  	// Truncate the text if it is longer than length so it isn't too long.
   307  	if len(textEscaped) > length {
   308  		if invert {
   309  			start := len(textEscaped) - length + 3
   310  			textEscaped = "..." + textEscaped[start:]
   311  		} else {
   312  			end := length - 3
   313  			textEscaped = textEscaped[:end] + "..."
   314  		}
   315  	}
   316  	return textEscaped
   317  }
   318  
   319  // Table prints a padded table containing the specified header and data
   320  func Table(header []string, data [][]string) {
   321  	pterm.Println()
   322  
   323  	// To avoid side effects make copies of the header and data before adding padding
   324  	headerCopy := make([]string, len(header))
   325  	copy(headerCopy, header)
   326  	dataCopy := make([][]string, len(data))
   327  	copy(dataCopy, data)
   328  	if len(headerCopy) > 0 {
   329  		headerCopy[0] = fmt.Sprintf("     %s", headerCopy[0])
   330  	}
   331  
   332  	table := pterm.TableData{
   333  		headerCopy,
   334  	}
   335  
   336  	for _, row := range dataCopy {
   337  		if len(row) > 0 {
   338  			row[0] = fmt.Sprintf("     %s", row[0])
   339  		}
   340  		table = append(table, pterm.TableData{row}...)
   341  	}
   342  
   343  	pterm.DefaultTable.WithHasHeader().WithData(table).Render()
   344  }
   345  
   346  // ColorWrap changes a string to an ansi color code and appends the default color to the end
   347  // preventing future characters from taking on the given color
   348  // returns string as normal if color is disabled
   349  func ColorWrap(str string, attr color.Attribute) string {
   350  	if config.NoColor {
   351  		return str
   352  	}
   353  	return fmt.Sprintf("\x1b[%dm%s\x1b[0m", attr, str)
   354  }
   355  
   356  func debugPrinter(offset int, a ...any) {
   357  	printer := pterm.Debug.WithShowLineNumber(logLevel > 2).WithLineNumberOffset(offset)
   358  	now := time.Now().Format(time.RFC3339)
   359  	// prepend to a
   360  	a = append([]any{now, " - "}, a...)
   361  
   362  	printer.Println(a...)
   363  
   364  	// Always write to the log file
   365  	if logFile != nil {
   366  		pterm.Debug.
   367  			WithShowLineNumber(true).
   368  			WithLineNumberOffset(offset).
   369  			WithDebugger(false).
   370  			WithWriter(logFile).
   371  			Println(a...)
   372  	}
   373  }
   374  
   375  func errorPrinter(offset int) *pterm.PrefixPrinter {
   376  	return pterm.Error.WithShowLineNumber(logLevel > 2).WithLineNumberOffset(offset)
   377  }