From e3f2fc8913b592aa1ecadd7f9b50a0ab621aa1a3 Mon Sep 17 00:00:00 2001 From: Stanislav Seletskiy Date: Fri, 24 Jun 2016 01:44:55 +0600 Subject: [PATCH] coloooors --- command.go | 5 + debug.go | 94 ++++++- distributed_lock_node.go | 9 +- format.go | 236 ++++++++++++++++-- json_output_writer.go | 43 ++++ lock.go | 10 +- main.go | 100 +++++--- remote_execution.go | 4 + remote_execution_runner.go | 6 +- status_bar.go | 120 +++++++++ status_bar_update_writer.go | 23 ++ ...ld-properly-escape-shell-arguments.test.sh | 8 + themes.go | 69 +++++ 13 files changed, 659 insertions(+), 68 deletions(-) create mode 100644 json_output_writer.go create mode 100644 status_bar.go create mode 100644 status_bar_update_writer.go create mode 100644 tests/testcases/commands/should-properly-escape-shell-arguments.test.sh create mode 100644 themes.go diff --git a/command.go b/command.go index 9f561fb..f67ab40 100644 --- a/command.go +++ b/command.go @@ -48,6 +48,8 @@ func runRemoteExecution( outputLock = nil } + status.SetOutputLock(logLock) + errors := make(chan error, 0) for _, node := range lockedNodes.nodes { go func(node *distributedLockNode) { @@ -198,6 +200,9 @@ func runRemoteExecutionNode( ) } + stdout = &statusBarUpdateWriter{stdout} + stderr = &statusBarUpdateWriter{stderr} + if outputLock != (*sync.Mutex)(nil) { sharedLock := newSharedLock(outputLock, 2) diff --git a/debug.go b/debug.go index a3e1de4..1b88425 100644 --- a/debug.go +++ b/debug.go @@ -2,11 +2,59 @@ package main import ( "fmt" + "os" "strings" + "github.com/fatih/color" + + "github.com/kovetskiy/lorg" "github.com/seletskiy/hierr" ) +func setupLogger(format *lorg.Format) { + logger.SetFormat(format) +} + +func setLoggerOutputFormat(format outputFormat, logger *lorg.Log) { + if format == outputFormatJSON { + logger.SetOutput(&jsonOutputWriter{ + stream: `stderr`, + node: ``, + output: os.Stderr, + }) + } +} + +func setLoggerVerbosity(level verbosity, logger *lorg.Log) { + logger.SetLevel(lorg.LevelWarning) + + switch { + case level >= verbosityTrace: + logger.SetLevel(lorg.LevelTrace) + + case level >= verbosityDebug: + logger.SetLevel(lorg.LevelDebug) + + case level >= verbosityNormal: + logger.SetLevel(lorg.LevelInfo) + } +} + +func colorize( + attributes ...color.Attribute, +) string { + if !isColorEnabled { + return "" + } + + sequence := []string{} + for _, attribute := range attributes { + sequence = append(sequence, fmt.Sprint(attribute)) + } + + return fmt.Sprintf("\x1b[%sm", strings.Join(sequence, ";")) +} + func tracef(format string, args ...interface{}) { if verbose < verbosityTrace { return @@ -14,19 +62,25 @@ func tracef(format string, args ...interface{}) { args = serializeErrors(args) - logger.Debugf(format, args...) + logger.Tracef(format, args...) + + drawStatus() } func debugf(format string, args ...interface{}) { args = serializeErrors(args) logger.Debugf(format, args...) + + drawStatus() } func infof(format string, args ...interface{}) { args = serializeErrors(args) logger.Infof(format, args...) + + drawStatus() } func warningf(format string, args ...interface{}) { @@ -37,6 +91,8 @@ func warningf(format string, args ...interface{}) { } logger.Warningf(format, args...) + + drawStatus() } func errorf(format string, args ...interface{}) { @@ -55,6 +111,42 @@ func serializeErrors(args []interface{}) []interface{} { return args } +func shouldDrawStatus() bool { + if !isOutputOnTTY { + return false + } + + if format != outputFormatText { + return false + } + + if verbose <= verbosityQuiet { + return false + } + + if status == nil { + return false + } + + return true +} + +func drawStatus() { + if !shouldDrawStatus() { + return + } + + status.Draw(os.Stderr) +} + +func clearStatus() { + if !shouldDrawStatus() { + return + } + + status.Clear(os.Stderr) +} + func serializeError(err error) string { if format == outputFormatText { return fmt.Sprint(err) diff --git a/distributed_lock_node.go b/distributed_lock_node.go index c76a862..8b84b66 100644 --- a/distributed_lock_node.go +++ b/distributed_lock_node.go @@ -38,10 +38,11 @@ func (node *distributedLockNode) lock( filename string, ) error { lockCommandString := fmt.Sprintf( - `sh -c $'`+ - `flock -nx %s -c \'`+ - `printf "%s\\n" && cat\' || `+ - `printf "%s\\n"'`, + `sh -c "`+ + `flock -nx %s -c '`+ + `printf \"%s\\n\" && cat' || `+ + `printf \"%s\\n\"`+ + `"`, filename, lockAcquiredString, lockLockedString, diff --git a/format.go b/format.go index f8b5917..926be9f 100644 --- a/format.go +++ b/format.go @@ -1,8 +1,17 @@ package main import ( - "encoding/json" - "io" + "fmt" + "os" + "regexp" + "strings" + "text/template" + + "golang.org/x/crypto/ssh/terminal" + + "github.com/kovetskiy/lorg" + "github.com/seletskiy/hierr" + "github.com/seletskiy/tplutil" ) type ( @@ -14,47 +23,224 @@ const ( outputFormatJSON ) -func parseOutputFormat(args map[string]interface{}) outputFormat { - if args["--json"].(bool) { - return outputFormatJSON +const ( + formatEscape = "\x1b" + + formatAttrForeground = "3" + formatAttrBackground = "4" + formatAttrDefault = "9" + + formatAttrReset = "0" + formatAttrReverse = "7" + formatAttrNoReverse = "27" + formatAttrBold = "1" + formatAttrNoBold = "22" + + formatAttrForeground256 = "38;5" + formatAttrBackground256 = "48;5" + + formatResetBlock = "{reset}" +) + +var ( + formatCodeRegexp = regexp.MustCompile(formatEscape + `[^m]+`) +) + +func getResetFormatSequence() string { + return getFormatSequence(formatAttrReset) +} + +func getBackgroundFormatSequence(color int) string { + if color == 0 { + return getFormatSequence( + formatAttrBackground + formatAttrDefault, + ) + } + + return getFormatSequence(formatAttrBackground256, fmt.Sprint(color)) +} + +func getForegroundFormatSequence(color int) string { + if color == 0 { + return getFormatSequence( + formatAttrForeground + formatAttrDefault, + ) + } + + return getFormatSequence(formatAttrForeground256, fmt.Sprint(color)) +} + +func getBoldFormatSequence() string { + return getFormatSequence(formatAttrBold) +} + +func getNoBoldFormatSequence() string { + return getFormatSequence(formatAttrNoBold) +} + +func getReverseFormatSequence() string { + return getFormatSequence(formatAttrReverse) +} + +func getNoReverseFormatSequence() string { + return getFormatSequence(formatAttrNoReverse) +} + +func getFormatSequence(attr ...string) string { + if !isColorEnabled { + return "" + } + + return fmt.Sprintf("%s[%sm", formatEscape, strings.Join(attr, `;`)) +} + +func getLogPlaceholder(placeholder string) string { + return fmt.Sprintf("${%s}", placeholder) +} + +func getLogLevelFormatPlaceholder( + level string, + formatString string, +) (string, error) { + format, err := compileFormat(formatString) + if err != nil { + return "", hierr.Errorf( + err, + `can't compile specified format string: '%s'`, + formatString, + ) } - return outputFormatText + formatCode, err := tplutil.ExecuteToString(format, nil) + if err != nil { + return "", hierr.Errorf( + err, + `can't execute specified format string: '%s'`, + formatString, + ) + } + + return fmt.Sprintf( + "${color:%s:%s}", + level, + strings.Replace(formatCode, ":", "\\:", -1), + ), nil } -type jsonOutputWriter struct { - stream string - node string +func executeLogColorPlaceholder(level lorg.Level, value string) string { + var ( + parts = strings.SplitN(value, ":", 2) + targetLevel = parts[0] + format = "" + ) + + if len(parts) > 1 { + format = parts[1] + } - output io.Writer + if targetLevel == strings.ToLower(level.String()) { + return format + } + + return "" +} + +func compileFormat(format string) (*template.Template, error) { + functions := map[string]interface{}{ + "bg": getBackgroundFormatSequence, + "fg": getForegroundFormatSequence, + "bold": getBoldFormatSequence, + "nobold": getNoBoldFormatSequence, + "reverse": getReverseFormatSequence, + "noreverse": getNoReverseFormatSequence, + "reset": getResetFormatSequence, + + "log": getLogPlaceholder, + "level": getLogLevelFormatPlaceholder, + } + + return template.New("format").Delims("{", "}").Funcs(functions).Parse( + format, + ) } -func (writer *jsonOutputWriter) Write(data []byte) (int, error) { - if len(data) == 0 { - return 0, nil +func parseOutputFormat( + args map[string]interface{}, +) (outputFormat, bool, bool) { + + format := outputFormatText + if args["--json"].(bool) { + format = outputFormatJSON + } + + isOutputOnTTY := terminal.IsTerminal(int(os.Stderr.Fd())) + + isColorEnabled := isOutputOnTTY + + if format != outputFormatText { + isColorEnabled = false } - message := map[string]interface{}{ - "stream": writer.stream, + if args["--no-colors"].(bool) { + isColorEnabled = false } - if writer.node == "" { - message["node"] = nil - } else { - message["node"] = writer.node + return format, isOutputOnTTY, isColorEnabled +} + +func trimFormatCodes(input string) string { + return formatCodeRegexp.ReplaceAllLiteralString(input, ``) +} + +func parseStatusBarFormat( + args map[string]interface{}, +) (*template.Template, error) { + var ( + theme = args["--status-format"].(string) + ) + + statusFormat := getStatusBarTheme(theme) + formatResetBlock + + format, err := compileFormat(statusFormat) + if err != nil { + return nil, hierr.Errorf( + err, + `can't compile status bar format template`, + ) } - message["body"] = string(data) + tracef("using status bar format: '%s'", statusFormat) + + return format, nil +} + +func parseLogFormat(args map[string]interface{}) (*lorg.Format, error) { + var ( + theme = args["--log-format"].(string) + ) + + logFormat := getLogTheme(theme) + formatResetBlock - jsonMessage, err := json.Marshal(message) + format, err := compileFormat(logFormat) if err != nil { - return 0, err + return nil, hierr.Errorf( + err, + `can't compile log format template`, + ) } - _, err = writer.output.Write(append(jsonMessage, '\n')) + formatString, err := tplutil.ExecuteToString(format, nil) if err != nil { - return 0, err + return nil, hierr.Errorf( + err, + `can't execute log format template`, + ) } - return len(data), nil + tracef("using log format: '%#s'", logFormat) + + lorgFormat := lorg.NewFormat(formatString) + lorgFormat.SetPlaceholder("color", executeLogColorPlaceholder) + + return lorgFormat, nil } diff --git a/json_output_writer.go b/json_output_writer.go new file mode 100644 index 0000000..4520422 --- /dev/null +++ b/json_output_writer.go @@ -0,0 +1,43 @@ +package main + +import ( + "encoding/json" + "io" +) + +type jsonOutputWriter struct { + stream string + node string + + output io.Writer +} + +func (writer *jsonOutputWriter) Write(data []byte) (int, error) { + if len(data) == 0 { + return 0, nil + } + + message := map[string]interface{}{ + "stream": writer.stream, + } + + if writer.node == "" { + message["node"] = nil + } else { + message["node"] = writer.node + } + + message["body"] = string(data) + + jsonMessage, err := json.Marshal(message) + if err != nil { + return 0, err + } + + _, err = writer.output.Write(append(jsonMessage, '\n')) + if err != nil { + return 0, err + } + + return len(data), nil +} diff --git a/lock.go b/lock.go index a4295fd..890b1c3 100644 --- a/lock.go +++ b/lock.go @@ -64,10 +64,14 @@ func acquireDistributedLock( } } - status := "established" + textStatus := "established" if failed { - status = "failed" + status.IncFailures() + + textStatus = "failed" } else { + status.IncSuccess() + atomic.AddInt64(&connections, 1) nodeAddMutex.Lock() @@ -81,7 +85,7 @@ func acquireDistributedLock( connections, int64(len(addresses))-failures, failures, - status, + textStatus, nodeAddress, ) diff --git a/main.go b/main.go index 447ef97..018d8ca 100644 --- a/main.go +++ b/main.go @@ -107,7 +107,7 @@ Options: [default: $HOME/.ssh/id_rsa] -p --password Enable password authentication. Exclude '-k' option. - TTY is required for reading password. + Interactive TTY is required for reading password. -x --sudo Obtain root via 'sudo -n'. By default, orgalorg will not obtain root and do all actions from specified user. To change that @@ -157,17 +157,49 @@ Advanced options: shell wrapper will be used. If any args are given using '-g', they will be appended to shell invocation. - [default: bash -c $'{}'] + [default: bash -c '{}'] -d --threads Set threads count which will be used for connection, locking and execution commands. [default: 16]. + --no-preserve-uid Do not preserve UIDs for transferred files. + --no-preserve-gid Do not preserve GIDs for transferred files. + +Output format and colors options: --json Output everything in line-by-line JSON format, printing objects with fields: * 'stream' = 'stdout' | 'stderr'; * 'node' = | null (if internal output); * 'body' = - --no-preserve-uid Do not preserve UIDs for transferred files. - --no-preserve-gid Do not preserve GIDs for transferred files. + --status-format Format for the status bar. + Full Go template syntax is available with delims + of '{' and '}'. Variables available: + * .Phase - either: + * '` + statusBarPhaseConnecting + `'; + * '` + statusBarPhaseExecuting + `'; + * .Total - total amount of connected nodes; + * .Success - number of nodes that successfully done + phase; + * .Failures - number of failed nodes; + Additional functions are available: + * bg - set background color to specified; + * 0 can be used for default bg; + * fg - set foreground color to specified; + * 0 can be used for default fg; + * bold/nobold - set or reset bold mode; + * reverse/noreverse - set or reset reverse mode; + * reset - completely resets mode; + For example, run orgalorg with '-vv' flag. + Two embedded themes are available by their names: + ` + themeDark + ` and ` + themeLight + ` + [default: ` + themeDark + `] + --log-format Format for the logs. + Same, as above, with additional functions: + * log - inserts lorg placeholder + specification; + * level - insert in place specified + format if log level matches specified level. + [default: ` + themeDark + `] + --no-colors Do not use colors. Timeout options: --conn-timeout Remote host connection timeout in milliseconds. @@ -201,7 +233,11 @@ var ( verbose = verbosityNormal format = outputFormatText - pool *threadPool + pool *threadPool + status *statusBar + + isOutputOnTTY = false + isColorEnabled = false ) var ( @@ -209,11 +245,9 @@ var ( ) func main() { - logger.SetFormat(lorg.NewFormat("* ${time} ${level:[%s]:right:true} %s")) - - usage, err := formatUsage(usage) + usage, err := formatUsage(string(usage)) if err != nil { - logger.Error(hierr.Errorf( + errorf("%s", hierr.Errorf( err, `can't format usage`, )) @@ -230,7 +264,16 @@ func main() { setLoggerVerbosity(verbose, logger) - format = parseOutputFormat(args) + format, isOutputOnTTY, isColorEnabled = parseOutputFormat(args) + + logFormat, err := parseLogFormat(args) + if err != nil { + errorf("%s", err) + + exit(1) + } + + setupLogger(logFormat) setLoggerOutputFormat(format, logger) @@ -244,6 +287,10 @@ func main() { pool = newThreadPool(poolSize) + statusFormat, err := parseStatusBarFormat(args) + + status = newStatusBar(statusFormat) + switch { case args["--upload"].(bool): fallthrough @@ -262,27 +309,6 @@ func main() { } } -func setLoggerOutputFormat(format outputFormat, logger *lorg.Log) { - if format == outputFormatJSON { - logger.SetOutput(&jsonOutputWriter{ - stream: `stderr`, - node: ``, - output: os.Stderr, - }) - } -} - -func setLoggerVerbosity(level verbosity, logger *lorg.Log) { - logger.SetLevel(lorg.LevelWarning) - - switch { - case level >= verbosityDebug: - logger.SetLevel(lorg.LevelDebug) - case level >= verbosityNormal: - logger.SetLevel(lorg.LevelInfo) - } -} - func formatUsage(template string) (string, error) { currentUser, err := user.Current() if err != nil { @@ -580,6 +606,8 @@ func connectAndLock( noLockFail = args["--no-lock-fail"].(bool) ) + status.SetPhase(statusBarPhaseConnecting) + addresses, err := parseAddresses(hosts, defaultUser, fromStdin) if err != nil { return nil, hierr.Errorf( @@ -588,6 +616,8 @@ func connectAndLock( ) } + status.SetTotal(len(addresses)) + timeouts, err := makeTimeouts(args) if err != nil { return nil, hierr.Errorf( @@ -746,7 +776,9 @@ func generateRunID() string { } func readPassword(prompt string) (string, error) { - fmt.Fprintf(os.Stderr, sshPasswordPrompt) + if isOutputOnTTY { + fmt.Fprintf(os.Stderr, sshPasswordPrompt) + } tty, err := os.Open("/dev/tty") if err != nil { @@ -765,7 +797,9 @@ func readPassword(prompt string) (string, error) { ) } - fmt.Fprintln(os.Stderr) + if isOutputOnTTY { + fmt.Fprintln(os.Stderr) + } return string(password), nil } diff --git a/remote_execution.go b/remote_execution.go index d7b7a8a..3f40f77 100644 --- a/remote_execution.go +++ b/remote_execution.go @@ -41,6 +41,8 @@ func (execution *remoteExecution) wait() error { for range execution.nodes { result := <-results if result.err != nil { + status.IncFailures() + exitCodes[result.node.exitCode]++ executionErrors = hierr.Push( @@ -57,6 +59,8 @@ func (execution *remoteExecution) wait() error { continue } + status.IncSuccess() + tracef( `%s has successfully finished execution`, result.node.node.String(), diff --git a/remote_execution_runner.go b/remote_execution_runner.go index fac8f1f..1e2dfe1 100644 --- a/remote_execution_runner.go +++ b/remote_execution_runner.go @@ -22,6 +22,8 @@ func (runner *remoteExecutionRunner) run( cluster *distributedLock, setupCallback func(*remoteExecutionNode), ) (*remoteExecution, error) { + status.SetPhase(statusBarPhaseExecuting) + command := joinCommand(runner.command) if runner.directory != "" { @@ -72,8 +74,7 @@ func joinCommand(command []string) string { } func escapeCommandArgument(argument string) string { - argument = strings.Replace(argument, `\`, `\\`, -1) - argument = strings.Replace(argument, ` `, `\ `, -1) + argument = strings.Replace(argument, `'`, `'\''`, -1) return argument } @@ -82,6 +83,7 @@ func escapeCommandArgumentStrict(argument string) string { argument = strings.Replace(argument, `\`, `\\`, -1) argument = strings.Replace(argument, "`", "\\`", -1) argument = strings.Replace(argument, `"`, `\"`, -1) + argument = strings.Replace(argument, `'`, `'\''`, -1) argument = strings.Replace(argument, `$`, `\$`, -1) return `"` + argument + `"` diff --git a/status_bar.go b/status_bar.go new file mode 100644 index 0000000..46a7316 --- /dev/null +++ b/status_bar.go @@ -0,0 +1,120 @@ +package main + +import ( + "bytes" + "fmt" + "io" + "strings" + "sync" + "text/template" + + "github.com/seletskiy/hierr" +) + +type ( + statusBarPhase string +) + +const ( + statusBarPhaseConnecting statusBarPhase = "CONNECTING" + statusBarPhaseExecuting = "EVALUATING" +) + +type statusBar struct { + sync.Mutex + + Phase statusBarPhase + Total int + Failures int + Success int + + format *template.Template + + last string + + lock sync.Locker +} + +func newStatusBar(format *template.Template) *statusBar { + return &statusBar{ + format: format, + } +} + +func (bar *statusBar) SetPhase(phase statusBarPhase) { + bar.Lock() + defer bar.Unlock() + + bar.Phase = phase + + bar.Success = 0 +} + +func (bar *statusBar) SetTotal(total int) { + bar.Lock() + defer bar.Unlock() + + bar.Total = total +} + +func (bar *statusBar) IncSuccess() { + bar.Lock() + defer bar.Unlock() + + bar.Success++ +} + +func (bar *statusBar) IncFailures() { + bar.Lock() + defer bar.Unlock() + + bar.Failures++ +} + +func (bar *statusBar) SetOutputLock(lock sync.Locker) { + bar.lock = lock +} + +func (bar *statusBar) Clear(writer io.Writer) { + bar.Lock() + defer bar.Unlock() + + if bar.lock != nil { + bar.lock.Lock() + defer bar.lock.Unlock() + } + + fmt.Fprint(writer, strings.Repeat(" ", len(bar.last))+"\r") + + bar.last = "" +} + +func (bar *statusBar) Draw(writer io.Writer) { + bar.Lock() + defer bar.Unlock() + + if bar.lock != nil { + bar.lock.Lock() + defer bar.lock.Unlock() + } + + buffer := &bytes.Buffer{} + + if bar.Phase == "" { + return + } + + err := bar.format.Execute(buffer, bar) + if err != nil { + errorf("%s", hierr.Errorf( + err, + `error during rendering status bar`, + )) + } + + fmt.Fprintf(buffer, "\r") + + bar.last = trimFormatCodes(buffer.String()) + + io.Copy(writer, buffer) +} diff --git a/status_bar_update_writer.go b/status_bar_update_writer.go new file mode 100644 index 0000000..6513e4e --- /dev/null +++ b/status_bar_update_writer.go @@ -0,0 +1,23 @@ +package main + +import ( + "io" +) + +type statusBarUpdateWriter struct { + writer io.WriteCloser +} + +func (writer *statusBarUpdateWriter) Write(data []byte) (int, error) { + clearStatus() + + written, err := writer.writer.Write(data) + + drawStatus() + + return written, err +} + +func (writer *statusBarUpdateWriter) Close() error { + return writer.writer.Close() +} diff --git a/tests/testcases/commands/should-properly-escape-shell-arguments.test.sh b/tests/testcases/commands/should-properly-escape-shell-arguments.test.sh new file mode 100644 index 0000000..03f7e13 --- /dev/null +++ b/tests/testcases/commands/should-properly-escape-shell-arguments.test.sh @@ -0,0 +1,8 @@ +tests:ensure echo "'1'" +tests:ensure :orgalorg:with-key -e -C echo "'1'" + +tests:assert-stdout-re "1$" + +tests:ensure :orgalorg:with-key -e -C echo "\\'" + +tests:assert-stdout-re "'$" diff --git a/themes.go b/themes.go new file mode 100644 index 0000000..d957a7a --- /dev/null +++ b/themes.go @@ -0,0 +1,69 @@ +package main + +import ( + "fmt" +) + +const ( + themeDark = `dark` + themeLight = `light` +) + +var ( + statusBarThemeTemplate = `{" "}{bg %d}{fg %d}` + + `{if eq .Phase "` + statusBarPhaseExecuting + `"}` + + `{bg %d}` + + `{end}` + + ` {bold}{.Phase}{nobold} ` + + `{fg %d}{reverse}{noreverse}{fg %d}{bg %d} ` + + `{fg %d}{printf "%%4d" .Success}{fg %d}/{printf "%%4d" .Total} ` + + `{if .Failures}{fg %d}(failed: {.Failures}){end}{" "}` + + statusBarThemes = map[string]string{ + themeDark: fmt.Sprintf( + statusBarThemeTemplate, + 99, 7, 64, 237, 15, 237, 28, 15, 9, + ), + + themeLight: fmt.Sprintf( + statusBarThemeTemplate, + 99, 7, 64, 254, 16, 254, 106, 16, 9, + ), + } + + logThemeTemplate = `{level "error" "{fg %d}"}` + + `{level "warning" "{fg %d}{bg %d}"}` + + `{level "debug" "{fg %d}"}` + + `{level "trace" "{fg %d}"}` + + `* {log "time"} ` + + `{level "error" "{bg %d}{bold}"}{log "level:[%%s]:right:true"}` + + `{level "error" "{bg %d}"}{nobold} %%s` + + logThemes = map[string]string{ + themeDark: fmt.Sprintf( + logThemeTemplate, + 1, 11, 0, 250, 243, 52, 0, + ), + + themeLight: fmt.Sprintf( + logThemeTemplate, + 199, 172, 230, 240, 248, 220, 0, + ), + } +) + +func getStatusBarTheme(theme string) string { + if format, ok := statusBarThemes[theme]; ok { + return format + } + + return theme +} + +func getLogTheme(theme string) string { + if format, ok := logThemes[theme]; ok { + return format + } + + return theme +}