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 }