Repository: JanDeDobbeleer/oh-my-posh
Pull request #1155 (2021-11-02 13:55:22)

feat: non-colored prompt
### Prerequisites

- [x] I have read and understood the `CONTRIBUTING` guide
- [x] The commit message follows the [conventional commits][cc] guidelines
- [x] Tests for the changes have been added (for bug fixes/features)
- [x] Docs have been added / updated (for bug fixes/features)

### Description

Adds a `--plain` flag that will write a prompt without ANSI characters.
Useful to leverage in situations where the shell/environment does not have ANSI capabilities.

<!---

Tips:

If you're not comfortable with working with Git, we're working a guide (https://ohmyposh.dev/docs/contributing_git) to help you out.
Oh My Posh advises GitKraken (https://www.gitkraken.com/invite/nQmDPR9D) as your preferred cross platform Git GUI power tool.

-->

[cc]: https://www.conventionalcommits.org/en/v1.0.0/#summary


lnu: A bit odd to use omp on a non ansi console... but great example of how features can be added quite "easily" to omp :)

Patch:
diff --git a/.vscode/settings.json b/.vscode/settings.json
index a6655675..c0616b25 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -11,5 +11,9 @@
     "editor.codeActionsOnSave": {
       "source.organizeImports": true
     }
-  }
+  },
+  "go.formatTool": "gofmt",
+  "go.formatFlags": [
+    "-s"
+  ]
 }
