refactor: rename color module to ansi

This commit is contained in:
Jan De Dobbeleer 2023-01-04 20:44:29 +01:00 committed by Jan De Dobbeleer
parent 005445b9fe
commit 48d8a522bf
26 changed files with 697 additions and 716 deletions

501
src/ansi/ansi_writer.go Normal file
View file

@ -0,0 +1,501 @@
package ansi
import (
"fmt"
"strings"
"github.com/jandedobbeleer/oh-my-posh/regex"
"github.com/jandedobbeleer/oh-my-posh/shell"
"github.com/mattn/go-runewidth"
)
var (
knownStyles = []*style{
{AnchorStart: `<b>`, AnchorEnd: `</b>`, Start: "\x1b[1m", End: "\x1b[22m"},
{AnchorStart: `<u>`, AnchorEnd: `</u>`, Start: "\x1b[4m", End: "\x1b[24m"},
{AnchorStart: `<o>`, AnchorEnd: `</o>`, Start: "\x1b[53m", End: "\x1b[55m"},
{AnchorStart: `<i>`, AnchorEnd: `</i>`, Start: "\x1b[3m", End: "\x1b[23m"},
{AnchorStart: `<s>`, AnchorEnd: `</s>`, Start: "\x1b[9m", End: "\x1b[29m"},
{AnchorStart: `<d>`, AnchorEnd: `</d>`, Start: "\x1b[2m", End: "\x1b[22m"},
{AnchorStart: `<f>`, AnchorEnd: `</f>`, Start: "\x1b[5m", End: "\x1b[25m"},
{AnchorStart: `<r>`, AnchorEnd: `</r>`, Start: "\x1b[7m", End: "\x1b[27m"},
}
colorStyle = &style{AnchorStart: "COLOR", AnchorEnd: `</>`, End: "\x1b[0m"}
)
type style struct {
AnchorStart string
AnchorEnd string
Start string
End string
}
type cachedColor struct {
Background string
Foreground string
}
const (
// Transparent implies a transparent color
Transparent = "transparent"
// Accent is the OS accent color
Accent = "accent"
// ParentBackground takes the previous segment's background color
ParentBackground = "parentBackground"
// ParentForeground takes the previous segment's color
ParentForeground = "parentForeground"
// Background takes the current segment's background color
Background = "background"
// Foreground takes the current segment's foreground color
Foreground = "foreground"
anchorRegex = `^(?P<ANCHOR><(?P<FG>[^,>]+)?,?(?P<BG>[^>]+)?>)`
colorise = "\x1b[%sm"
transparent = "\x1b[%s;49m\x1b[7m"
AnsiRegex = "[\u001B\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[a-zA-Z\\d]*)*)?\u0007)|(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PRZcf-ntqry=><~]))"
OSC99 string = "osc99"
OSC7 string = "osc7"
OSC51 string = "osc51"
)
// Writer writes colorized ANSI strings
type Writer struct {
TerminalBackground string
Colors *cachedColor
ParentColors []*cachedColor
AnsiColors Colors
Plain bool
builder strings.Builder
length int
foreground Color
background Color
currentForeground Color
currentBackground Color
runes []rune
shell string
format string
left string
right string
title string
linechange string
clearBelow string
clearLine string
saveCursorPosition string
restoreCursorPosition string
escapeLeft string
escapeRight string
hyperlink string
hyperlinkRegex string
osc99 string
osc7 string
osc51 string
}
func (w *Writer) Init(shellName string) {
w.shell = shellName
switch w.shell {
case shell.BASH:
w.format = "\\[%s\\]"
w.linechange = "\\[\x1b[%d%s\\]"
w.right = "\\[\x1b[%dC\\]"
w.left = "\\[\x1b[%dD\\]"
w.clearBelow = "\\[\x1b[0J\\]"
w.clearLine = "\\[\x1b[K\\]"
w.saveCursorPosition = "\\[\x1b7\\]"
w.restoreCursorPosition = "\\[\x1b8\\]"
w.title = "\\[\x1b]0;%s\007\\]"
w.escapeLeft = "\\["
w.escapeRight = "\\]"
w.hyperlink = "\\[\x1b]8;;%s\x1b\\\\\\]%s\\[\x1b]8;;\x1b\\\\\\]"
w.hyperlinkRegex = `(?P<STR>\\\[\x1b\]8;;(.+)\x1b\\\\\\\](?P<TEXT>.+)\\\[\x1b\]8;;\x1b\\\\\\\])`
w.osc99 = "\\[\x1b]9;9;\"%s\"\x1b\\\\\\]"
w.osc7 = "\\[\x1b]7;\"file://%s/%s\"\x1b\\\\\\]"
w.osc51 = "\\[\x1b]51;A;%s@%s:%s\x1b\\\\\\]"
case "zsh":
w.format = "%%{%s%%}"
w.linechange = "%%{\x1b[%d%s%%}"
w.right = "%%{\x1b[%dC%%}"
w.left = "%%{\x1b[%dD%%}"
w.clearBelow = "%{\x1b[0J%}"
w.clearLine = "%{\x1b[K%}"
w.saveCursorPosition = "%{\x1b7%}"
w.restoreCursorPosition = "%{\x1b8%}"
w.title = "%%{\x1b]0;%s\007%%}"
w.escapeLeft = "%{"
w.escapeRight = "%}"
w.hyperlink = "%%{\x1b]8;;%s\x1b\\%%}%s%%{\x1b]8;;\x1b\\%%}"
w.hyperlinkRegex = `(?P<STR>%{\x1b]8;;(.+)\x1b\\%}(?P<TEXT>.+)%{\x1b]8;;\x1b\\%})`
w.osc99 = "%%{\x1b]9;9;\"%s\"\x1b\\%%}"
w.osc7 = "%%{\x1b]7;file:\"//%s/%s\"\x1b\\%%}"
w.osc51 = "%%{\x1b]51;A%s@%s:%s\x1b\\%%}"
default:
w.linechange = "\x1b[%d%s"
w.right = "\x1b[%dC"
w.left = "\x1b[%dD"
w.clearBelow = "\x1b[0J"
w.clearLine = "\x1b[K"
w.saveCursorPosition = "\x1b7"
w.restoreCursorPosition = "\x1b8"
w.title = "\x1b]0;%s\007"
// when in fish on Linux, it seems hyperlinks ending with \\ print a \
// unlike on macOS. However, this is a fish bug, so do not try to fix it here:
// https://github.com/JanDeDobbeleer/oh-my-posh/pull/3288#issuecomment-1369137068
w.hyperlink = "\x1b]8;;%s\x1b\\%s\x1b]8;;\x1b\\"
w.hyperlinkRegex = "(?P<STR>\x1b]8;;(.+)\x1b\\\\\\\\?(?P<TEXT>.+)\x1b]8;;\x1b\\\\)"
w.osc99 = "\x1b]9;9;\"%s\"\x1b\\"
w.osc7 = "\x1b]7;\"file://%s/%s\"\x1b\\"
w.osc51 = "\x1b]51;A%s@%s:%s\x1b\\"
}
}
func (w *Writer) SetColors(background, foreground string) {
w.Colors = &cachedColor{
Background: background,
Foreground: foreground,
}
}
func (w *Writer) SetParentColors(background, foreground string) {
if w.ParentColors == nil {
w.ParentColors = make([]*cachedColor, 0)
}
w.ParentColors = append([]*cachedColor{{
Background: background,
Foreground: foreground,
}}, w.ParentColors...)
}
func (w *Writer) CarriageForward() string {
return fmt.Sprintf(w.right, 1000)
}
func (w *Writer) GetCursorForRightWrite(length, offset int) string {
strippedLen := length + (-offset)
return fmt.Sprintf(w.left, strippedLen)
}
func (w *Writer) ChangeLine(numberOfLines int) string {
if w.Plain {
return ""
}
position := "B"
if numberOfLines < 0 {
position = "F"
numberOfLines = -numberOfLines
}
return fmt.Sprintf(w.linechange, numberOfLines, position)
}
func (w *Writer) ConsolePwd(pwdType, userName, hostName, pwd string) string {
if w.Plain {
return ""
}
if strings.HasSuffix(pwd, ":") {
pwd += "\\"
}
switch pwdType {
case OSC7:
return fmt.Sprintf(w.osc7, hostName, pwd)
case OSC51:
return fmt.Sprintf(w.osc51, userName, hostName, pwd)
case OSC99:
fallthrough
default:
return fmt.Sprintf(w.osc99, pwd)
}
}
func (w *Writer) ClearAfter() string {
if w.Plain {
return ""
}
return w.clearLine + w.clearBelow
}
func (w *Writer) FormatTitle(title string) string {
title = w.trimAnsi(title)
// we have to do this to prevent bash/zsh from misidentifying escape sequences
switch w.shell {
case shell.BASH:
title = strings.NewReplacer("`", "\\`", `\`, `\\`).Replace(title)
case shell.ZSH:
title = strings.NewReplacer("`", "\\`", `%`, `%%`).Replace(title)
}
return fmt.Sprintf(w.title, title)
}
func (w *Writer) FormatText(text string) string {
return fmt.Sprintf(w.format, text)
}
func (w *Writer) SaveCursorPosition() string {
return w.saveCursorPosition
}
func (w *Writer) RestoreCursorPosition() string {
return w.restoreCursorPosition
}
func (w *Writer) LineBreak() string {
cr := fmt.Sprintf(w.left, 1000)
lf := fmt.Sprintf(w.linechange, 1, "B")
return cr + lf
}
func (w *Writer) Write(background, foreground, text string) {
if len(text) == 0 {
return
}
if !w.Plain {
text = w.GenerateHyperlink(text)
}
w.background, w.foreground = w.asAnsiColors(background, foreground)
// default to white foreground
if w.foreground.IsEmpty() {
w.foreground = w.AnsiColors.AnsiColorFromString("white", false)
}
// validate if we start with a color override
match := regex.FindNamedRegexMatch(anchorRegex, text)
if len(match) != 0 {
colorOverride := true
for _, style := range knownStyles {
if match["ANCHOR"] != style.AnchorStart {
continue
}
w.printEscapedAnsiString(style.Start)
colorOverride = false
}
if colorOverride {
w.currentBackground, w.currentForeground = w.asAnsiColors(match["BG"], match["FG"])
}
}
w.writeSegmentColors()
text = text[len(match["ANCHOR"]):]
w.runes = []rune(text)
for i := 0; i < len(w.runes); i++ {
s := w.runes[i]
// ignore everything which isn't overriding
if s != '<' {
w.length += runewidth.RuneWidth(s)
w.builder.WriteRune(s)
continue
}
// color/end overrides first
text = string(w.runes[i:])
match = regex.FindNamedRegexMatch(anchorRegex, text)
if len(match) > 0 {
i = w.writeColorOverrides(match, background, i)
continue
}
w.length += runewidth.RuneWidth(s)
w.builder.WriteRune(s)
}
w.printEscapedAnsiString(colorStyle.End)
// reset current
w.currentBackground = ""
w.currentForeground = ""
}
func (w *Writer) printEscapedAnsiString(text string) {
if w.Plain {
return
}
if len(w.format) == 0 {
w.builder.WriteString(text)
return
}
w.builder.WriteString(fmt.Sprintf(w.format, text))
}
func (w *Writer) getAnsiFromColorString(colorString string, isBackground bool) Color {
return w.AnsiColors.AnsiColorFromString(colorString, isBackground)
}
func (w *Writer) writeSegmentColors() {
// use correct starting colors
bg := w.background
fg := w.foreground
if !w.currentBackground.IsEmpty() {
bg = w.currentBackground
}
if !w.currentForeground.IsEmpty() {
fg = w.currentForeground
}
if fg.IsTransparent() && len(w.TerminalBackground) != 0 {
background := w.getAnsiFromColorString(w.TerminalBackground, false)
w.printEscapedAnsiString(fmt.Sprintf(colorise, background))
w.printEscapedAnsiString(fmt.Sprintf(colorise, bg.ToForeground()))
} else if fg.IsTransparent() && !bg.IsEmpty() {
w.printEscapedAnsiString(fmt.Sprintf(transparent, bg))
} else {
if !bg.IsEmpty() && !bg.IsTransparent() {
w.printEscapedAnsiString(fmt.Sprintf(colorise, bg))
}
if !fg.IsEmpty() {
w.printEscapedAnsiString(fmt.Sprintf(colorise, fg))
}
}
// set current colors
w.currentBackground = bg
w.currentForeground = fg
}
func (w *Writer) writeColorOverrides(match map[string]string, background string, i int) (position int) {
position = i
// check color reset first
if match["ANCHOR"] == colorStyle.AnchorEnd {
// make sure to reset the colors if needed
position += len([]rune(colorStyle.AnchorEnd)) - 1
// do not restore colors at the end of the string, we print it anyways
if position == len(w.runes)-1 {
return
}
if w.currentBackground != w.background {
w.printEscapedAnsiString(fmt.Sprintf(colorise, w.background))
}
if w.currentForeground != w.foreground {
w.printEscapedAnsiString(fmt.Sprintf(colorise, w.foreground))
}
return
}
position += len([]rune(match["ANCHOR"])) - 1
for _, style := range knownStyles {
if style.AnchorEnd == match["ANCHOR"] {
w.printEscapedAnsiString(style.End)
return
}
if style.AnchorStart == match["ANCHOR"] {
w.printEscapedAnsiString(style.Start)
return
}
}
if match["FG"] == Transparent && len(match["BG"]) == 0 {
match["BG"] = background
}
w.currentBackground, w.currentForeground = w.asAnsiColors(match["BG"], match["FG"])
// make sure we have colors
if w.currentForeground.IsEmpty() {
w.currentForeground = w.foreground
}
if w.currentBackground.IsEmpty() {
w.currentBackground = w.background
}
if w.currentForeground.IsTransparent() && len(w.TerminalBackground) != 0 {
background := w.getAnsiFromColorString(w.TerminalBackground, false)
w.printEscapedAnsiString(fmt.Sprintf(colorise, background))
w.printEscapedAnsiString(fmt.Sprintf(colorise, w.currentBackground.ToForeground()))
return
}
if w.currentForeground.IsTransparent() && !w.currentBackground.IsTransparent() {
w.printEscapedAnsiString(fmt.Sprintf(transparent, w.currentBackground))
return
}
if w.currentBackground != w.background {
// end the colors in case we have a transparent background
if w.currentBackground.IsTransparent() {
w.printEscapedAnsiString(colorStyle.End)
} else {
w.printEscapedAnsiString(fmt.Sprintf(colorise, w.currentBackground))
}
}
if w.currentForeground != w.foreground || w.currentBackground.IsTransparent() {
w.printEscapedAnsiString(fmt.Sprintf(colorise, w.currentForeground))
}
return position
}
func (w *Writer) asAnsiColors(background, foreground string) (Color, Color) {
background = w.expandKeyword(background)
foreground = w.expandKeyword(foreground)
inverted := foreground == Transparent && len(background) != 0
backgroundAnsi := w.getAnsiFromColorString(background, !inverted)
foregroundAnsi := w.getAnsiFromColorString(foreground, false)
return backgroundAnsi, foregroundAnsi
}
func (w *Writer) isKeyword(color string) bool {
switch color {
case Transparent, ParentBackground, ParentForeground, Background, Foreground:
return true
default:
return false
}
}
func (w *Writer) expandKeyword(keyword string) string {
resolveParentColor := func(keyword string) string {
for _, color := range w.ParentColors {
if color == nil {
return Transparent
}
switch keyword {
case ParentBackground:
keyword = color.Background
case ParentForeground:
keyword = color.Foreground
default:
if len(keyword) == 0 {
return Transparent
}
return keyword
}
}
if len(keyword) == 0 {
return Transparent
}
return keyword
}
resolveKeyword := func(keyword string) string {
switch {
case keyword == Background && w.Colors != nil:
return w.Colors.Background
case keyword == Foreground && w.Colors != nil:
return w.Colors.Foreground
case (keyword == ParentBackground || keyword == ParentForeground) && w.ParentColors != nil:
return resolveParentColor(keyword)
default:
return Transparent
}
}
for ok := w.isKeyword(keyword); ok; ok = w.isKeyword(keyword) {
resolved := resolveKeyword(keyword)
if resolved == keyword {
break
}
keyword = resolved
}
return keyword
}
func (w *Writer) String() (string, int) {
defer func() {
w.length = 0
w.builder.Reset()
}()
return w.builder.String(), w.length
}

View file

@ -1,4 +1,4 @@
package color package ansi
import ( import (
"fmt" "fmt"
@ -8,7 +8,7 @@ import (
"github.com/jandedobbeleer/oh-my-posh/shell" "github.com/jandedobbeleer/oh-my-posh/shell"
) )
func (a *AnsiWriter) GenerateHyperlink(text string) string { func (w *Writer) GenerateHyperlink(text string) string {
const ( const (
LINK = "link" LINK = "link"
TEXT = "text" TEXT = "text"
@ -60,7 +60,7 @@ func (a *AnsiWriter) GenerateHyperlink(text string) string {
continue continue
} }
// end of link part // end of link part
result.WriteString(a.replaceHyperlink(hyperlink.String())) result.WriteString(w.replaceHyperlink(hyperlink.String()))
hyperlink.Reset() hyperlink.Reset()
state = OTHER state = OTHER
} }
@ -70,21 +70,21 @@ func (a *AnsiWriter) GenerateHyperlink(text string) string {
return result.String() return result.String()
} }
func (a *AnsiWriter) replaceHyperlink(text string) string { func (w *Writer) replaceHyperlink(text string) string {
// hyperlink matching // hyperlink matching
results := regex.FindNamedRegexMatch("(?P<ALL>(?:\\[(?P<TEXT>.+)\\])(?:\\((?P<URL>.*)\\)))", text) results := regex.FindNamedRegexMatch("(?P<ALL>(?:\\[(?P<TEXT>.+)\\])(?:\\((?P<URL>.*)\\)))", text)
if len(results) != 3 { if len(results) != 3 {
return text return text
} }
linkText := a.escapeLinkTextForFishShell(results["TEXT"]) linkText := w.escapeLinkTextForFishShell(results["TEXT"])
// build hyperlink ansi // build hyperlink ansi
hyperlink := fmt.Sprintf(a.hyperlink, results["URL"], linkText) hyperlink := fmt.Sprintf(w.hyperlink, results["URL"], linkText)
// replace original text by the new onex // replace original text by the new onex
return strings.Replace(text, results["ALL"], hyperlink, 1) return strings.Replace(text, results["ALL"], hyperlink, 1)
} }
func (a *AnsiWriter) escapeLinkTextForFishShell(text string) string { func (w *Writer) escapeLinkTextForFishShell(text string) string {
if a.shell != shell.FISH { if w.shell != shell.FISH {
return text return text
} }
escapeChars := map[string]string{ escapeChars := map[string]string{

View file

@ -1,4 +1,4 @@
package color package ansi
import ( import (
"testing" "testing"
@ -19,7 +19,7 @@ func TestGenerateHyperlinkNoUrl(t *testing.T) {
{Text: "sample text with no url", ShellName: shell.BASH, Expected: "sample text with no url"}, {Text: "sample text with no url", ShellName: shell.BASH, Expected: "sample text with no url"},
} }
for _, tc := range cases { for _, tc := range cases {
a := AnsiWriter{} a := Writer{}
a.Init(tc.ShellName) a.Init(tc.ShellName)
hyperlinkText := a.GenerateHyperlink(tc.Text) hyperlinkText := a.GenerateHyperlink(tc.Text)
assert.Equal(t, tc.Expected, hyperlinkText) assert.Equal(t, tc.Expected, hyperlinkText)
@ -52,7 +52,7 @@ func TestGenerateHyperlinkWithUrl(t *testing.T) {
}, },
} }
for _, tc := range cases { for _, tc := range cases {
a := AnsiWriter{} a := Writer{}
a.Init(tc.ShellName) a.Init(tc.ShellName)
hyperlinkText := a.GenerateHyperlink(tc.Text) hyperlinkText := a.GenerateHyperlink(tc.Text)
assert.Equal(t, tc.Expected, hyperlinkText) assert.Equal(t, tc.Expected, hyperlinkText)
@ -70,7 +70,7 @@ func TestGenerateHyperlinkWithUrlNoName(t *testing.T) {
{Text: "[](http://www.google.be)", ShellName: shell.BASH, Expected: "[](http://www.google.be)"}, {Text: "[](http://www.google.be)", ShellName: shell.BASH, Expected: "[](http://www.google.be)"},
} }
for _, tc := range cases { for _, tc := range cases {
a := AnsiWriter{} a := Writer{}
a.Init(tc.ShellName) a.Init(tc.ShellName)
hyperlinkText := a.GenerateHyperlink(tc.Text) hyperlinkText := a.GenerateHyperlink(tc.Text)
assert.Equal(t, tc.Expected, hyperlinkText) assert.Equal(t, tc.Expected, hyperlinkText)
@ -89,7 +89,7 @@ func TestGenerateFileLink(t *testing.T) {
{Text: `[Windows](file:C:/Windows)`, Expected: "\x1b]8;;file:C:/Windows\x1b\\Windows\x1b]8;;\x1b\\"}, {Text: `[Windows](file:C:/Windows)`, Expected: "\x1b]8;;file:C:/Windows\x1b\\Windows\x1b]8;;\x1b\\"},
} }
for _, tc := range cases { for _, tc := range cases {
a := AnsiWriter{} a := Writer{}
a.Init(shell.PWSH) a.Init(shell.PWSH)
hyperlinkText := a.GenerateHyperlink(tc.Text) hyperlinkText := a.GenerateHyperlink(tc.Text)
assert.Equal(t, tc.Expected, hyperlinkText) assert.Equal(t, tc.Expected, hyperlinkText)

View file

@ -1,4 +1,4 @@
package color package ansi
import ( import (
"testing" "testing"
@ -13,187 +13,187 @@ func TestWriteANSIColors(t *testing.T) {
Case string Case string
Expected string Expected string
Input string Input string
Colors *Color Colors *cachedColor
Parent *Color Parent *cachedColor
TerminalBackground string TerminalBackground string
}{ }{
{ {
Case: "Bold", Case: "Bold",
Input: "<b>test</b>", Input: "<b>test</b>",
Expected: "\x1b[1m\x1b[30mtest\x1b[22m\x1b[0m", Expected: "\x1b[1m\x1b[30mtest\x1b[22m\x1b[0m",
Colors: &Color{Foreground: "black", Background: ParentBackground}, Colors: &cachedColor{Foreground: "black", Background: ParentBackground},
}, },
{ {
Case: "Bold with color override", Case: "Bold with color override",
Input: "<b><#ffffff>test</></b>", Input: "<b><#ffffff>test</></b>",
Expected: "\x1b[1m\x1b[30m\x1b[38;2;255;255;255mtest\x1b[30m\x1b[22m\x1b[0m", Expected: "\x1b[1m\x1b[30m\x1b[38;2;255;255;255mtest\x1b[30m\x1b[22m\x1b[0m",
Colors: &Color{Foreground: "black", Background: ParentBackground}, Colors: &cachedColor{Foreground: "black", Background: ParentBackground},
}, },
{ {
Case: "Bold with color override, flavor 2", Case: "Bold with color override, flavor 2",
Input: "<#ffffff><b>test</b></>", Input: "<#ffffff><b>test</b></>",
Expected: "\x1b[38;2;255;255;255m\x1b[1mtest\x1b[22m\x1b[0m", Expected: "\x1b[38;2;255;255;255m\x1b[1mtest\x1b[22m\x1b[0m",
Colors: &Color{Foreground: "black", Background: ParentBackground}, Colors: &cachedColor{Foreground: "black", Background: ParentBackground},
}, },
{ {
Case: "Double override", Case: "Double override",
Input: "<#ffffff>jan</>@<#ffffff>Jans-MBP</>", Input: "<#ffffff>jan</>@<#ffffff>Jans-MBP</>",
Expected: "\x1b[48;2;255;87;51m\x1b[38;2;255;255;255mjan\x1b[32m@\x1b[38;2;255;255;255mJans-MBP\x1b[0m", Expected: "\x1b[48;2;255;87;51m\x1b[38;2;255;255;255mjan\x1b[32m@\x1b[38;2;255;255;255mJans-MBP\x1b[0m",
Colors: &Color{Foreground: "green", Background: "#FF5733"}, Colors: &cachedColor{Foreground: "green", Background: "#FF5733"},
}, },
{ {
Case: "No color override", Case: "No color override",
Input: "test", Input: "test",
Expected: "\x1b[47m\x1b[30mtest\x1b[0m", Expected: "\x1b[47m\x1b[30mtest\x1b[0m",
Colors: &Color{Foreground: "black", Background: "white"}, Colors: &cachedColor{Foreground: "black", Background: "white"},
Parent: &Color{Foreground: "black", Background: "white"}, Parent: &cachedColor{Foreground: "black", Background: "white"},
}, },
{ {
Case: "Inherit foreground", Case: "Inherit foreground",
Input: "test", Input: "test",
Expected: "\x1b[47m\x1b[33mtest\x1b[0m", Expected: "\x1b[47m\x1b[33mtest\x1b[0m",
Colors: &Color{Foreground: ParentForeground, Background: "white"}, Colors: &cachedColor{Foreground: ParentForeground, Background: "white"},
Parent: &Color{Foreground: "yellow", Background: "white"}, Parent: &cachedColor{Foreground: "yellow", Background: "white"},
}, },
{ {
Case: "Inherit background", Case: "Inherit background",
Input: "test", Input: "test",
Expected: "\x1b[41m\x1b[30mtest\x1b[0m", Expected: "\x1b[41m\x1b[30mtest\x1b[0m",
Colors: &Color{Foreground: "black", Background: ParentBackground}, Colors: &cachedColor{Foreground: "black", Background: ParentBackground},
Parent: &Color{Foreground: "yellow", Background: "red"}, Parent: &cachedColor{Foreground: "yellow", Background: "red"},
}, },
{ {
Case: "No parent", Case: "No parent",
Input: "test", Input: "test",
Expected: "\x1b[30mtest\x1b[0m", Expected: "\x1b[30mtest\x1b[0m",
Colors: &Color{Foreground: "black", Background: ParentBackground}, Colors: &cachedColor{Foreground: "black", Background: ParentBackground},
}, },
{ {
Case: "Inherit override foreground", Case: "Inherit override foreground",
Input: "hello <parentForeground>world</>", Input: "hello <parentForeground>world</>",
Expected: "\x1b[47m\x1b[30mhello \x1b[33mworld\x1b[0m", Expected: "\x1b[47m\x1b[30mhello \x1b[33mworld\x1b[0m",
Colors: &Color{Foreground: "black", Background: "white"}, Colors: &cachedColor{Foreground: "black", Background: "white"},
Parent: &Color{Foreground: "yellow", Background: "red"}, Parent: &cachedColor{Foreground: "yellow", Background: "red"},
}, },
{ {
Case: "Inherit override background", Case: "Inherit override background",
Input: "hello <black,parentBackground>world</>", Input: "hello <black,parentBackground>world</>",
Expected: "\x1b[47m\x1b[30mhello \x1b[41mworld\x1b[0m", Expected: "\x1b[47m\x1b[30mhello \x1b[41mworld\x1b[0m",
Colors: &Color{Foreground: "black", Background: "white"}, Colors: &cachedColor{Foreground: "black", Background: "white"},
Parent: &Color{Foreground: "yellow", Background: "red"}, Parent: &cachedColor{Foreground: "yellow", Background: "red"},
}, },
{ {
Case: "Inherit override background, no foreground specified", Case: "Inherit override background, no foreground specified",
Input: "hello <,parentBackground>world</>", Input: "hello <,parentBackground>world</>",
Expected: "\x1b[47m\x1b[30mhello \x1b[41mworld\x1b[0m", Expected: "\x1b[47m\x1b[30mhello \x1b[41mworld\x1b[0m",
Colors: &Color{Foreground: "black", Background: "white"}, Colors: &cachedColor{Foreground: "black", Background: "white"},
Parent: &Color{Foreground: "yellow", Background: "red"}, Parent: &cachedColor{Foreground: "yellow", Background: "red"},
}, },
{ {
Case: "Inherit no parent foreground", Case: "Inherit no parent foreground",
Input: "hello <parentForeground>world</>", Input: "hello <parentForeground>world</>",
Expected: "\x1b[47m\x1b[30mhello \x1b[47;49m\x1b[7mworld\x1b[0m", Expected: "\x1b[47m\x1b[30mhello \x1b[47;49m\x1b[7mworld\x1b[0m",
Colors: &Color{Foreground: "black", Background: "white"}, Colors: &cachedColor{Foreground: "black", Background: "white"},
}, },
{ {
Case: "Inherit no parent background", Case: "Inherit no parent background",
Input: "hello <,parentBackground>world</>", Input: "hello <,parentBackground>world</>",
Expected: "\x1b[47m\x1b[30mhello \x1b[0m\x1b[30mworld\x1b[0m", Expected: "\x1b[47m\x1b[30mhello \x1b[0m\x1b[30mworld\x1b[0m",
Colors: &Color{Foreground: "black", Background: "white"}, Colors: &cachedColor{Foreground: "black", Background: "white"},
}, },
{ {
Case: "Inherit override both", Case: "Inherit override both",
Input: "hello <parentForeground,parentBackground>world</>", Input: "hello <parentForeground,parentBackground>world</>",
Expected: "\x1b[47m\x1b[30mhello \x1b[41m\x1b[33mworld\x1b[0m", Expected: "\x1b[47m\x1b[30mhello \x1b[41m\x1b[33mworld\x1b[0m",
Colors: &Color{Foreground: "black", Background: "white"}, Colors: &cachedColor{Foreground: "black", Background: "white"},
Parent: &Color{Foreground: "yellow", Background: "red"}, Parent: &cachedColor{Foreground: "yellow", Background: "red"},
}, },
{ {
Case: "Inherit override both inverted", Case: "Inherit override both inverted",
Input: "hello <parentBackground,parentForeground>world</>", Input: "hello <parentBackground,parentForeground>world</>",
Expected: "\x1b[47m\x1b[30mhello \x1b[43m\x1b[31mworld\x1b[0m", Expected: "\x1b[47m\x1b[30mhello \x1b[43m\x1b[31mworld\x1b[0m",
Colors: &Color{Foreground: "black", Background: "white"}, Colors: &cachedColor{Foreground: "black", Background: "white"},
Parent: &Color{Foreground: "yellow", Background: "red"}, Parent: &cachedColor{Foreground: "yellow", Background: "red"},
}, },
{ {
Case: "Inline override", Case: "Inline override",
Input: "hello, <red>world</>, rabbit", Input: "hello, <red>world</>, rabbit",
Expected: "\x1b[47m\x1b[30mhello, \x1b[31mworld\x1b[30m, rabbit\x1b[0m", Expected: "\x1b[47m\x1b[30mhello, \x1b[31mworld\x1b[30m, rabbit\x1b[0m",
Colors: &Color{Foreground: "black", Background: "white"}, Colors: &cachedColor{Foreground: "black", Background: "white"},
}, },
{ {
Case: "Transparent background", Case: "Transparent background",
Input: "hello world", Input: "hello world",
Expected: "\x1b[37mhello world\x1b[0m", Expected: "\x1b[37mhello world\x1b[0m",
Colors: &Color{Foreground: "white", Background: Transparent}, Colors: &cachedColor{Foreground: "white", Background: Transparent},
}, },
{ {
Case: "Transparent foreground override", Case: "Transparent foreground override",
Input: "hello <#ffffff>world</>", Input: "hello <#ffffff>world</>",
Expected: "\x1b[32mhello \x1b[38;2;255;255;255mworld\x1b[0m", Expected: "\x1b[32mhello \x1b[38;2;255;255;255mworld\x1b[0m",
Colors: &Color{Foreground: "green", Background: Transparent}, Colors: &cachedColor{Foreground: "green", Background: Transparent},
}, },
{ {
Case: "No foreground", Case: "No foreground",
Input: "test", Input: "test",
Expected: "\x1b[48;2;255;87;51m\x1b[37mtest\x1b[0m", Expected: "\x1b[48;2;255;87;51m\x1b[37mtest\x1b[0m",
Colors: &Color{Foreground: "", Background: "#FF5733"}, Colors: &cachedColor{Foreground: "", Background: "#FF5733"},
}, },
{ {
Case: "Transparent foreground", Case: "Transparent foreground",
Input: "test", Input: "test",
Expected: "\x1b[38;2;255;87;51;49m\x1b[7mtest\x1b[0m", Expected: "\x1b[38;2;255;87;51;49m\x1b[7mtest\x1b[0m",
Colors: &Color{Foreground: Transparent, Background: "#FF5733"}, Colors: &cachedColor{Foreground: Transparent, Background: "#FF5733"},
}, },
{ {
Case: "Transparent foreground, terminal background set", Case: "Transparent foreground, terminal background set",
Input: "test", Input: "test",
Expected: "\x1b[38;2;33;47;60m\x1b[48;2;255;87;51mtest\x1b[0m", Expected: "\x1b[38;2;33;47;60m\x1b[48;2;255;87;51mtest\x1b[0m",
Colors: &Color{Foreground: Transparent, Background: "#FF5733"}, Colors: &cachedColor{Foreground: Transparent, Background: "#FF5733"},
TerminalBackground: "#212F3C", TerminalBackground: "#212F3C",
}, },
{ {
Case: "Foreground for foreground override", Case: "Foreground for foreground override",
Input: "<foreground>test</>", Input: "<foreground>test</>",
Expected: "\x1b[47m\x1b[30mtest\x1b[0m", Expected: "\x1b[47m\x1b[30mtest\x1b[0m",
Colors: &Color{Foreground: "black", Background: "white"}, Colors: &cachedColor{Foreground: "black", Background: "white"},
}, },
{ {
Case: "Background for background override", Case: "Background for background override",
Input: "<,background>test</>", Input: "<,background>test</>",
Expected: "\x1b[47m\x1b[30mtest\x1b[0m", Expected: "\x1b[47m\x1b[30mtest\x1b[0m",
Colors: &Color{Foreground: "black", Background: "white"}, Colors: &cachedColor{Foreground: "black", Background: "white"},
}, },
{ {
Case: "Google", Case: "Google",
Input: "<blue,white>G</><red,white>o</><yellow,white>o</><blue,white>g</><green,white>l</><red,white>e</>", Input: "<blue,white>G</><red,white>o</><yellow,white>o</><blue,white>g</><green,white>l</><red,white>e</>",
Expected: "\x1b[47m\x1b[34mG\x1b[40m\x1b[30m\x1b[47m\x1b[31mo\x1b[40m\x1b[30m\x1b[47m\x1b[33mo\x1b[40m\x1b[30m\x1b[47m\x1b[34mg\x1b[40m\x1b[30m\x1b[47m\x1b[32ml\x1b[40m\x1b[30m\x1b[47m\x1b[31me\x1b[0m", //nolint: lll Expected: "\x1b[47m\x1b[34mG\x1b[40m\x1b[30m\x1b[47m\x1b[31mo\x1b[40m\x1b[30m\x1b[47m\x1b[33mo\x1b[40m\x1b[30m\x1b[47m\x1b[34mg\x1b[40m\x1b[30m\x1b[47m\x1b[32ml\x1b[40m\x1b[30m\x1b[47m\x1b[31me\x1b[0m", //nolint: lll
Colors: &Color{Foreground: "black", Background: "black"}, Colors: &cachedColor{Foreground: "black", Background: "black"},
}, },
{ {
Case: "Foreground for background override", Case: "Foreground for background override",
Input: "<background>test</>", Input: "<background>test</>",
Expected: "\x1b[47m\x1b[37mtest\x1b[0m", Expected: "\x1b[47m\x1b[37mtest\x1b[0m",
Colors: &Color{Foreground: "black", Background: "white"}, Colors: &cachedColor{Foreground: "black", Background: "white"},
}, },
{ {
Case: "Foreground for background vice versa override", Case: "Foreground for background vice versa override",
Input: "<background,foreground>test</>", Input: "<background,foreground>test</>",
Expected: "\x1b[40m\x1b[37mtest\x1b[0m", Expected: "\x1b[40m\x1b[37mtest\x1b[0m",
Colors: &Color{Foreground: "black", Background: "white"}, Colors: &cachedColor{Foreground: "black", Background: "white"},
}, },
{ {
Case: "Background for foreground override", Case: "Background for foreground override",
Input: "<,foreground>test</>", Input: "<,foreground>test</>",
Expected: "\x1b[40m\x1b[30mtest\x1b[0m", Expected: "\x1b[40m\x1b[30mtest\x1b[0m",
Colors: &Color{Foreground: "black", Background: "white"}, Colors: &cachedColor{Foreground: "black", Background: "white"},
}, },
} }
for _, tc := range cases { for _, tc := range cases {
renderer := &AnsiWriter{ renderer := &Writer{
ParentColors: []*Color{tc.Parent}, ParentColors: []*cachedColor{tc.Parent},
Colors: tc.Colors, Colors: tc.Colors,
TerminalBackground: tc.TerminalBackground, TerminalBackground: tc.TerminalBackground,
AnsiColors: &DefaultColors{}, AnsiColors: &DefaultColors{},

View file

@ -1,4 +1,4 @@
package color package ansi
import ( import (
"fmt" "fmt"
@ -10,41 +10,41 @@ import (
"github.com/gookit/color" "github.com/gookit/color"
) )
// AnsiColors is the interface that wraps AnsiColorFromString method. // Colors is the interface that wraps AnsiColorFromString method.
// //
// AnsiColorFromString gets the ANSI color code for a given color string. // AnsiColorFromString gets the ANSI color code for a given color string.
// This can include a valid hex color in the format `#FFFFFF`, // 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`. // but also a name of one of the first 16 ANSI colors like `lightBlue`.
type AnsiColors interface { type Colors interface {
AnsiColorFromString(colorString string, isBackground bool) AnsiColor AnsiColorFromString(colorString string, isBackground bool) Color
} }
// AnsiColor is an ANSI color code ready to be printed to the console. // Color is an ANSI color code ready to be printed to the console.
// Example: "38;2;255;255;255", "48;2;255;255;255", "31", "95". // Example: "38;2;255;255;255", "48;2;255;255;255", "31", "95".
type AnsiColor string type Color string
const ( const (
emptyAnsiColor = AnsiColor("") emptyColor = Color("")
transparentAnsiColor = AnsiColor(Transparent) transparentColor = Color(Transparent)
) )
func (c AnsiColor) IsEmpty() bool { func (c Color) IsEmpty() bool {
return c == emptyAnsiColor return c == emptyColor
} }
func (c AnsiColor) IsTransparent() bool { func (c Color) IsTransparent() bool {
return c == transparentAnsiColor return c == transparentColor
} }
func (c AnsiColor) ToForeground() AnsiColor { func (c Color) ToForeground() Color {
colorString := string(c) colorString := string(c)
if strings.HasPrefix(colorString, "38;") { if strings.HasPrefix(colorString, "38;") {
return AnsiColor(strings.Replace(colorString, "38;", "48;", 1)) return Color(strings.Replace(colorString, "38;", "48;", 1))
} }
return c return c
} }
func MakeColors(palette Palette, cacheEnabled bool, accentColor string, env platform.Environment) (colors AnsiColors) { func MakeColors(palette Palette, cacheEnabled bool, accentColor string, env platform.Environment) (colors Colors) {
defaultColors := &DefaultColors{} defaultColors := &DefaultColors{}
defaultColors.SetAccentColor(env, accentColor) defaultColors.SetAccentColor(env, accentColor)
colors = defaultColors colors = defaultColors
@ -63,12 +63,12 @@ type RGB struct {
// DefaultColors is the default AnsiColors implementation. // DefaultColors is the default AnsiColors implementation.
type DefaultColors struct { type DefaultColors struct {
accent *Color accent *cachedColor
} }
var ( var (
// Map for color names and their respective foreground [0] or background [1] color codes // Map for color names and their respective foreground [0] or background [1] color codes
ansiColorCodes = map[string][2]AnsiColor{ ansiColorCodes = map[string][2]Color{
"black": {"30", "40"}, "black": {"30", "40"},
"red": {"31", "41"}, "red": {"31", "41"},
"green": {"32", "42"}, "green": {"32", "42"},
@ -94,21 +94,21 @@ const (
backgroundIndex = 1 backgroundIndex = 1
) )
func (d *DefaultColors) AnsiColorFromString(colorString string, isBackground bool) AnsiColor { func (d *DefaultColors) AnsiColorFromString(colorString string, isBackground bool) Color {
if len(colorString) == 0 { if len(colorString) == 0 {
return emptyAnsiColor return emptyColor
} }
if colorString == Transparent { if colorString == Transparent {
return transparentAnsiColor return transparentColor
} }
if colorString == Accent { if colorString == Accent {
if d.accent == nil { if d.accent == nil {
return emptyAnsiColor return emptyColor
} }
if isBackground { if isBackground {
return AnsiColor(d.accent.Background) return Color(d.accent.Background)
} }
return AnsiColor(d.accent.Foreground) return Color(d.accent.Foreground)
} }
colorFromName, err := getAnsiColorFromName(colorString, isBackground) colorFromName, err := getAnsiColorFromName(colorString, isBackground)
if err == nil { if err == nil {
@ -117,25 +117,25 @@ func (d *DefaultColors) AnsiColorFromString(colorString string, isBackground boo
if !strings.HasPrefix(colorString, "#") { if !strings.HasPrefix(colorString, "#") {
val, err := strconv.ParseUint(colorString, 10, 64) val, err := strconv.ParseUint(colorString, 10, 64)
if err != nil || val > 255 { if err != nil || val > 255 {
return emptyAnsiColor return emptyColor
} }
c256 := color.C256(uint8(val), isBackground) c256 := color.C256(uint8(val), isBackground)
return AnsiColor(c256.RGBColor().String()) return Color(c256.RGBColor().String())
} }
style := color.HEX(colorString, isBackground) style := color.HEX(colorString, isBackground)
if !style.IsEmpty() { if !style.IsEmpty() {
return AnsiColor(style.String()) return Color(style.String())
} }
if colorInt, err := strconv.ParseInt(colorString, 10, 8); err == nil { if colorInt, err := strconv.ParseInt(colorString, 10, 8); err == nil {
c := color.C256(uint8(colorInt), isBackground) c := color.C256(uint8(colorInt), isBackground)
return AnsiColor(c.String()) return Color(c.String())
} }
return emptyAnsiColor return emptyColor
} }
// getAnsiColorFromName returns the color code for a given color name if the name is // getAnsiColorFromName returns the color code for a given color name if the name is
// known ANSI color name. // known ANSI color name.
func getAnsiColorFromName(colorName string, isBackground bool) (AnsiColor, error) { func getAnsiColorFromName(colorName string, isBackground bool) (Color, error) {
if colorCodes, found := ansiColorCodes[colorName]; found { if colorCodes, found := ansiColorCodes[colorName]; found {
if isBackground { if isBackground {
return colorCodes[backgroundIndex], nil return colorCodes[backgroundIndex], nil
@ -153,14 +153,14 @@ func IsAnsiColorName(colorString string) bool {
// PaletteColors is the AnsiColors Decorator that uses the Palette to do named color // PaletteColors is the AnsiColors Decorator that uses the Palette to do named color
// lookups before ANSI color code generation. // lookups before ANSI color code generation.
type PaletteColors struct { type PaletteColors struct {
ansiColors AnsiColors ansiColors Colors
palette Palette palette Palette
} }
func (p *PaletteColors) AnsiColorFromString(colorString string, isBackground bool) AnsiColor { func (p *PaletteColors) AnsiColorFromString(colorString string, isBackground bool) Color {
paletteColor, err := p.palette.ResolveColor(colorString) paletteColor, err := p.palette.ResolveColor(colorString)
if err != nil { if err != nil {
return emptyAnsiColor return emptyColor
} }
ansiColor := p.ansiColors.AnsiColorFromString(paletteColor, isBackground) ansiColor := p.ansiColors.AnsiColorFromString(paletteColor, isBackground)
return ansiColor return ansiColor
@ -170,8 +170,8 @@ func (p *PaletteColors) AnsiColorFromString(colorString string, isBackground boo
// AnsiColorFromString calls are cheap, but not free, and having a simple cache in // AnsiColorFromString calls are cheap, but not free, and having a simple cache in
// has measurable positive effect on performance. // has measurable positive effect on performance.
type CachedColors struct { type CachedColors struct {
ansiColors AnsiColors ansiColors Colors
colorCache map[cachedColorKey]AnsiColor colorCache map[cachedColorKey]Color
} }
type cachedColorKey struct { type cachedColorKey struct {
@ -179,9 +179,9 @@ type cachedColorKey struct {
isBackground bool isBackground bool
} }
func (c *CachedColors) AnsiColorFromString(colorString string, isBackground bool) AnsiColor { func (c *CachedColors) AnsiColorFromString(colorString string, isBackground bool) Color {
if c.colorCache == nil { if c.colorCache == nil {
c.colorCache = make(map[cachedColorKey]AnsiColor) c.colorCache = make(map[cachedColorKey]Color)
} }
key := cachedColorKey{colorString, isBackground} key := cachedColorKey{colorString, isBackground}
if ansiColor, hit := c.colorCache[key]; hit { if ansiColor, hit := c.colorCache[key]; hit {

View file

@ -1,4 +1,4 @@
package color package ansi
import ( import (
"testing" "testing"
@ -9,20 +9,20 @@ import (
func TestGetAnsiFromColorString(t *testing.T) { func TestGetAnsiFromColorString(t *testing.T) {
cases := []struct { cases := []struct {
Case string Case string
Expected AnsiColor Expected Color
Color string Color string
Background bool Background bool
}{ }{
{Case: "256 color", Expected: AnsiColor("38;2;135;95;255"), Color: "99", Background: false}, {Case: "256 color", Expected: Color("38;2;135;95;255"), Color: "99", Background: false},
{Case: "256 color", Expected: AnsiColor("38;2;135;255;215"), Color: "122", Background: false}, {Case: "256 color", Expected: Color("38;2;135;255;215"), Color: "122", Background: false},
{Case: "Invalid background", Expected: emptyAnsiColor, Color: "invalid", Background: true}, {Case: "Invalid background", Expected: emptyColor, Color: "invalid", Background: true},
{Case: "Invalid background", Expected: emptyAnsiColor, Color: "invalid", Background: false}, {Case: "Invalid background", Expected: emptyColor, Color: "invalid", Background: false},
{Case: "Hex foreground", Expected: AnsiColor("38;2;170;187;204"), Color: "#AABBCC", Background: false}, {Case: "Hex foreground", Expected: Color("38;2;170;187;204"), Color: "#AABBCC", Background: false},
{Case: "Hex backgrond", Expected: AnsiColor("48;2;170;187;204"), Color: "#AABBCC", Background: true}, {Case: "Hex backgrond", Expected: Color("48;2;170;187;204"), Color: "#AABBCC", Background: true},
{Case: "Base 8 foreground", Expected: AnsiColor("31"), Color: "red", Background: false}, {Case: "Base 8 foreground", Expected: Color("31"), Color: "red", Background: false},
{Case: "Base 8 background", Expected: AnsiColor("41"), Color: "red", Background: true}, {Case: "Base 8 background", Expected: Color("41"), Color: "red", Background: true},
{Case: "Base 16 foreground", Expected: AnsiColor("91"), Color: "lightRed", Background: false}, {Case: "Base 16 foreground", Expected: Color("91"), Color: "lightRed", Background: false},
{Case: "Base 16 backround", Expected: AnsiColor("101"), Color: "lightRed", Background: true}, {Case: "Base 16 backround", Expected: Color("101"), Color: "lightRed", Background: true},
} }
for _, tc := range cases { for _, tc := range cases {
ansiColors := &DefaultColors{} ansiColors := &DefaultColors{}

View file

@ -1,6 +1,6 @@
//go:build !windows //go:build !windows
package color package ansi
import "github.com/jandedobbeleer/oh-my-posh/platform" import "github.com/jandedobbeleer/oh-my-posh/platform"
@ -12,7 +12,7 @@ func (d *DefaultColors) SetAccentColor(env platform.Environment, defaultColor st
if len(defaultColor) == 0 { if len(defaultColor) == 0 {
return return
} }
d.accent = &Color{ d.accent = &cachedColor{
Foreground: string(d.AnsiColorFromString(defaultColor, false)), Foreground: string(d.AnsiColorFromString(defaultColor, false)),
Background: string(d.AnsiColorFromString(defaultColor, true)), Background: string(d.AnsiColorFromString(defaultColor, true)),
} }

View file

@ -1,4 +1,4 @@
package color package ansi
import ( import (
"errors" "errors"
@ -27,7 +27,7 @@ func GetAccentColor(env platform.Environment) (*RGB, error) {
func (d *DefaultColors) SetAccentColor(env platform.Environment, defaultColor string) { func (d *DefaultColors) SetAccentColor(env platform.Environment, defaultColor string) {
rgb, err := GetAccentColor(env) rgb, err := GetAccentColor(env)
if err != nil { if err != nil {
d.accent = &Color{ d.accent = &cachedColor{
Foreground: string(d.AnsiColorFromString(defaultColor, false)), Foreground: string(d.AnsiColorFromString(defaultColor, false)),
Background: string(d.AnsiColorFromString(defaultColor, true)), Background: string(d.AnsiColorFromString(defaultColor, true)),
} }
@ -35,7 +35,7 @@ func (d *DefaultColors) SetAccentColor(env platform.Environment, defaultColor st
} }
foreground := color.RGB(rgb.R, rgb.G, rgb.B, false) foreground := color.RGB(rgb.R, rgb.G, rgb.B, false)
background := color.RGB(rgb.R, rgb.G, rgb.B, true) background := color.RGB(rgb.R, rgb.G, rgb.B, true)
d.accent = &Color{ d.accent = &cachedColor{
Foreground: foreground.String(), Foreground: foreground.String(),
Background: background.String(), Background: background.String(),
} }

View file

@ -1,4 +1,4 @@
package color package ansi
import ( import (
"fmt" "fmt"

View file

@ -1,4 +1,4 @@
package color package ansi
import ( import (
"testing" "testing"

View file

@ -1,4 +1,4 @@
package color package ansi
type Palettes struct { type Palettes struct {
Template string `json:"template,omitempty"` Template string `json:"template,omitempty"`

View file

@ -1,4 +1,4 @@
package color package ansi
import ( import (
"strings" "strings"
@ -12,7 +12,7 @@ func init() { //nolint:gochecknoinits
runewidth.DefaultCondition.EastAsianWidth = false runewidth.DefaultCondition.EastAsianWidth = false
} }
func (a *AnsiWriter) MeasureText(text string) int { func (a *Writer) MeasureText(text string) int {
// skip strings with ANSI // skip strings with ANSI
if !strings.Contains(text, "\x1b") { if !strings.Contains(text, "\x1b") {
text = a.TrimEscapeSequences(text) text = a.TrimEscapeSequences(text)
@ -25,20 +25,20 @@ func (a *AnsiWriter) MeasureText(text string) int {
text = strings.ReplaceAll(text, match["STR"], match["TEXT"]) text = strings.ReplaceAll(text, match["STR"], match["TEXT"])
} }
} }
text = a.TrimAnsi(text) text = a.trimAnsi(text)
text = a.TrimEscapeSequences(text) text = a.TrimEscapeSequences(text)
length := runewidth.StringWidth(text) length := runewidth.StringWidth(text)
return length return length
} }
func (a *AnsiWriter) TrimAnsi(text string) string { func (a *Writer) trimAnsi(text string) string {
if len(text) == 0 || !strings.Contains(text, "\x1b") { if len(text) == 0 || !strings.Contains(text, "\x1b") {
return text return text
} }
return regex.ReplaceAllString(AnsiRegex, text, "") return regex.ReplaceAllString(AnsiRegex, text, "")
} }
func (a *AnsiWriter) TrimEscapeSequences(text string) string { func (a *Writer) TrimEscapeSequences(text string) string {
if len(text) == 0 { if len(text) == 0 {
return text return text
} }

View file

@ -1,4 +1,4 @@
package color package ansi
import ( import (
"fmt" "fmt"
@ -36,7 +36,7 @@ func TestMeasureText(t *testing.T) {
shells := []string{shell.BASH, shell.ZSH, shell.GENERIC} shells := []string{shell.BASH, shell.ZSH, shell.GENERIC}
for _, shell := range shells { for _, shell := range shells {
for _, tc := range cases { for _, tc := range cases {
ansiWriter := &AnsiWriter{} ansiWriter := &Writer{}
ansiWriter.Init(shell) ansiWriter.Init(shell)
tmpl := &template.Text{ tmpl := &template.Text{
Template: tc.Template, Template: tc.Template,

View file

@ -3,7 +3,7 @@ package cli
import ( import (
"fmt" "fmt"
"github.com/jandedobbeleer/oh-my-posh/color" "github.com/jandedobbeleer/oh-my-posh/ansi"
"github.com/jandedobbeleer/oh-my-posh/engine" "github.com/jandedobbeleer/oh-my-posh/engine"
"github.com/jandedobbeleer/oh-my-posh/platform" "github.com/jandedobbeleer/oh-my-posh/platform"
"github.com/jandedobbeleer/oh-my-posh/shell" "github.com/jandedobbeleer/oh-my-posh/shell"
@ -58,7 +58,7 @@ Exports the config to an image file using customized output options.`,
defer env.Close() defer env.Close()
cfg := engine.LoadConfig(env) cfg := engine.LoadConfig(env)
writerColors := cfg.MakeColors() writerColors := cfg.MakeColors()
writer := &color.AnsiWriter{ writer := &ansi.Writer{
TerminalBackground: shell.ConsoleBackgroundColor(env, cfg.TerminalBackground), TerminalBackground: shell.ConsoleBackgroundColor(env, cfg.TerminalBackground),
AnsiColors: writerColors, AnsiColors: writerColors,
} }

View file

@ -4,7 +4,7 @@ import (
"fmt" "fmt"
"time" "time"
"github.com/jandedobbeleer/oh-my-posh/color" "github.com/jandedobbeleer/oh-my-posh/ansi"
"github.com/jandedobbeleer/oh-my-posh/engine" "github.com/jandedobbeleer/oh-my-posh/engine"
"github.com/jandedobbeleer/oh-my-posh/platform" "github.com/jandedobbeleer/oh-my-posh/platform"
"github.com/jandedobbeleer/oh-my-posh/shell" "github.com/jandedobbeleer/oh-my-posh/shell"
@ -33,7 +33,7 @@ var debugCmd = &cobra.Command{
defer env.Close() defer env.Close()
cfg := engine.LoadConfig(env) cfg := engine.LoadConfig(env)
writerColors := cfg.MakeColors() writerColors := cfg.MakeColors()
writer := &color.AnsiWriter{ writer := &ansi.Writer{
TerminalBackground: shell.ConsoleBackgroundColor(env, cfg.TerminalBackground), TerminalBackground: shell.ConsoleBackgroundColor(env, cfg.TerminalBackground),
AnsiColors: writerColors, AnsiColors: writerColors,
} }

View file

@ -5,7 +5,7 @@ import (
"strings" "strings"
"time" "time"
"github.com/jandedobbeleer/oh-my-posh/color" "github.com/jandedobbeleer/oh-my-posh/ansi"
"github.com/jandedobbeleer/oh-my-posh/platform" "github.com/jandedobbeleer/oh-my-posh/platform"
color2 "github.com/gookit/color" color2 "github.com/gookit/color"
@ -52,7 +52,7 @@ This command is used to get the value of the following variables:
case "shell": case "shell":
fmt.Println(env.Shell()) fmt.Println(env.Shell())
case "accent": case "accent":
rgb, err := color.GetAccentColor(env) rgb, err := ansi.GetAccentColor(env)
if err != nil { if err != nil {
fmt.Println("error getting accent color:", err.Error()) fmt.Println("error getting accent color:", err.Error())
return return

View file

@ -1,520 +0,0 @@
package color
import (
"fmt"
"strings"
"github.com/jandedobbeleer/oh-my-posh/regex"
"github.com/jandedobbeleer/oh-my-posh/shell"
"github.com/mattn/go-runewidth"
)
type Writer interface {
Init(shellName string)
Write(background, foreground, text string)
String() (string, int)
SetColors(background, foreground string)
SetParentColors(background, foreground string)
CarriageForward() string
GetCursorForRightWrite(length, offset int) string
ChangeLine(numberOfLines int) string
ConsolePwd(pwdType, userName, hostName, pwd string) string
ClearAfter() string
FormatTitle(title string) string
FormatText(text string) string
SaveCursorPosition() string
RestoreCursorPosition() string
LineBreak() string
TrimAnsi(text string) string
}
var (
knownStyles = []*style{
{AnchorStart: `<b>`, AnchorEnd: `</b>`, Start: "\x1b[1m", End: "\x1b[22m"},
{AnchorStart: `<u>`, AnchorEnd: `</u>`, Start: "\x1b[4m", End: "\x1b[24m"},
{AnchorStart: `<o>`, AnchorEnd: `</o>`, Start: "\x1b[53m", End: "\x1b[55m"},
{AnchorStart: `<i>`, AnchorEnd: `</i>`, Start: "\x1b[3m", End: "\x1b[23m"},
{AnchorStart: `<s>`, AnchorEnd: `</s>`, Start: "\x1b[9m", End: "\x1b[29m"},
{AnchorStart: `<d>`, AnchorEnd: `</d>`, Start: "\x1b[2m", End: "\x1b[22m"},
{AnchorStart: `<f>`, AnchorEnd: `</f>`, Start: "\x1b[5m", End: "\x1b[25m"},
{AnchorStart: `<r>`, AnchorEnd: `</r>`, Start: "\x1b[7m", End: "\x1b[27m"},
}
colorStyle = &style{AnchorStart: "COLOR", AnchorEnd: `</>`, End: "\x1b[0m"}
)
type style struct {
AnchorStart string
AnchorEnd string
Start string
End string
}
type Color struct {
Background string
Foreground string
}
const (
// Transparent implies a transparent color
Transparent = "transparent"
// Accent is the OS accent color
Accent = "accent"
// ParentBackground takes the previous segment's background color
ParentBackground = "parentBackground"
// ParentForeground takes the previous segment's color
ParentForeground = "parentForeground"
// Background takes the current segment's background color
Background = "background"
// Foreground takes the current segment's foreground color
Foreground = "foreground"
anchorRegex = `^(?P<ANCHOR><(?P<FG>[^,>]+)?,?(?P<BG>[^>]+)?>)`
colorise = "\x1b[%sm"
transparent = "\x1b[%s;49m\x1b[7m"
AnsiRegex = "[\u001B\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[a-zA-Z\\d]*)*)?\u0007)|(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PRZcf-ntqry=><~]))"
OSC99 string = "osc99"
OSC7 string = "osc7"
OSC51 string = "osc51"
)
// AnsiWriter writes colorized ANSI strings
type AnsiWriter struct {
TerminalBackground string
Colors *Color
ParentColors []*Color
AnsiColors AnsiColors
Plain bool
builder strings.Builder
length int
foreground AnsiColor
background AnsiColor
currentForeground AnsiColor
currentBackground AnsiColor
runes []rune
shell string
format string
left string
right string
title string
linechange string
clearBelow string
clearLine string
saveCursorPosition string
restoreCursorPosition string
escapeLeft string
escapeRight string
hyperlink string
hyperlinkRegex string
osc99 string
osc7 string
osc51 string
}
func (a *AnsiWriter) Init(shellName string) {
a.shell = shellName
switch a.shell {
case shell.BASH:
a.format = "\\[%s\\]"
a.linechange = "\\[\x1b[%d%s\\]"
a.right = "\\[\x1b[%dC\\]"
a.left = "\\[\x1b[%dD\\]"
a.clearBelow = "\\[\x1b[0J\\]"
a.clearLine = "\\[\x1b[K\\]"
a.saveCursorPosition = "\\[\x1b7\\]"
a.restoreCursorPosition = "\\[\x1b8\\]"
a.title = "\\[\x1b]0;%s\007\\]"
a.escapeLeft = "\\["
a.escapeRight = "\\]"
a.hyperlink = "\\[\x1b]8;;%s\x1b\\\\\\]%s\\[\x1b]8;;\x1b\\\\\\]"
a.hyperlinkRegex = `(?P<STR>\\\[\x1b\]8;;(.+)\x1b\\\\\\\](?P<TEXT>.+)\\\[\x1b\]8;;\x1b\\\\\\\])`
a.osc99 = "\\[\x1b]9;9;\"%s\"\x1b\\\\\\]"
a.osc7 = "\\[\x1b]7;\"file://%s/%s\"\x1b\\\\\\]"
a.osc51 = "\\[\x1b]51;A;%s@%s:%s\x1b\\\\\\]"
case "zsh":
a.format = "%%{%s%%}"
a.linechange = "%%{\x1b[%d%s%%}"
a.right = "%%{\x1b[%dC%%}"
a.left = "%%{\x1b[%dD%%}"
a.clearBelow = "%{\x1b[0J%}"
a.clearLine = "%{\x1b[K%}"
a.saveCursorPosition = "%{\x1b7%}"
a.restoreCursorPosition = "%{\x1b8%}"
a.title = "%%{\x1b]0;%s\007%%}"
a.escapeLeft = "%{"
a.escapeRight = "%}"
a.hyperlink = "%%{\x1b]8;;%s\x1b\\%%}%s%%{\x1b]8;;\x1b\\%%}"
a.hyperlinkRegex = `(?P<STR>%{\x1b]8;;(.+)\x1b\\%}(?P<TEXT>.+)%{\x1b]8;;\x1b\\%})`
a.osc99 = "%%{\x1b]9;9;\"%s\"\x1b\\%%}"
a.osc7 = "%%{\x1b]7;file:\"//%s/%s\"\x1b\\%%}"
a.osc51 = "%%{\x1b]51;A%s@%s:%s\x1b\\%%}"
default:
a.linechange = "\x1b[%d%s"
a.right = "\x1b[%dC"
a.left = "\x1b[%dD"
a.clearBelow = "\x1b[0J"
a.clearLine = "\x1b[K"
a.saveCursorPosition = "\x1b7"
a.restoreCursorPosition = "\x1b8"
a.title = "\x1b]0;%s\007"
// when in fish on Linux, it seems hyperlinks ending with \\ print a \
// unlike on macOS. However, this is a fish bug, so do not try to fix it here:
// https://github.com/JanDeDobbeleer/oh-my-posh/pull/3288#issuecomment-1369137068
a.hyperlink = "\x1b]8;;%s\x1b\\%s\x1b]8;;\x1b\\"
a.hyperlinkRegex = "(?P<STR>\x1b]8;;(.+)\x1b\\\\\\\\?(?P<TEXT>.+)\x1b]8;;\x1b\\\\)"
a.osc99 = "\x1b]9;9;\"%s\"\x1b\\"
a.osc7 = "\x1b]7;\"file://%s/%s\"\x1b\\"
a.osc51 = "\x1b]51;A%s@%s:%s\x1b\\"
}
}
func (a *AnsiWriter) SetColors(background, foreground string) {
a.Colors = &Color{
Background: background,
Foreground: foreground,
}
}
func (a *AnsiWriter) SetParentColors(background, foreground string) {
if a.ParentColors == nil {
a.ParentColors = make([]*Color, 0)
}
a.ParentColors = append([]*Color{{
Background: background,
Foreground: foreground,
}}, a.ParentColors...)
}
func (a *AnsiWriter) CarriageForward() string {
return fmt.Sprintf(a.right, 1000)
}
func (a *AnsiWriter) GetCursorForRightWrite(length, offset int) string {
strippedLen := length + (-offset)
return fmt.Sprintf(a.left, strippedLen)
}
func (a *AnsiWriter) ChangeLine(numberOfLines int) string {
if a.Plain {
return ""
}
position := "B"
if numberOfLines < 0 {
position = "F"
numberOfLines = -numberOfLines
}
return fmt.Sprintf(a.linechange, numberOfLines, position)
}
func (a *AnsiWriter) ConsolePwd(pwdType, userName, hostName, pwd string) string {
if a.Plain {
return ""
}
if strings.HasSuffix(pwd, ":") {
pwd += "\\"
}
switch pwdType {
case OSC7:
return fmt.Sprintf(a.osc7, hostName, pwd)
case OSC51:
return fmt.Sprintf(a.osc51, userName, hostName, pwd)
case OSC99:
fallthrough
default:
return fmt.Sprintf(a.osc99, pwd)
}
}
func (a *AnsiWriter) ClearAfter() string {
if a.Plain {
return ""
}
return a.clearLine + a.clearBelow
}
func (a *AnsiWriter) FormatTitle(title string) string {
title = a.TrimAnsi(title)
// we have to do this to prevent bash/zsh from misidentifying escape sequences
switch a.shell {
case shell.BASH:
title = strings.NewReplacer("`", "\\`", `\`, `\\`).Replace(title)
case shell.ZSH:
title = strings.NewReplacer("`", "\\`", `%`, `%%`).Replace(title)
}
return fmt.Sprintf(a.title, title)
}
func (a *AnsiWriter) FormatText(text string) string {
return fmt.Sprintf(a.format, text)
}
func (a *AnsiWriter) SaveCursorPosition() string {
return a.saveCursorPosition
}
func (a *AnsiWriter) RestoreCursorPosition() string {
return a.restoreCursorPosition
}
func (a *AnsiWriter) LineBreak() string {
cr := fmt.Sprintf(a.left, 1000)
lf := fmt.Sprintf(a.linechange, 1, "B")
return cr + lf
}
func (a *AnsiWriter) Write(background, foreground, text string) {
if len(text) == 0 {
return
}
if !a.Plain {
text = a.GenerateHyperlink(text)
}
a.background, a.foreground = a.asAnsiColors(background, foreground)
// default to white foreground
if a.foreground.IsEmpty() {
a.foreground = a.AnsiColors.AnsiColorFromString("white", false)
}
// validate if we start with a color override
match := regex.FindNamedRegexMatch(anchorRegex, text)
if len(match) != 0 {
colorOverride := true
for _, style := range knownStyles {
if match["ANCHOR"] != style.AnchorStart {
continue
}
a.printEscapedAnsiString(style.Start)
colorOverride = false
}
if colorOverride {
a.currentBackground, a.currentForeground = a.asAnsiColors(match["BG"], match["FG"])
}
}
a.writeSegmentColors()
text = text[len(match["ANCHOR"]):]
a.runes = []rune(text)
for i := 0; i < len(a.runes); i++ {
s := a.runes[i]
// ignore everything which isn't overriding
if s != '<' {
a.length += runewidth.RuneWidth(s)
a.builder.WriteRune(s)
continue
}
// color/end overrides first
text = string(a.runes[i:])
match = regex.FindNamedRegexMatch(anchorRegex, text)
if len(match) > 0 {
i = a.writeColorOverrides(match, background, i)
continue
}
a.length += runewidth.RuneWidth(s)
a.builder.WriteRune(s)
}
a.printEscapedAnsiString(colorStyle.End)
// reset current
a.currentBackground = ""
a.currentForeground = ""
}
func (a *AnsiWriter) printEscapedAnsiString(text string) {
if a.Plain {
return
}
if len(a.format) == 0 {
a.builder.WriteString(text)
return
}
a.builder.WriteString(fmt.Sprintf(a.format, text))
}
func (a *AnsiWriter) getAnsiFromColorString(colorString string, isBackground bool) AnsiColor {
return a.AnsiColors.AnsiColorFromString(colorString, isBackground)
}
func (a *AnsiWriter) writeSegmentColors() {
// use correct starting colors
bg := a.background
fg := a.foreground
if !a.currentBackground.IsEmpty() {
bg = a.currentBackground
}
if !a.currentForeground.IsEmpty() {
fg = a.currentForeground
}
if fg.IsTransparent() && len(a.TerminalBackground) != 0 {
background := a.getAnsiFromColorString(a.TerminalBackground, false)
a.printEscapedAnsiString(fmt.Sprintf(colorise, background))
a.printEscapedAnsiString(fmt.Sprintf(colorise, bg.ToForeground()))
} else if fg.IsTransparent() && !bg.IsEmpty() {
a.printEscapedAnsiString(fmt.Sprintf(transparent, bg))
} else {
if !bg.IsEmpty() && !bg.IsTransparent() {
a.printEscapedAnsiString(fmt.Sprintf(colorise, bg))
}
if !fg.IsEmpty() {
a.printEscapedAnsiString(fmt.Sprintf(colorise, fg))
}
}
// set current colors
a.currentBackground = bg
a.currentForeground = fg
}
func (a *AnsiWriter) writeColorOverrides(match map[string]string, background string, i int) (position int) {
position = i
// check color reset first
if match["ANCHOR"] == colorStyle.AnchorEnd {
// make sure to reset the colors if needed
position += len([]rune(colorStyle.AnchorEnd)) - 1
// do not restore colors at the end of the string, we print it anyways
if position == len(a.runes)-1 {
return
}
if a.currentBackground != a.background {
a.printEscapedAnsiString(fmt.Sprintf(colorise, a.background))
}
if a.currentForeground != a.foreground {
a.printEscapedAnsiString(fmt.Sprintf(colorise, a.foreground))
}
return
}
position += len([]rune(match["ANCHOR"])) - 1
for _, style := range knownStyles {
if style.AnchorEnd == match["ANCHOR"] {
a.printEscapedAnsiString(style.End)
return
}
if style.AnchorStart == match["ANCHOR"] {
a.printEscapedAnsiString(style.Start)
return
}
}
if match["FG"] == Transparent && len(match["BG"]) == 0 {
match["BG"] = background
}
a.currentBackground, a.currentForeground = a.asAnsiColors(match["BG"], match["FG"])
// make sure we have colors
if a.currentForeground.IsEmpty() {
a.currentForeground = a.foreground
}
if a.currentBackground.IsEmpty() {
a.currentBackground = a.background
}
if a.currentForeground.IsTransparent() && len(a.TerminalBackground) != 0 {
background := a.getAnsiFromColorString(a.TerminalBackground, false)
a.printEscapedAnsiString(fmt.Sprintf(colorise, background))
a.printEscapedAnsiString(fmt.Sprintf(colorise, a.currentBackground.ToForeground()))
return
}
if a.currentForeground.IsTransparent() && !a.currentBackground.IsTransparent() {
a.printEscapedAnsiString(fmt.Sprintf(transparent, a.currentBackground))
return
}
if a.currentBackground != a.background {
// end the colors in case we have a transparent background
if a.currentBackground.IsTransparent() {
a.printEscapedAnsiString(colorStyle.End)
} else {
a.printEscapedAnsiString(fmt.Sprintf(colorise, a.currentBackground))
}
}
if a.currentForeground != a.foreground || a.currentBackground.IsTransparent() {
a.printEscapedAnsiString(fmt.Sprintf(colorise, a.currentForeground))
}
return position
}
func (a *AnsiWriter) asAnsiColors(background, foreground string) (AnsiColor, AnsiColor) {
background = a.expandKeyword(background)
foreground = a.expandKeyword(foreground)
inverted := foreground == Transparent && len(background) != 0
backgroundAnsi := a.getAnsiFromColorString(background, !inverted)
foregroundAnsi := a.getAnsiFromColorString(foreground, false)
return backgroundAnsi, foregroundAnsi
}
func (a *AnsiWriter) isKeyword(color string) bool {
switch color {
case Transparent, ParentBackground, ParentForeground, Background, Foreground:
return true
default:
return false
}
}
func (a *AnsiWriter) expandKeyword(keyword string) string {
resolveParentColor := func(keyword string) string {
for _, color := range a.ParentColors {
if color == nil {
return Transparent
}
switch keyword {
case ParentBackground:
keyword = color.Background
case ParentForeground:
keyword = color.Foreground
default:
if len(keyword) == 0 {
return Transparent
}
return keyword
}
}
if len(keyword) == 0 {
return Transparent
}
return keyword
}
resolveKeyword := func(keyword string) string {
switch {
case keyword == Background && a.Colors != nil:
return a.Colors.Background
case keyword == Foreground && a.Colors != nil:
return a.Colors.Foreground
case (keyword == ParentBackground || keyword == ParentForeground) && a.ParentColors != nil:
return resolveParentColor(keyword)
default:
return Transparent
}
}
for ok := a.isKeyword(keyword); ok; ok = a.isKeyword(keyword) {
resolved := resolveKeyword(keyword)
if resolved == keyword {
break
}
keyword = resolved
}
return keyword
}
func (a *AnsiWriter) String() (string, int) {
defer func() {
a.length = 0
a.builder.Reset()
}()
return a.builder.String(), a.length
}

View file

@ -4,7 +4,7 @@ import (
"sync" "sync"
"time" "time"
"github.com/jandedobbeleer/oh-my-posh/color" "github.com/jandedobbeleer/oh-my-posh/ansi"
"github.com/jandedobbeleer/oh-my-posh/platform" "github.com/jandedobbeleer/oh-my-posh/platform"
"github.com/jandedobbeleer/oh-my-posh/shell" "github.com/jandedobbeleer/oh-my-posh/shell"
) )
@ -52,19 +52,19 @@ type Block struct {
MinWidth int `json:"min_width,omitempty"` MinWidth int `json:"min_width,omitempty"`
env platform.Environment env platform.Environment
writer color.Writer writer *ansi.Writer
activeSegment *Segment activeSegment *Segment
previousActiveSegment *Segment previousActiveSegment *Segment
} }
func (b *Block) Init(env platform.Environment, writer color.Writer) { func (b *Block) Init(env platform.Environment, writer *ansi.Writer) {
b.env = env b.env = env
b.writer = writer b.writer = writer
b.executeSegmentLogic() b.executeSegmentLogic()
} }
func (b *Block) InitPlain(env platform.Environment, config *Config) { func (b *Block) InitPlain(env platform.Environment, config *Config) {
b.writer = &color.AnsiWriter{ b.writer = &ansi.Writer{
TerminalBackground: shell.ConsoleBackgroundColor(env, config.TerminalBackground), TerminalBackground: shell.ConsoleBackgroundColor(env, config.TerminalBackground),
AnsiColors: config.MakeColors(), AnsiColors: config.MakeColors(),
} }
@ -141,14 +141,14 @@ func (b *Block) renderActiveSegment() {
b.writePowerline(false) b.writePowerline(false)
switch b.activeSegment.style() { switch b.activeSegment.style() {
case Plain, Powerline: case Plain, Powerline:
b.writer.Write(color.Background, color.Foreground, b.activeSegment.text) b.writer.Write(ansi.Background, ansi.Foreground, b.activeSegment.text)
case Diamond: case Diamond:
b.writer.Write(color.Transparent, color.Background, b.activeSegment.LeadingDiamond) b.writer.Write(ansi.Transparent, ansi.Background, b.activeSegment.LeadingDiamond)
b.writer.Write(color.Background, color.Foreground, b.activeSegment.text) b.writer.Write(ansi.Background, ansi.Foreground, b.activeSegment.text)
b.writer.Write(color.Transparent, color.Background, b.activeSegment.TrailingDiamond) b.writer.Write(ansi.Transparent, ansi.Background, b.activeSegment.TrailingDiamond)
case Accordion: case Accordion:
if b.activeSegment.Enabled { if b.activeSegment.Enabled {
b.writer.Write(color.Background, color.Foreground, b.activeSegment.text) b.writer.Write(ansi.Background, ansi.Foreground, b.activeSegment.text)
} }
} }
b.previousActiveSegment = b.activeSegment b.previousActiveSegment = b.activeSegment
@ -169,12 +169,12 @@ func (b *Block) writePowerline(final bool) {
if len(symbol) == 0 { if len(symbol) == 0 {
return return
} }
bgColor := color.Background bgColor := ansi.Background
if final || !b.activeSegment.isPowerline() { if final || !b.activeSegment.isPowerline() {
bgColor = color.Transparent bgColor = ansi.Transparent
} }
if b.activeSegment.style() == Diamond && len(b.activeSegment.LeadingDiamond) == 0 { if b.activeSegment.style() == Diamond && len(b.activeSegment.LeadingDiamond) == 0 {
bgColor = color.Background bgColor = ansi.Background
} }
if b.activeSegment.InvertPowerline { if b.activeSegment.InvertPowerline {
b.writer.Write(b.getPowerlineColor(), bgColor, symbol) b.writer.Write(b.getPowerlineColor(), bgColor, symbol)
@ -185,7 +185,7 @@ func (b *Block) writePowerline(final bool) {
func (b *Block) getPowerlineColor() string { func (b *Block) getPowerlineColor() string {
if b.previousActiveSegment == nil { if b.previousActiveSegment == nil {
return color.Transparent return ansi.Transparent
} }
if b.previousActiveSegment.style() == Diamond && len(b.previousActiveSegment.TrailingDiamond) == 0 { if b.previousActiveSegment.style() == Diamond && len(b.previousActiveSegment.TrailingDiamond) == 0 {
return b.previousActiveSegment.background() return b.previousActiveSegment.background()
@ -194,7 +194,7 @@ func (b *Block) getPowerlineColor() string {
return b.previousActiveSegment.background() return b.previousActiveSegment.background()
} }
if !b.previousActiveSegment.isPowerline() { if !b.previousActiveSegment.isPowerline() {
return color.Transparent return ansi.Transparent
} }
return b.previousActiveSegment.background() return b.previousActiveSegment.background()
} }

View file

@ -10,7 +10,7 @@ import (
"strings" "strings"
"time" "time"
"github.com/jandedobbeleer/oh-my-posh/color" "github.com/jandedobbeleer/oh-my-posh/ansi"
"github.com/jandedobbeleer/oh-my-posh/platform" "github.com/jandedobbeleer/oh-my-posh/platform"
"github.com/jandedobbeleer/oh-my-posh/properties" "github.com/jandedobbeleer/oh-my-posh/properties"
"github.com/jandedobbeleer/oh-my-posh/segments" "github.com/jandedobbeleer/oh-my-posh/segments"
@ -33,21 +33,21 @@ const (
// Config holds all the theme for rendering the prompt // Config holds all the theme for rendering the prompt
type Config struct { type Config struct {
Version int `json:"version"` Version int `json:"version"`
FinalSpace bool `json:"final_space,omitempty"` FinalSpace bool `json:"final_space,omitempty"`
ConsoleTitleTemplate string `json:"console_title_template,omitempty"` ConsoleTitleTemplate string `json:"console_title_template,omitempty"`
TerminalBackground string `json:"terminal_background,omitempty"` TerminalBackground string `json:"terminal_background,omitempty"`
AccentColor string `json:"accent_color,omitempty"` AccentColor string `json:"accent_color,omitempty"`
Blocks []*Block `json:"blocks,omitempty"` Blocks []*Block `json:"blocks,omitempty"`
Tooltips []*Segment `json:"tooltips,omitempty"` Tooltips []*Segment `json:"tooltips,omitempty"`
TransientPrompt *Segment `json:"transient_prompt,omitempty"` TransientPrompt *Segment `json:"transient_prompt,omitempty"`
ValidLine *Segment `json:"valid_line,omitempty"` ValidLine *Segment `json:"valid_line,omitempty"`
ErrorLine *Segment `json:"error_line,omitempty"` ErrorLine *Segment `json:"error_line,omitempty"`
SecondaryPrompt *Segment `json:"secondary_prompt,omitempty"` SecondaryPrompt *Segment `json:"secondary_prompt,omitempty"`
DebugPrompt *Segment `json:"debug_prompt,omitempty"` DebugPrompt *Segment `json:"debug_prompt,omitempty"`
Palette color.Palette `json:"palette,omitempty"` Palette ansi.Palette `json:"palette,omitempty"`
Palettes *color.Palettes `json:"palettes,omitempty"` Palettes *ansi.Palettes `json:"palettes,omitempty"`
PWD string `json:"pwd,omitempty"` PWD string `json:"pwd,omitempty"`
// Deprecated // Deprecated
OSC99 bool `json:"osc99,omitempty"` OSC99 bool `json:"osc99,omitempty"`
@ -63,12 +63,12 @@ type Config struct {
// MakeColors creates instance of AnsiColors to use in AnsiWriter according to // MakeColors creates instance of AnsiColors to use in AnsiWriter according to
// environment and configuration. // environment and configuration.
func (cfg *Config) MakeColors() color.AnsiColors { func (cfg *Config) MakeColors() ansi.Colors {
cacheDisabled := cfg.env.Getenv("OMP_CACHE_DISABLED") == "1" cacheDisabled := cfg.env.Getenv("OMP_CACHE_DISABLED") == "1"
return color.MakeColors(cfg.getPalette(), !cacheDisabled, cfg.AccentColor, cfg.env) return ansi.MakeColors(cfg.getPalette(), !cacheDisabled, cfg.AccentColor, cfg.env)
} }
func (cfg *Config) getPalette() color.Palette { func (cfg *Config) getPalette() ansi.Palette {
if cfg.Palettes == nil { if cfg.Palettes == nil {
return cfg.Palette return cfg.Palette
} }
@ -387,7 +387,7 @@ func defaultConfig(warning bool) *Config {
}, },
}, },
ConsoleTitleTemplate: "{{ .Shell }} in {{ .Folder }}", ConsoleTitleTemplate: "{{ .Shell }} in {{ .Folder }}",
Palette: color.Palette{ Palette: ansi.Palette{
"black": "#262B44", "black": "#262B44",
"blue": "#4B95E9", "blue": "#4B95E9",
"green": "#59C9A5", "green": "#59C9A5",

View file

@ -3,7 +3,7 @@ package engine
import ( import (
"testing" "testing"
"github.com/jandedobbeleer/oh-my-posh/color" "github.com/jandedobbeleer/oh-my-posh/ansi"
"github.com/jandedobbeleer/oh-my-posh/mock" "github.com/jandedobbeleer/oh-my-posh/mock"
"github.com/jandedobbeleer/oh-my-posh/platform" "github.com/jandedobbeleer/oh-my-posh/platform"
"github.com/jandedobbeleer/oh-my-posh/segments" "github.com/jandedobbeleer/oh-my-posh/segments"
@ -61,21 +61,21 @@ func TestEscapeGlyphs(t *testing.T) {
} }
func TestGetPalette(t *testing.T) { func TestGetPalette(t *testing.T) {
palette := color.Palette{ palette := ansi.Palette{
"red": "#ff0000", "red": "#ff0000",
"blue": "#0000ff", "blue": "#0000ff",
} }
cases := []struct { cases := []struct {
Case string Case string
Palettes *color.Palettes Palettes *ansi.Palettes
Palette color.Palette Palette ansi.Palette
ExpectedPalette color.Palette ExpectedPalette ansi.Palette
}{ }{
{ {
Case: "match", Case: "match",
Palettes: &color.Palettes{ Palettes: &ansi.Palettes{
Template: "{{ .Shell }}", Template: "{{ .Shell }}",
List: map[string]color.Palette{ List: map[string]ansi.Palette{
"bash": palette, "bash": palette,
"zsh": { "zsh": {
"red": "#ff0001", "red": "#ff0001",
@ -87,9 +87,9 @@ func TestGetPalette(t *testing.T) {
}, },
{ {
Case: "no match, no fallback", Case: "no match, no fallback",
Palettes: &color.Palettes{ Palettes: &ansi.Palettes{
Template: "{{ .Shell }}", Template: "{{ .Shell }}",
List: map[string]color.Palette{ List: map[string]ansi.Palette{
"fish": palette, "fish": palette,
"zsh": { "zsh": {
"red": "#ff0001", "red": "#ff0001",
@ -101,9 +101,9 @@ func TestGetPalette(t *testing.T) {
}, },
{ {
Case: "no match, default", Case: "no match, default",
Palettes: &color.Palettes{ Palettes: &ansi.Palettes{
Template: "{{ .Shell }}", Template: "{{ .Shell }}",
List: map[string]color.Palette{ List: map[string]ansi.Palette{
"zsh": { "zsh": {
"red": "#ff0001", "red": "#ff0001",
"blue": "#0000fb", "blue": "#0000fb",

View file

@ -5,7 +5,7 @@ import (
"strings" "strings"
"time" "time"
"github.com/jandedobbeleer/oh-my-posh/color" "github.com/jandedobbeleer/oh-my-posh/ansi"
"github.com/jandedobbeleer/oh-my-posh/platform" "github.com/jandedobbeleer/oh-my-posh/platform"
"github.com/jandedobbeleer/oh-my-posh/shell" "github.com/jandedobbeleer/oh-my-posh/shell"
"github.com/jandedobbeleer/oh-my-posh/template" "github.com/jandedobbeleer/oh-my-posh/template"
@ -14,7 +14,7 @@ import (
type Engine struct { type Engine struct {
Config *Config Config *Config
Env platform.Environment Env platform.Environment
Writer color.Writer Writer *ansi.Writer
Plain bool Plain bool
console strings.Builder console strings.Builder
@ -78,7 +78,7 @@ func (e *Engine) printPWD() {
cwd := e.Env.Pwd() cwd := e.Env.Pwd()
// Backwards compatibility for deprecated OSC99 // Backwards compatibility for deprecated OSC99
if e.Config.OSC99 { if e.Config.OSC99 {
e.write(e.Writer.ConsolePwd(color.OSC99, "", "", cwd)) e.write(e.Writer.ConsolePwd(ansi.OSC99, "", "", cwd))
return return
} }
// Allow template logic to define when to enable the PWD (when supported) // Allow template logic to define when to enable the PWD (when supported)
@ -304,7 +304,7 @@ func (e *Engine) print() string {
} }
// in bash, the entire rprompt needs to be escaped for the prompt to be interpreted correctly // in bash, the entire rprompt needs to be escaped for the prompt to be interpreted correctly
// see https://github.com/jandedobbeleer/oh-my-posh/pull/2398 // see https://github.com/jandedobbeleer/oh-my-posh/pull/2398
writer := &color.AnsiWriter{} writer := &ansi.Writer{}
writer.Init(shell.GENERIC) writer.Init(shell.GENERIC)
prompt := writer.SaveCursorPosition() prompt := writer.SaveCursorPosition()
prompt += writer.CarriageForward() prompt += writer.CarriageForward()

View file

@ -4,7 +4,7 @@ import (
"errors" "errors"
"testing" "testing"
"github.com/jandedobbeleer/oh-my-posh/color" "github.com/jandedobbeleer/oh-my-posh/ansi"
"github.com/jandedobbeleer/oh-my-posh/mock" "github.com/jandedobbeleer/oh-my-posh/mock"
"github.com/jandedobbeleer/oh-my-posh/platform" "github.com/jandedobbeleer/oh-my-posh/platform"
"github.com/jandedobbeleer/oh-my-posh/shell" "github.com/jandedobbeleer/oh-my-posh/shell"
@ -52,9 +52,9 @@ func TestPrintPWD(t *testing.T) {
OSC99 bool OSC99 bool
}{ }{
{Case: "Empty PWD"}, {Case: "Empty PWD"},
{Case: "OSC99", PWD: color.OSC99, Expected: "\x1b]9;9;\"pwd\"\x1b\\"}, {Case: "OSC99", PWD: ansi.OSC99, Expected: "\x1b]9;9;\"pwd\"\x1b\\"},
{Case: "OSC7", PWD: color.OSC7, Expected: "\x1b]7;\"file://host/pwd\"\x1b\\"}, {Case: "OSC7", PWD: ansi.OSC7, Expected: "\x1b]7;\"file://host/pwd\"\x1b\\"},
{Case: "OSC51", PWD: color.OSC51, Expected: "\x1b]51;Auser@host:pwd\x1b\\"}, {Case: "OSC51", PWD: ansi.OSC51, Expected: "\x1b]51;Auser@host:pwd\x1b\\"},
{Case: "Deprecated OSC99", OSC99: true, Expected: "\x1b]9;9;\"pwd\"\x1b\\"}, {Case: "Deprecated OSC99", OSC99: true, Expected: "\x1b]9;9;\"pwd\"\x1b\\"},
{Case: "Template (empty)", PWD: "{{ if eq .Shell \"pwsh\" }}osc7{{ end }}"}, {Case: "Template (empty)", PWD: "{{ if eq .Shell \"pwsh\" }}osc7{{ end }}"},
{Case: "Template (non empty)", PWD: "{{ if eq .Shell \"shell\" }}osc7{{ end }}", Expected: "\x1b]7;\"file://host/pwd\"\x1b\\"}, {Case: "Template (non empty)", PWD: "{{ if eq .Shell \"shell\" }}osc7{{ end }}", Expected: "\x1b]7;\"file://host/pwd\"\x1b\\"},
@ -71,7 +71,7 @@ func TestPrintPWD(t *testing.T) {
Shell: "shell", Shell: "shell",
}) })
writer := &color.AnsiWriter{} writer := &ansi.Writer{}
writer.Init(shell.GENERIC) writer.Init(shell.GENERIC)
engine := &Engine{ engine := &Engine{
Env: env, Env: env,
@ -102,7 +102,7 @@ func engineRender() {
defer testClearDefaultConfig() defer testClearDefaultConfig()
writerColors := cfg.MakeColors() writerColors := cfg.MakeColors()
writer := &color.AnsiWriter{ writer := &ansi.Writer{
TerminalBackground: shell.ConsoleBackgroundColor(env, cfg.TerminalBackground), TerminalBackground: shell.ConsoleBackgroundColor(env, cfg.TerminalBackground),
AnsiColors: writerColors, AnsiColors: writerColors,
} }
@ -173,17 +173,17 @@ func TestGetTitle(t *testing.T) {
PWD: tc.Cwd, PWD: tc.Cwd,
Folder: "vagrant", Folder: "vagrant",
}) })
ansi := &color.AnsiWriter{} writer := &ansi.Writer{}
ansi.Init(shell.GENERIC) writer.Init(shell.GENERIC)
engine := &Engine{ engine := &Engine{
Config: &Config{ Config: &Config{
ConsoleTitleTemplate: tc.Template, ConsoleTitleTemplate: tc.Template,
}, },
Writer: ansi, Writer: writer,
Env: env, Env: env,
} }
title := engine.getTitleTemplateText() title := engine.getTitleTemplateText()
got := ansi.FormatTitle(title) got := writer.FormatTitle(title)
assert.Equal(t, tc.Expected, got) assert.Equal(t, tc.Expected, got)
} }
} }
@ -231,17 +231,17 @@ func TestGetConsoleTitleIfGethostnameReturnsError(t *testing.T) {
Root: tc.Root, Root: tc.Root,
HostName: "", HostName: "",
}) })
ansi := &color.AnsiWriter{} writer := &ansi.Writer{}
ansi.Init(shell.GENERIC) writer.Init(shell.GENERIC)
engine := &Engine{ engine := &Engine{
Config: &Config{ Config: &Config{
ConsoleTitleTemplate: tc.Template, ConsoleTitleTemplate: tc.Template,
}, },
Writer: ansi, Writer: writer,
Env: env, Env: env,
} }
title := engine.getTitleTemplateText() title := engine.getTitleTemplateText()
got := ansi.FormatTitle(title) got := writer.FormatTitle(title)
assert.Equal(t, tc.Expected, got) assert.Equal(t, tc.Expected, got)
} }
} }

View file

@ -30,7 +30,7 @@ import (
"strings" "strings"
"unicode/utf8" "unicode/utf8"
"github.com/jandedobbeleer/oh-my-posh/color" "github.com/jandedobbeleer/oh-my-posh/ansi"
"github.com/jandedobbeleer/oh-my-posh/regex" "github.com/jandedobbeleer/oh-my-posh/regex"
"github.com/esimov/stackblur-go" "github.com/esimov/stackblur-go"
@ -110,7 +110,7 @@ type ImageRenderer struct {
CursorPadding int CursorPadding int
RPromptOffset int RPromptOffset int
BgColor string BgColor string
Ansi *color.AnsiWriter Ansi *ansi.Writer
Path string Path string
@ -278,7 +278,7 @@ func (ir *ImageRenderer) lenWithoutANSI(text string) int {
for _, match := range matches { for _, match := range matches {
text = strings.ReplaceAll(text, match[str], "") text = strings.ReplaceAll(text, match[str], "")
} }
stripped := regex.ReplaceAllString(color.AnsiRegex, text, "") stripped := regex.ReplaceAllString(ansi.AnsiRegex, text, "")
length := utf8.RuneCountInString(stripped) length := utf8.RuneCountInString(stripped)
for _, rune := range stripped { for _, rune := range stripped {
length += ir.runeAdditionalWidth(rune) length += ir.runeAdditionalWidth(rune)

View file

@ -6,7 +6,7 @@ import (
"path/filepath" "path/filepath"
"testing" "testing"
"github.com/jandedobbeleer/oh-my-posh/color" "github.com/jandedobbeleer/oh-my-posh/ansi"
"github.com/jandedobbeleer/oh-my-posh/shell" "github.com/jandedobbeleer/oh-my-posh/shell"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@ -33,11 +33,11 @@ func runImageTest(config, content string) (string, error) {
return "", err return "", err
} }
defer os.Remove(file.Name()) defer os.Remove(file.Name())
ansi := &color.AnsiWriter{} writer := &ansi.Writer{}
ansi.Init(shell.GENERIC) writer.Init(shell.GENERIC)
image := &ImageRenderer{ image := &ImageRenderer{
AnsiString: content, AnsiString: content,
Ansi: ansi, Ansi: writer,
} }
image.Init(config) image.Init(config)
err = image.SavePNG() err = image.SavePNG()

View file

@ -1,7 +1,7 @@
package engine package engine
import ( import (
"github.com/jandedobbeleer/oh-my-posh/color" "github.com/jandedobbeleer/oh-my-posh/ansi"
"github.com/jandedobbeleer/oh-my-posh/platform" "github.com/jandedobbeleer/oh-my-posh/platform"
"github.com/jandedobbeleer/oh-my-posh/shell" "github.com/jandedobbeleer/oh-my-posh/shell"
) )
@ -17,7 +17,7 @@ func New(flags *platform.Flags) *Engine {
env.Init() env.Init()
cfg := LoadConfig(env) cfg := LoadConfig(env)
ansiWriter := &color.AnsiWriter{ ansiWriter := &ansi.Writer{
TerminalBackground: shell.ConsoleBackgroundColor(env, cfg.TerminalBackground), TerminalBackground: shell.ConsoleBackgroundColor(env, cfg.TerminalBackground),
AnsiColors: cfg.MakeColors(), AnsiColors: cfg.MakeColors(),
Plain: flags.Plain, Plain: flags.Plain,

View file

@ -3,7 +3,7 @@ package properties
import ( import (
"fmt" "fmt"
"github.com/jandedobbeleer/oh-my-posh/color" "github.com/jandedobbeleer/oh-my-posh/ansi"
"github.com/jandedobbeleer/oh-my-posh/regex" "github.com/jandedobbeleer/oh-my-posh/regex"
) )
@ -70,7 +70,7 @@ func (m Map) GetColor(property Property, defaultValue string) string {
return defaultValue return defaultValue
} }
colorString := fmt.Sprint(val) colorString := fmt.Sprint(val)
if color.IsAnsiColorName(colorString) { if ansi.IsAnsiColorName(colorString) {
return colorString return colorString
} }
values := regex.FindNamedRegexMatch(`(?P<color>#[A-Fa-f0-9]{6}|[A-Fa-f0-9]{3}|p:.*)`, colorString) values := regex.FindNamedRegexMatch(`(?P<color>#[A-Fa-f0-9]{6}|[A-Fa-f0-9]{3}|p:.*)`, colorString)