diff --git a/src/block.go b/src/block.go
index 20f4d1b5..dcbabc52 100644
--- a/src/block.go
+++ b/src/block.go
@@ -34,7 +34,7 @@ type Block struct {
 	Newline          bool           `config:"newline"`
 
 	env                   environmentInfo
-	writer                colorWriter
+	writer                promptWriter
 	ansi                  *ansiUtils
 	activeSegment         *Segment
 	previousActiveSegment *Segment
@@ -42,7 +42,7 @@ type Block struct {
 	activeForeground      string
 }
 
-func (b *Block) init(env environmentInfo, writer colorWriter, ansi *ansiUtils) {
+func (b *Block) init(env environmentInfo, writer promptWriter, ansi *ansiUtils) {
 	b.env = env
 	b.writer = writer
 	b.ansi = ansi
@@ -51,7 +51,7 @@ func (b *Block) init(env environmentInfo, writer colorWriter, ansi *ansiUtils) {
 func (b *Block) initPlain(env environmentInfo, config *Config) {
 	b.ansi = &ansiUtils{}
 	b.ansi.init(plain)
-	b.writer = &AnsiColor{
+	b.writer = &AnsiWriter{
 		ansi:               b.ansi,
 		terminalBackground: getConsoleBackgroundColor(env, config.TerminalBackground),
 	}
diff --git a/src/engine.go b/src/engine.go
index 53d16edc..2761e0ce 100644
--- a/src/engine.go
+++ b/src/engine.go
@@ -9,9 +9,10 @@ import (
 type engine struct {
 	config       *Config
 	env          environmentInfo
-	colorWriter  colorWriter
+	writer       promptWriter
 	ansi         *ansiUtils
 	consoleTitle *consoleTitle
+	plain        bool
 
 	console strings.Builder
 	rprompt string
@@ -21,6 +22,13 @@ func (e *engine) write(text string) {
 	e.console.WriteString(text)
 }
 
+func (e *engine) writeANSI(text string) {
+	if e.plain {
+		return
+	}
+	e.console.WriteString(text)
+}
+
 func (e *engine) string() string {
 	return e.console.String()
 }
@@ -48,9 +56,9 @@ func (e *engine) render() string {
 		e.renderBlock(block)
 	}
 	if e.config.ConsoleTitle {
-		e.write(e.consoleTitle.getConsoleTitle())
+		e.writeANSI(e.consoleTitle.getConsoleTitle())
 	}
-	e.write(e.ansi.creset)
+	e.writeANSI(e.ansi.creset)
 	if e.config.FinalSpace {
 		e.write(" ")
 	}
@@ -62,7 +70,7 @@ func (e *engine) render() string {
 	if e.env.isWsl() {
 		cwd, _ = e.env.runCommand("wslpath", "-m", cwd)
 	}
-	e.write(e.ansi.consolePwd(cwd))
+	e.writeANSI(e.ansi.consolePwd(cwd))
 	return e.print()
 }
 
@@ -72,7 +80,7 @@ func (e *engine) renderBlock(block *Block) {
 	if block.Type == RPrompt && e.env.getShellName() == bash {
 		block.initPlain(e.env, e.config)
 	} else {
-		block.init(e.env, e.colorWriter, e.ansi)
+		block.init(e.env, e.writer, e.ansi)
 	}
 	block.setStringValues()
 	if !block.enabled() {
@@ -89,13 +97,13 @@ func (e *engine) renderBlock(block *Block) {
 		e.write("\n")
 	case Prompt:
 		if block.VerticalOffset != 0 {
-			e.write(e.ansi.changeLine(block.VerticalOffset))
+			e.writeANSI(e.ansi.changeLine(block.VerticalOffset))
 		}
 		switch block.Alignment {
 		case Right:
-			e.write(e.ansi.carriageForward())
+			e.writeANSI(e.ansi.carriageForward())
 			blockText := block.renderSegments()
-			e.write(e.ansi.getCursorForRightWrite(blockText, block.HorizontalOffset))
+			e.writeANSI(e.ansi.getCursorForRightWrite(blockText, block.HorizontalOffset))
 			e.write(blockText)
 		case Left:
 			e.write(block.renderSegments())
@@ -112,7 +120,7 @@ func (e *engine) renderBlock(block *Block) {
 	// color of the line above the new input line. Clearing the line fixes this,
 	// but can hopefully one day be removed when this is resolved natively.
 	if e.ansi.shell == pwsh || e.ansi.shell == powershell5 {
-		e.write(e.ansi.clearAfter())
+		e.writeANSI(e.ansi.clearAfter())
 	}
 }
 
@@ -137,7 +145,7 @@ func (e *engine) debug() string {
 	segmentTimings = append(segmentTimings, segmentTiming)
 	// loop each segments of each blocks
 	for _, block := range e.config.Blocks {
-		block.init(e.env, e.colorWriter, e.ansi)
+		block.init(e.env, e.writer, e.ansi)
 		longestSegmentName, timings := block.debug()
 		segmentTimings = append(segmentTimings, timings...)
 		if longestSegmentName > largestSegmentNameLength {
@@ -169,7 +177,7 @@ func (e *engine) print() string {
 		prompt += fmt.Sprintf("\nRPROMPT=\"%s\"", e.rprompt)
 		return prompt
 	case pwsh, powershell5, bash, plain:
-		if e.rprompt == "" || !e.canWriteRPrompt() {
+		if e.rprompt == "" || !e.canWriteRPrompt() || e.plain {
 			break
 		}
 		e.write(e.ansi.saveCursorPosition)
@@ -207,7 +215,7 @@ func (e *engine) renderTooltip(tip string) string {
 	}
 	switch e.env.getShellName() {
 	case zsh:
-		block.init(e.env, e.colorWriter, e.ansi)
+		block.init(e.env, e.writer, e.ansi)
 		return block.renderSegments()
 	case pwsh, powershell5:
 		block.initPlain(e.env, e.config)
@@ -234,15 +242,15 @@ func (e *engine) renderTransientPrompt() string {
 		Env:      e.env,
 	}
 	prompt := template.renderPlainContextTemplate(nil)
-	e.colorWriter.write(e.config.TransientPrompt.Background, e.config.TransientPrompt.Foreground, prompt)
+	e.writer.write(e.config.TransientPrompt.Background, e.config.TransientPrompt.Foreground, prompt)
 	switch e.env.getShellName() {
 	case zsh:
 		// escape double quotes contained in the prompt
-		prompt := fmt.Sprintf("PS1=\"%s\"", strings.ReplaceAll(e.colorWriter.string(), "\"", "\"\""))
+		prompt := fmt.Sprintf("PS1=\"%s\"", strings.ReplaceAll(e.writer.string(), "\"", "\"\""))
 		prompt += "\nRPROMPT=\"\""
 		return prompt
 	case pwsh, powershell5:
-		return e.colorWriter.string()
+		return e.writer.string()
 	}
 	return ""
 }
diff --git a/src/main.go b/src/main.go
index 268a56b3..a1a07ef1 100644
--- a/src/main.go
+++ b/src/main.go
@@ -60,6 +60,7 @@ type args struct {
 	StackCount     *int
 	Command        *string
 	PrintTransient *bool
+	Plain          *bool
 }
 
 func main() {
@@ -156,6 +157,10 @@ func main() {
 			"print-transient",
 			false,
 			"Print the transient prompt"),
+		Plain: flag.Bool(
+			"plain",
+			false,
+			"Print a plain prompt without ANSI"),
 	}
 	flag.Parse()
 	env := &environment{}
@@ -191,9 +196,14 @@ func main() {
 
 	ansi := &ansiUtils{}
 	ansi.init(env.getShellName())
-	colorer := &AnsiColor{
-		ansi:               ansi,
-		terminalBackground: getConsoleBackgroundColor(env, cfg.TerminalBackground),
+	var writer promptWriter
+	if *args.Plain {
+		writer = &PlainWriter{}
+	} else {
+		writer = &AnsiWriter{
+			ansi:               ansi,
+			terminalBackground: getConsoleBackgroundColor(env, cfg.TerminalBackground),
+		}
 	}
 	title := &consoleTitle{
 		env:    env,
@@ -203,9 +213,10 @@ func main() {
 	engine := &engine{
 		config:       cfg,
 		env:          env,
-		colorWriter:  colorer,
+		writer:       writer,
 		consoleTitle: title,
 		ansi:         ansi,
+		plain:        *args.Plain,
 	}
 	if *args.Debug {
 		fmt.Print(engine.debug())
diff --git a/src/ansi_color.go b/src/writer_ansi.go
similarity index 87%
rename from src/ansi_color.go
rename to src/writer_ansi.go
index db17134d..f4b4f69a 100644
--- a/src/ansi_color.go
+++ b/src/writer_ansi.go
@@ -31,6 +31,10 @@ var (
 	}
 )
 
+const (
+	colorRegex = `<(?P<foreground>[^,>]+)?,?(?P<background>[^>]+)?>(?P<content>[^<]*)<\/>`
+)
+
 // Returns the color code for a given color name
 func getColorFromName(colorName string, isBackground bool) (string, error) {
 	colorMapOffset := 0
@@ -43,7 +47,7 @@ func getColorFromName(colorName string, isBackground bool) (string, error) {
 	return "", errors.New("color name does not exist")
 }
 
-type colorWriter interface {
+type promptWriter interface {
 	write(background, foreground, text string)
 	string() string
 	reset()
@@ -52,8 +56,8 @@ type colorWriter interface {
 	clearParentColors()
 }
 
-// AnsiColor writes colorized strings
-type AnsiColor struct {
+// AnsiWriter writes colorized strings
+type AnsiWriter struct {
 	builder            strings.Builder
 	ansi               *ansiUtils
 	terminalBackground string
@@ -77,28 +81,28 @@ const (
 	Foreground = "foreground"
 )
 
-func (a *AnsiColor) setColors(background, foreground string) {
+func (a *AnsiWriter) setColors(background, foreground string) {
 	a.Colors = &Color{
 		Background: background,
 		Foreground: foreground,
 	}
 }
 
-func (a *AnsiColor) setParentColors(background, foreground string) {
+func (a *AnsiWriter) setParentColors(background, foreground string) {
 	a.ParentColors = &Color{
 		Background: background,
 		Foreground: foreground,
 	}
 }
 
-func (a *AnsiColor) clearParentColors() {
+func (a *AnsiWriter) clearParentColors() {
 	a.ParentColors = nil
 }
 
 // Gets the ANSI color code for a given color string.
 // This can include a valid hex color in the format `#FFFFFF`,
 // but also a name of one of the first 16 ANSI colors like `lightBlue`.
-func (a *AnsiColor) getAnsiFromColorString(colorString string, isBackground bool) string {
+func (a *AnsiWriter) getAnsiFromColorString(colorString string, isBackground bool) string {
 	if colorString == Transparent || len(colorString) == 0 {
 		return colorString
 	}
@@ -113,7 +117,7 @@ func (a *AnsiColor) getAnsiFromColorString(colorString string, isBackground bool
 	return style.String()
 }
 
-func (a *AnsiColor) writeColoredText(background, foreground, text string) {
+func (a *AnsiWriter) writeColoredText(background, foreground, text string) {
 	// Avoid emitting empty strings with color codes
 	if text == "" || (foreground == Transparent && background == Transparent) {
 		return
@@ -141,12 +145,12 @@ func (a *AnsiColor) writeColoredText(background, foreground, text string) {
 	a.builder.WriteString(coloredText)
 }
 
-func (a *AnsiColor) writeAndRemoveText(background, foreground, text, textToRemove, parentText string) string {
+func (a *AnsiWriter) writeAndRemoveText(background, foreground, text, textToRemove, parentText string) string {
 	a.writeColoredText(background, foreground, text)
 	return strings.Replace(parentText, textToRemove, "", 1)
 }
 
-func (a *AnsiColor) write(background, foreground, text string) {
+func (a *AnsiWriter) write(background, foreground, text string) {
 	if len(text) == 0 {
 		return
 	}
@@ -189,7 +193,7 @@ func (a *AnsiColor) write(background, foreground, text string) {
 	text = a.ansi.generateHyperlink(text)
 
 	// first we match for any potentially valid colors enclosed in <>
-	match := findAllNamedRegexMatch(`<(?P<foreground>[^,>]+)?,?(?P<background>[^>]+)?>(?P<content>[^<]*)<\/>`, text)
+	match := findAllNamedRegexMatch(colorRegex, text)
 	for i := range match {
 		fg := match[i]["foreground"]
 		bg := match[i]["background"]
@@ -214,10 +218,10 @@ func (a *AnsiColor) write(background, foreground, text string) {
 	a.writeColoredText(bgAnsi, fgAnsi, text)
 }
 
-func (a *AnsiColor) string() string {
+func (a *AnsiWriter) string() string {
 	return a.builder.String()
 }
 
-func (a *AnsiColor) reset() {
+func (a *AnsiWriter) reset() {
 	a.builder.Reset()
 }
diff --git a/src/writer_plain.go b/src/writer_plain.go
new file mode 100644
index 00000000..27f36b4d
--- /dev/null
+++ b/src/writer_plain.go
@@ -0,0 +1,41 @@
+package main
+
+import (
+	"strings"
+)
+
+// PlainWriter writes a plain string
+type PlainWriter struct {
+	builder strings.Builder
+}
+
+func (a *PlainWriter) setColors(background, foreground string)       {}
+func (a *PlainWriter) setParentColors(background, foreground string) {}
+func (a *PlainWriter) clearParentColors()                            {}
+
+func (a *PlainWriter) write(background, foreground, text string) {
+	if len(text) == 0 {
+		return
+	}
+	writeAndRemoveText := func(text, textToRemove, parentText string) string {
+		a.builder.WriteString(text)
+		return strings.Replace(parentText, textToRemove, "", 1)
+	}
+	match := findAllNamedRegexMatch(colorRegex, text)
+	for i := range match {
+		escapedTextSegment := match[i]["text"]
+		innerText := match[i]["content"]
+		textBeforeColorOverride := strings.Split(text, escapedTextSegment)[0]
+		text = writeAndRemoveText(textBeforeColorOverride, textBeforeColorOverride, text)
+		text = writeAndRemoveText(innerText, escapedTextSegment, text)
+	}
+	a.builder.WriteString(text)
+}
+
+func (a *PlainWriter) string() string {
+	return a.builder.String()
+}
+
+func (a *PlainWriter) reset() {
+	a.builder.Reset()
+}


Test patch:
diff --git a/src/ansi_color_test.go b/src/writer_ansi_test.go
similarity index 99%
rename from src/ansi_color_test.go
rename to src/writer_ansi_test.go
index 05a77dac..7d6bbcd4 100644
--- a/src/ansi_color_test.go
+++ b/src/writer_ansi_test.go
@@ -22,7 +22,7 @@ func TestGetAnsiFromColorString(t *testing.T) {
 		{Case: "Base 16 backround", Expected: "101", Color: "lightRed", Background: true},
 	}
 	for _, tc := range cases {
-		renderer := &AnsiColor{}
+		renderer := &AnsiWriter{}
 		ansiColor := renderer.getAnsiFromColorString(tc.Color, true)
 		assert.Equal(t, tc.Expected, ansiColor, tc.Case)
 	}
@@ -182,7 +182,7 @@ func TestWriteANSIColors(t *testing.T) {
 	for _, tc := range cases {
 		ansi := &ansiUtils{}
 		ansi.init("pwsh")
-		renderer := &AnsiColor{
+		renderer := &AnsiWriter{
 			ansi:               ansi,
 			ParentColors:       tc.Parent,
 			Colors:             tc.Colors,