refactor(ansi): rewrite ansi and writer

This commit is contained in:
Jan De Dobbeleer 2023-01-03 12:21:27 +01:00 committed by Jan De Dobbeleer
parent e957e5f8cc
commit 005445b9fe
22 changed files with 970 additions and 1041 deletions

View file

@ -4,7 +4,6 @@ import (
"fmt"
"github.com/jandedobbeleer/oh-my-posh/color"
"github.com/jandedobbeleer/oh-my-posh/console"
"github.com/jandedobbeleer/oh-my-posh/engine"
"github.com/jandedobbeleer/oh-my-posh/platform"
"github.com/jandedobbeleer/oh-my-posh/shell"
@ -52,31 +51,21 @@ Exports the config to an image file using customized output options.`,
Version: cliVersion,
CmdFlags: &platform.Flags{
Config: config,
Shell: shell.PLAIN,
Shell: shell.GENERIC,
},
}
env.Init()
defer env.Close()
cfg := engine.LoadConfig(env)
ansi := &color.Ansi{}
ansi.InitPlain()
writerColors := cfg.MakeColors()
writer := &color.AnsiWriter{
Ansi: ansi,
TerminalBackground: shell.ConsoleBackgroundColor(env, cfg.TerminalBackground),
AnsiColors: writerColors,
}
consoleTitle := &console.Title{
Env: env,
Ansi: ansi,
Template: cfg.ConsoleTitleTemplate,
}
eng := &engine.Engine{
Config: cfg,
Env: env,
Writer: writer,
ConsoleTitle: consoleTitle,
Ansi: ansi,
Config: cfg,
Env: env,
Writer: writer,
}
prompt := eng.PrintPrimary()
imageCreator := &engine.ImageRenderer{
@ -85,7 +74,7 @@ Exports the config to an image file using customized output options.`,
CursorPadding: cursorPadding,
RPromptOffset: rPromptOffset,
BgColor: bgColor,
Ansi: ansi,
Ansi: writer,
}
if outputImage != "" {
imageCreator.Path = cleanOutputPath(outputImage, env)

View file

@ -5,7 +5,6 @@ import (
"time"
"github.com/jandedobbeleer/oh-my-posh/color"
"github.com/jandedobbeleer/oh-my-posh/console"
"github.com/jandedobbeleer/oh-my-posh/engine"
"github.com/jandedobbeleer/oh-my-posh/platform"
"github.com/jandedobbeleer/oh-my-posh/shell"
@ -33,26 +32,17 @@ var debugCmd = &cobra.Command{
env.Init()
defer env.Close()
cfg := engine.LoadConfig(env)
ansi := &color.Ansi{}
ansi.InitPlain()
writerColors := cfg.MakeColors()
writer := &color.AnsiWriter{
Ansi: ansi,
TerminalBackground: shell.ConsoleBackgroundColor(env, cfg.TerminalBackground),
AnsiColors: writerColors,
}
consoleTitle := &console.Title{
Env: env,
Ansi: ansi,
Template: cfg.ConsoleTitleTemplate,
}
writer.Init(shell.GENERIC)
eng := &engine.Engine{
Config: cfg,
Env: env,
Writer: writer,
ConsoleTitle: consoleTitle,
Ansi: ansi,
Plain: plain,
Config: cfg,
Env: env,
Writer: writer,
Plain: plain,
}
fmt.Print(eng.PrintDebug(startTime, cliVersion))
},

View file

@ -1,366 +0,0 @@
package color
import (
"fmt"
"strings"
"github.com/jandedobbeleer/oh-my-posh/regex"
"github.com/jandedobbeleer/oh-my-posh/shell"
)
const (
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"
)
type Ansi struct {
title string
shell string
linechange string
left string
right string
creset string
clearBelow string
clearLine string
saveCursorPosition string
restoreCursorPosition string
colorSingle string
colorFull string
colorTransparent string
escapeLeft string
escapeRight string
hyperlink string
hyperlinkRegex string
osc99 string
osc7 string
osc51 string
bold string
italic string
underline string
overline string
strikethrough string
blink string
reverse string
dimmed string
format string
}
func (a *Ansi) Init(shellName string) {
a.shell = shellName
switch shellName {
case shell.ZSH:
a.format = "%%{%s%%}"
a.linechange = "%%{\x1b[%d%s%%}"
a.right = "%%{\x1b[%dC%%}"
a.left = "%%{\x1b[%dD%%}"
a.creset = "%{\x1b[0m%}"
a.clearBelow = "%{\x1b[0J%}"
a.clearLine = "%{\x1b[K%}"
a.saveCursorPosition = "%{\x1b7%}"
a.restoreCursorPosition = "%{\x1b8%}"
a.title = "%%{\x1b]0;%s\007%%}"
a.colorSingle = "%%{\x1b[%sm%%}%s%%{\x1b[0m%%}"
a.colorFull = "%%{\x1b[%sm\x1b[%sm%%}%s%%{\x1b[0m%%}"
a.colorTransparent = "%%{\x1b[%s;49m\x1b[7m%%}%s%%{\x1b[0m%%}"
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\\%%}"
a.bold = "%%{\x1b[1m%%}%s%%{\x1b[22m%%}"
a.italic = "%%{\x1b[3m%%}%s%%{\x1b[23m%%}"
a.underline = "%%{\x1b[4m%%}%s%%{\x1b[24m%%}"
a.overline = "%%{\x1b[53m%%}%s%%{\x1b[55m%%}"
a.blink = "%%{\x1b[5m%%}%s%%{\x1b[25m%%}"
a.reverse = "%%{\x1b[7m%%}%s%%{\x1b[27m%%}"
a.dimmed = "%%{\x1b[2m%%}%s%%{\x1b[22m%%}"
a.strikethrough = "%%{\x1b[9m%%}%s%%{\x1b[29m%%}"
case shell.BASH:
a.format = "\\[%s\\]"
a.linechange = "\\[\x1b[%d%s\\]"
a.right = "\\[\x1b[%dC\\]"
a.left = "\\[\x1b[%dD\\]"
a.creset = "\\[\x1b[0m\\]"
a.clearBelow = "\\[\x1b[0J\\]"
a.clearLine = "\\[\x1b[K\\]"
a.saveCursorPosition = "\\[\x1b7\\]"
a.restoreCursorPosition = "\\[\x1b8\\]"
a.title = "\\[\x1b]0;%s\007\\]"
a.colorSingle = "\\[\x1b[%sm\\]%s\\[\x1b[0m\\]"
a.colorFull = "\\[\x1b[%sm\x1b[%sm\\]%s\\[\x1b[0m\\]"
a.colorTransparent = "\\[\x1b[%s;49m\x1b[7m\\]%s\\[\x1b[0m\\]"
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\\\\\\]"
a.bold = "\\[\x1b[1m\\]%s\\[\x1b[22m\\]"
a.italic = "\\[\x1b[3m\\]%s\\[\x1b[23m\\]"
a.underline = "\\[\x1b[4m\\]%s\\[\x1b[24m\\]"
a.overline = "\\[\x1b[53m\\]%s\\[\x1b[55m\\]"
a.blink = "\\[\x1b[5m\\]%s\\[\x1b[25m\\]"
a.reverse = "\\[\x1b[7m\\]%s\\[\x1b[27m\\]"
a.dimmed = "\\[\x1b[2m\\]%s\\[\x1b[22m\\]"
a.strikethrough = "\\[\x1b[9m\\]%s\\[\x1b[29m\\]"
default:
a.format = "%s"
a.linechange = "\x1b[%d%s"
a.right = "\x1b[%dC"
a.left = "\x1b[%dD"
a.creset = "\x1b[0m"
a.clearBelow = "\x1b[0J"
a.clearLine = "\x1b[K"
a.saveCursorPosition = "\x1b7"
a.restoreCursorPosition = "\x1b8"
a.title = "\x1b]0;%s\007"
a.colorSingle = "\x1b[%sm%s\x1b[0m"
a.colorFull = "\x1b[%sm\x1b[%sm%s\x1b[0m"
a.colorTransparent = "\x1b[%s;49m\x1b[7m%s\x1b[0m"
a.escapeLeft = ""
a.escapeRight = ""
// 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\\"
a.bold = "\x1b[1m%s\x1b[22m"
a.italic = "\x1b[3m%s\x1b[23m"
a.underline = "\x1b[4m%s\x1b[24m"
a.overline = "\x1b[53m%s\x1b[55m"
a.blink = "\x1b[5m%s\x1b[25m"
a.reverse = "\x1b[7m%s\x1b[27m"
a.dimmed = "\x1b[2m%s\x1b[22m"
a.strikethrough = "\x1b[9m%s\x1b[29m"
}
}
func (a *Ansi) InitPlain() {
a.Init(shell.PLAIN)
}
func (a *Ansi) GenerateHyperlink(text string) string {
const (
LINK = "link"
TEXT = "text"
OTHER = "plain"
)
var result, hyperlink strings.Builder
var squareIndex, roundCount int
state := OTHER
for i, s := range text {
if s == '[' && state == OTHER {
state = TEXT
hyperlink.WriteRune(s)
continue
}
if state == OTHER {
result.WriteRune(s)
continue
}
hyperlink.WriteRune(s)
switch s {
case ']':
// potential end of text part of hyperlink
squareIndex = i
case '(':
// split into link part
if squareIndex == i-1 {
state = LINK
}
if state == LINK {
roundCount++
}
case ')':
if state != LINK {
continue
}
roundCount--
if roundCount != 0 {
continue
}
// end of link part
result.WriteString(a.replaceHyperlink(hyperlink.String()))
hyperlink.Reset()
state = OTHER
}
}
result.WriteString(hyperlink.String())
return result.String()
}
func (a *Ansi) replaceHyperlink(text string) string {
// hyperlink matching
results := regex.FindNamedRegexMatch("(?P<ALL>(?:\\[(?P<TEXT>.+)\\])(?:\\((?P<URL>.*)\\)))", text)
if len(results) != 3 {
return text
}
linkText := a.escapeLinkTextForFishShell(results["TEXT"])
// build hyperlink ansi
hyperlink := fmt.Sprintf(a.hyperlink, results["URL"], linkText)
// replace original text by the new onex
return strings.Replace(text, results["ALL"], hyperlink, 1)
}
func (a *Ansi) escapeLinkTextForFishShell(text string) string {
if a.shell != shell.FISH {
return text
}
escapeChars := map[string]string{
`c`: `\c`,
`a`: `\a`,
`b`: `\b`,
`e`: `\e`,
`f`: `\f`,
`n`: `\n`,
`r`: `\r`,
`t`: `\t`,
`v`: `\v`,
`$`: `\$`,
`*`: `\*`,
`?`: `\?`,
`~`: `\~`,
`%`: `\%`,
`#`: `\#`,
`(`: `\(`,
`)`: `\)`,
`{`: `\{`,
`}`: `\}`,
`[`: `\[`,
`]`: `\]`,
`<`: `\<`,
`>`: `\>`,
`^`: `\^`,
`&`: `\&`,
`;`: `\;`,
`"`: `\"`,
`'`: `\'`,
`x`: `\x`,
`X`: `\X`,
`0`: `\0`,
`u`: `\u`,
`U`: `\U`,
}
if val, ok := escapeChars[text[0:1]]; ok {
return val + text[1:]
}
return text
}
func (a *Ansi) formatText(text string) string {
replaceFormats := func(results []map[string]string) {
for _, result := range results {
var formatted string
switch result["format"] {
case "b":
formatted = fmt.Sprintf(a.bold, result["text"])
case "u":
formatted = fmt.Sprintf(a.underline, result["text"])
case "o":
formatted = fmt.Sprintf(a.overline, result["text"])
case "i":
formatted = fmt.Sprintf(a.italic, result["text"])
case "s":
formatted = fmt.Sprintf(a.strikethrough, result["text"])
case "d":
formatted = fmt.Sprintf(a.dimmed, result["text"])
case "f":
formatted = fmt.Sprintf(a.blink, result["text"])
case "r":
formatted = fmt.Sprintf(a.reverse, result["text"])
}
text = strings.Replace(text, result["context"], formatted, 1)
}
}
rgx := "(?P<context><(?P<format>[buisrdfo])>(?P<text>[^<]+)</[buisrdfo]>)"
for results := regex.FindAllNamedRegexMatch(rgx, text); len(results) != 0; results = regex.FindAllNamedRegexMatch(rgx, text) {
replaceFormats(results)
}
return text
}
func (a *Ansi) CarriageForward() string {
return fmt.Sprintf(a.right, 1000)
}
func (a *Ansi) GetCursorForRightWrite(length, offset int) string {
strippedLen := length + (-offset)
return fmt.Sprintf(a.left, strippedLen)
}
func (a *Ansi) ChangeLine(numberOfLines int) string {
position := "B"
if numberOfLines < 0 {
position = "F"
numberOfLines = -numberOfLines
}
return fmt.Sprintf(a.linechange, numberOfLines, position)
}
func (a *Ansi) ConsolePwd(pwdType, userName, hostName, pwd string) string {
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 *Ansi) ClearAfter() string {
return a.clearLine + a.clearBelow
}
func (a *Ansi) Title(title string) string {
// 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 *Ansi) ColorReset() string {
return a.creset
}
func (a *Ansi) FormatText(text string) string {
return fmt.Sprintf(a.format, text)
}
func (a *Ansi) SaveCursorPosition() string {
return a.saveCursorPosition
}
func (a *Ansi) RestoreCursorPosition() string {
return a.restoreCursorPosition
}
func (a *Ansi) LineBreak() string {
cr := fmt.Sprintf(a.left, 1000)
lf := fmt.Sprintf(a.linechange, 1, "B")
return cr + lf
}

520
src/color/ansi_writer.go Normal file
View file

@ -0,0 +1,520 @@
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

@ -0,0 +1,129 @@
package color
import (
"fmt"
"strings"
"github.com/jandedobbeleer/oh-my-posh/regex"
"github.com/jandedobbeleer/oh-my-posh/shell"
)
func (a *AnsiWriter) GenerateHyperlink(text string) string {
const (
LINK = "link"
TEXT = "text"
OTHER = "plain"
)
// do not do this when we do not need to
anchorCount := strings.Count(text, "[") + strings.Count(text, "]") + strings.Count(text, "(") + strings.Count(text, ")")
if anchorCount < 4 {
return text
}
var result, hyperlink strings.Builder
var squareIndex, roundCount int
state := OTHER
for i, s := range text {
if s == '[' && state == OTHER {
state = TEXT
hyperlink.WriteRune(s)
continue
}
if state == OTHER {
result.WriteRune(s)
continue
}
hyperlink.WriteRune(s)
switch s {
case ']':
// potential end of text part of hyperlink
squareIndex = i
case '(':
// split into link part
if squareIndex == i-1 {
state = LINK
}
if state == LINK {
roundCount++
}
case ')':
if state != LINK {
continue
}
roundCount--
if roundCount != 0 {
continue
}
// end of link part
result.WriteString(a.replaceHyperlink(hyperlink.String()))
hyperlink.Reset()
state = OTHER
}
}
result.WriteString(hyperlink.String())
return result.String()
}
func (a *AnsiWriter) replaceHyperlink(text string) string {
// hyperlink matching
results := regex.FindNamedRegexMatch("(?P<ALL>(?:\\[(?P<TEXT>.+)\\])(?:\\((?P<URL>.*)\\)))", text)
if len(results) != 3 {
return text
}
linkText := a.escapeLinkTextForFishShell(results["TEXT"])
// build hyperlink ansi
hyperlink := fmt.Sprintf(a.hyperlink, results["URL"], linkText)
// replace original text by the new onex
return strings.Replace(text, results["ALL"], hyperlink, 1)
}
func (a *AnsiWriter) escapeLinkTextForFishShell(text string) string {
if a.shell != shell.FISH {
return text
}
escapeChars := map[string]string{
`c`: `\c`,
`a`: `\a`,
`b`: `\b`,
`e`: `\e`,
`f`: `\f`,
`n`: `\n`,
`r`: `\r`,
`t`: `\t`,
`v`: `\v`,
`$`: `\$`,
`*`: `\*`,
`?`: `\?`,
`~`: `\~`,
`%`: `\%`,
`#`: `\#`,
`(`: `\(`,
`)`: `\)`,
`{`: `\{`,
`}`: `\}`,
`[`: `\[`,
`]`: `\]`,
`<`: `\<`,
`>`: `\>`,
`^`: `\^`,
`&`: `\&`,
`;`: `\;`,
`"`: `\"`,
`'`: `\'`,
`x`: `\x`,
`X`: `\X`,
`0`: `\0`,
`u`: `\u`,
`U`: `\U`,
}
if val, ok := escapeChars[text[0:1]]; ok {
return val + text[1:]
}
return text
}

View file

@ -19,7 +19,7 @@ func TestGenerateHyperlinkNoUrl(t *testing.T) {
{Text: "sample text with no url", ShellName: shell.BASH, Expected: "sample text with no url"},
}
for _, tc := range cases {
a := Ansi{}
a := AnsiWriter{}
a.Init(tc.ShellName)
hyperlinkText := a.GenerateHyperlink(tc.Text)
assert.Equal(t, tc.Expected, hyperlinkText)
@ -52,7 +52,7 @@ func TestGenerateHyperlinkWithUrl(t *testing.T) {
},
}
for _, tc := range cases {
a := Ansi{}
a := AnsiWriter{}
a.Init(tc.ShellName)
hyperlinkText := a.GenerateHyperlink(tc.Text)
assert.Equal(t, tc.Expected, hyperlinkText)
@ -70,38 +70,13 @@ func TestGenerateHyperlinkWithUrlNoName(t *testing.T) {
{Text: "[](http://www.google.be)", ShellName: shell.BASH, Expected: "[](http://www.google.be)"},
}
for _, tc := range cases {
a := Ansi{}
a := AnsiWriter{}
a.Init(tc.ShellName)
hyperlinkText := a.GenerateHyperlink(tc.Text)
assert.Equal(t, tc.Expected, hyperlinkText)
}
}
func TestFormatText(t *testing.T) {
cases := []struct {
Case string
Text string
Expected string
}{
{Case: "single format", Text: "This <b>is</b> white", Expected: "This \x1b[1mis\x1b[22m white"},
{Case: "double format", Text: "This <b>is</b> white, this <b>is</b> orange", Expected: "This \x1b[1mis\x1b[22m white, this \x1b[1mis\x1b[22m orange"},
{Case: "underline", Text: "This <u>is</u> white", Expected: "This \x1b[4mis\x1b[24m white"},
{Case: "italic", Text: "This <i>is</i> white", Expected: "This \x1b[3mis\x1b[23m white"},
{Case: "strikethrough", Text: "This <s>is</s> white", Expected: "This \x1b[9mis\x1b[29m white"},
{Case: "dimmed", Text: "This <d>is</d> white", Expected: "This \x1b[2mis\x1b[22m white"},
{Case: "flash", Text: "This <f>is</f> white", Expected: "This \x1b[5mis\x1b[25m white"},
{Case: "reversed", Text: "This <r>is</r> white", Expected: "This \x1b[7mis\x1b[27m white"},
{Case: "double", Text: "This <i><f>is</f></i> white", Expected: "This \x1b[3m\x1b[5mis\x1b[25m\x1b[23m white"},
{Case: "overline", Text: "This <o>is</o> white", Expected: "This \x1b[53mis\x1b[55m white"},
}
for _, tc := range cases {
a := Ansi{}
a.InitPlain()
formattedText := a.formatText(tc.Text)
assert.Equal(t, tc.Expected, formattedText, tc.Case)
}
}
func TestGenerateFileLink(t *testing.T) {
cases := []struct {
Text string
@ -114,7 +89,7 @@ func TestGenerateFileLink(t *testing.T) {
{Text: `[Windows](file:C:/Windows)`, Expected: "\x1b]8;;file:C:/Windows\x1b\\Windows\x1b]8;;\x1b\\"},
}
for _, tc := range cases {
a := Ansi{}
a := AnsiWriter{}
a.Init(shell.PWSH)
hyperlinkText := a.GenerateHyperlink(tc.Text)
assert.Equal(t, tc.Expected, hyperlinkText)

View file

@ -17,6 +17,31 @@ func TestWriteANSIColors(t *testing.T) {
Parent *Color
TerminalBackground string
}{
{
Case: "Bold",
Input: "<b>test</b>",
Expected: "\x1b[1m\x1b[30mtest\x1b[22m\x1b[0m",
Colors: &Color{Foreground: "black", Background: ParentBackground},
},
{
Case: "Bold with color override",
Input: "<b><#ffffff>test</></b>",
Expected: "\x1b[1m\x1b[30m\x1b[38;2;255;255;255mtest\x1b[30m\x1b[22m\x1b[0m",
Colors: &Color{Foreground: "black", Background: ParentBackground},
},
{
Case: "Bold with color override, flavor 2",
Input: "<#ffffff><b>test</b></>",
Expected: "\x1b[38;2;255;255;255m\x1b[1mtest\x1b[22m\x1b[0m",
Colors: &Color{Foreground: "black", Background: ParentBackground},
},
{
Case: "Double override",
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",
Colors: &Color{Foreground: "green", Background: "#FF5733"},
},
{
Case: "No color override",
Input: "test",
@ -47,28 +72,28 @@ func TestWriteANSIColors(t *testing.T) {
{
Case: "Inherit override foreground",
Input: "hello <parentForeground>world</>",
Expected: "\x1b[47m\x1b[30mhello \x1b[0m\x1b[47m\x1b[33mworld\x1b[0m",
Expected: "\x1b[47m\x1b[30mhello \x1b[33mworld\x1b[0m",
Colors: &Color{Foreground: "black", Background: "white"},
Parent: &Color{Foreground: "yellow", Background: "red"},
},
{
Case: "Inherit override background",
Input: "hello <black,parentBackground>world</>",
Expected: "\x1b[47m\x1b[30mhello \x1b[0m\x1b[41m\x1b[30mworld\x1b[0m",
Expected: "\x1b[47m\x1b[30mhello \x1b[41mworld\x1b[0m",
Colors: &Color{Foreground: "black", Background: "white"},
Parent: &Color{Foreground: "yellow", Background: "red"},
},
{
Case: "Inherit override background, no foreground specified",
Input: "hello <,parentBackground>world</>",
Expected: "\x1b[47m\x1b[30mhello \x1b[0m\x1b[41m\x1b[30mworld\x1b[0m",
Expected: "\x1b[47m\x1b[30mhello \x1b[41mworld\x1b[0m",
Colors: &Color{Foreground: "black", Background: "white"},
Parent: &Color{Foreground: "yellow", Background: "red"},
},
{
Case: "Inherit no parent foreground",
Input: "hello <parentForeground>world</>",
Expected: "\x1b[47m\x1b[30mhello \x1b[0m\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"},
},
{
@ -80,21 +105,21 @@ func TestWriteANSIColors(t *testing.T) {
{
Case: "Inherit override both",
Input: "hello <parentForeground,parentBackground>world</>",
Expected: "\x1b[47m\x1b[30mhello \x1b[0m\x1b[41m\x1b[33mworld\x1b[0m",
Expected: "\x1b[47m\x1b[30mhello \x1b[41m\x1b[33mworld\x1b[0m",
Colors: &Color{Foreground: "black", Background: "white"},
Parent: &Color{Foreground: "yellow", Background: "red"},
},
{
Case: "Inherit override both inverted",
Input: "hello <parentBackground,parentForeground>world</>",
Expected: "\x1b[47m\x1b[30mhello \x1b[0m\x1b[43m\x1b[31mworld\x1b[0m",
Expected: "\x1b[47m\x1b[30mhello \x1b[43m\x1b[31mworld\x1b[0m",
Colors: &Color{Foreground: "black", Background: "white"},
Parent: &Color{Foreground: "yellow", Background: "red"},
},
{
Case: "Inline override",
Input: "hello, <red>world</>, rabbit",
Expected: "\x1b[47m\x1b[30mhello, \x1b[0m\x1b[47m\x1b[31mworld\x1b[0m\x1b[47m\x1b[30m, rabbit\x1b[0m",
Expected: "\x1b[47m\x1b[30mhello, \x1b[31mworld\x1b[30m, rabbit\x1b[0m",
Colors: &Color{Foreground: "black", Background: "white"},
},
{
@ -106,15 +131,9 @@ func TestWriteANSIColors(t *testing.T) {
{
Case: "Transparent foreground override",
Input: "hello <#ffffff>world</>",
Expected: "\x1b[32mhello \x1b[0m\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},
},
{
Case: "Double override",
Input: "<#ffffff>jan</>@<#ffffff>Jans-MBP</>",
Expected: "\x1b[48;2;255;87;51m\x1b[38;2;255;255;255mjan\x1b[0m\x1b[48;2;255;87;51m\x1b[32m@\x1b[0m\x1b[48;2;255;87;51m\x1b[38;2;255;255;255mJans-MBP\x1b[0m",
Colors: &Color{Foreground: "green", Background: "#FF5733"},
},
{
Case: "No foreground",
Input: "test",
@ -130,7 +149,7 @@ func TestWriteANSIColors(t *testing.T) {
{
Case: "Transparent foreground, terminal background set",
Input: "test",
Expected: "\x1b[48;2;255;87;51m\x1b[38;2;33;47;60mtest\x1b[0m",
Expected: "\x1b[38;2;33;47;60m\x1b[48;2;255;87;51mtest\x1b[0m",
Colors: &Color{Foreground: Transparent, Background: "#FF5733"},
TerminalBackground: "#212F3C",
},
@ -140,6 +159,18 @@ func TestWriteANSIColors(t *testing.T) {
Expected: "\x1b[47m\x1b[30mtest\x1b[0m",
Colors: &Color{Foreground: "black", Background: "white"},
},
{
Case: "Background for background override",
Input: "<,background>test</>",
Expected: "\x1b[47m\x1b[30mtest\x1b[0m",
Colors: &Color{Foreground: "black", Background: "white"},
},
{
Case: "Google",
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
Colors: &Color{Foreground: "black", Background: "black"},
},
{
Case: "Foreground for background override",
Input: "<background>test</>",
@ -152,36 +183,22 @@ func TestWriteANSIColors(t *testing.T) {
Expected: "\x1b[40m\x1b[37mtest\x1b[0m",
Colors: &Color{Foreground: "black", Background: "white"},
},
{
Case: "Background for background override",
Input: "<,background>test</>",
Expected: "\x1b[47m\x1b[30mtest\x1b[0m",
Colors: &Color{Foreground: "black", Background: "white"},
},
{
Case: "Background for foreground override",
Input: "<,foreground>test</>",
Expected: "\x1b[40m\x1b[30mtest\x1b[0m",
Colors: &Color{Foreground: "black", Background: "white"},
},
{
Case: "Google",
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[0m\x1b[47m\x1b[31mo\x1b[0m\x1b[47m\x1b[33mo\x1b[0m\x1b[47m\x1b[34mg\x1b[0m\x1b[47m\x1b[32ml\x1b[0m\x1b[47m\x1b[31me\x1b[0m",
Colors: &Color{Foreground: "black", Background: "black"},
},
}
for _, tc := range cases {
ansi := &Ansi{}
ansi.Init(shell.PWSH)
renderer := &AnsiWriter{
Ansi: ansi,
ParentColors: []*Color{tc.Parent},
Colors: tc.Colors,
TerminalBackground: tc.TerminalBackground,
AnsiColors: &DefaultColors{},
}
renderer.Init(shell.GENERIC)
renderer.Write(tc.Colors.Background, tc.Colors.Foreground, tc.Input)
got, _ := renderer.String()
assert.Equal(t, tc.Expected, got, tc.Case)

View file

@ -10,6 +10,40 @@ import (
"github.com/gookit/color"
)
// AnsiColors is the interface that wraps AnsiColorFromString method.
//
// AnsiColorFromString 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`.
type AnsiColors interface {
AnsiColorFromString(colorString string, isBackground bool) AnsiColor
}
// AnsiColor 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".
type AnsiColor string
const (
emptyAnsiColor = AnsiColor("")
transparentAnsiColor = AnsiColor(Transparent)
)
func (c AnsiColor) IsEmpty() bool {
return c == emptyAnsiColor
}
func (c AnsiColor) IsTransparent() bool {
return c == transparentAnsiColor
}
func (c AnsiColor) ToForeground() AnsiColor {
colorString := string(c)
if strings.HasPrefix(colorString, "38;") {
return AnsiColor(strings.Replace(colorString, "38;", "48;", 1))
}
return c
}
func MakeColors(palette Palette, cacheEnabled bool, accentColor string, env platform.Environment) (colors AnsiColors) {
defaultColors := &DefaultColors{}
defaultColors.SetAccentColor(env, accentColor)

View file

@ -1,44 +0,0 @@
package color
import (
"strings"
"github.com/jandedobbeleer/oh-my-posh/regex"
)
// PlainWriter writes a plain string
type PlainWriter struct {
Ansi *Ansi
builder strings.Builder
length int
}
func (a *PlainWriter) SetColors(background, foreground string) {}
func (a *PlainWriter) SetParentColors(background, foreground string) {}
func (a *PlainWriter) Write(background, foreground, text string) {
if len(text) == 0 {
return
}
writeAndRemoveText := func(text, textToRemove, parentText string) string {
a.length += a.Ansi.MeasureText(text)
a.builder.WriteString(text)
return strings.Replace(parentText, textToRemove, "", 1)
}
match := regex.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.length += a.Ansi.MeasureText(text)
a.builder.WriteString(text)
}
func (a *PlainWriter) String() (string, int) {
defer a.builder.Reset()
return a.builder.String(), a.length
}

View file

@ -12,37 +12,37 @@ func init() { //nolint:gochecknoinits
runewidth.DefaultCondition.EastAsianWidth = false
}
func (ansi *Ansi) MeasureText(text string) int {
func (a *AnsiWriter) MeasureText(text string) int {
// skip strings with ANSI
if !strings.Contains(text, "\x1b") {
text = ansi.TrimEscapeSequences(text)
text = a.TrimEscapeSequences(text)
length := runewidth.StringWidth(text)
return length
}
if strings.Contains(text, "\x1b]8;;") {
matches := regex.FindAllNamedRegexMatch(ansi.hyperlinkRegex, text)
matches := regex.FindAllNamedRegexMatch(a.hyperlinkRegex, text)
for _, match := range matches {
text = strings.ReplaceAll(text, match["STR"], match["TEXT"])
}
}
text = ansi.TrimAnsi(text)
text = ansi.TrimEscapeSequences(text)
text = a.TrimAnsi(text)
text = a.TrimEscapeSequences(text)
length := runewidth.StringWidth(text)
return length
}
func (ansi *Ansi) TrimAnsi(text string) string {
func (a *AnsiWriter) TrimAnsi(text string) string {
if len(text) == 0 || !strings.Contains(text, "\x1b") {
return text
}
return regex.ReplaceAllString(AnsiRegex, text, "")
}
func (ansi *Ansi) TrimEscapeSequences(text string) string {
func (a *AnsiWriter) TrimEscapeSequences(text string) string {
if len(text) == 0 {
return text
}
text = strings.ReplaceAll(text, ansi.escapeLeft, "")
text = strings.ReplaceAll(text, ansi.escapeRight, "")
text = strings.ReplaceAll(text, a.escapeLeft, "")
text = strings.ReplaceAll(text, a.escapeRight, "")
return text
}

View file

@ -33,18 +33,18 @@ func TestMeasureText(t *testing.T) {
env.On("TemplateCache").Return(&platform.TemplateCache{
Env: make(map[string]string),
})
shells := []string{shell.BASH, shell.ZSH, shell.PLAIN}
shells := []string{shell.BASH, shell.ZSH, shell.GENERIC}
for _, shell := range shells {
for _, tc := range cases {
ansi := &Ansi{}
ansi.Init(shell)
ansiWriter := &AnsiWriter{}
ansiWriter.Init(shell)
tmpl := &template.Text{
Template: tc.Template,
Env: env,
}
text, _ := tmpl.Render()
text = ansi.GenerateHyperlink(text)
got := ansi.MeasureText(text)
text = ansiWriter.GenerateHyperlink(text)
got := ansiWriter.MeasureText(text)
assert.Equal(t, tc.Expected, got, fmt.Sprintf("%s: %s", shell, tc.Case))
}
}

View file

@ -1,247 +0,0 @@
package color
import (
"fmt"
"strings"
"github.com/jandedobbeleer/oh-my-posh/regex"
)
const (
colorRegex = `<(?P<foreground>[^,>]+)?,?(?P<background>[^>]+)?>(?P<content>[^<]*)<\/>`
)
type Writer interface {
Write(background, foreground, text string)
String() (string, int)
SetColors(background, foreground string)
SetParentColors(background, foreground string)
}
// AnsiWriter writes colorized ANSI strings
type AnsiWriter struct {
Ansi *Ansi
TerminalBackground string
Colors *Color
ParentColors []*Color
AnsiColors AnsiColors
builder strings.Builder
length int
}
type Color struct {
Background string
Foreground string
}
// AnsiColors is the interface that wraps AnsiColorFromString method.
//
// AnsiColorFromString 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`.
type AnsiColors interface {
AnsiColorFromString(colorString string, isBackground bool) AnsiColor
}
// AnsiColor 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".
type AnsiColor string
const (
emptyAnsiColor = AnsiColor("")
transparentAnsiColor = AnsiColor(Transparent)
)
func (c AnsiColor) IsEmpty() bool {
return c == emptyAnsiColor
}
func (c AnsiColor) IsTransparent() bool {
return c == transparentAnsiColor
}
func (c AnsiColor) ToForeground() AnsiColor {
colorString := string(c)
if strings.HasPrefix(colorString, "38;") {
return AnsiColor(strings.Replace(colorString, "38;", "48;", 1))
}
return c
}
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"
)
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) getAnsiFromColorString(colorString string, isBackground bool) AnsiColor {
return a.AnsiColors.AnsiColorFromString(colorString, isBackground)
}
func (a *AnsiWriter) writeColoredText(background, foreground AnsiColor, text string) {
// Avoid emitting empty strings with color codes
if text == "" || (foreground.IsTransparent() && background.IsTransparent()) {
return
}
a.length += a.Ansi.MeasureText(text)
// default to white fg if empty, empty backgrond is supported
if foreground.IsEmpty() {
foreground = a.getAnsiFromColorString("white", false)
}
if foreground.IsTransparent() && !background.IsEmpty() && len(a.TerminalBackground) != 0 {
bgAnsiColor := a.getAnsiFromColorString(a.TerminalBackground, false)
coloredText := fmt.Sprintf(a.Ansi.colorFull, background.ToForeground(), bgAnsiColor, text)
a.builder.WriteString(coloredText)
return
}
if foreground.IsTransparent() && !background.IsEmpty() {
coloredText := fmt.Sprintf(a.Ansi.colorTransparent, background, text)
a.builder.WriteString(coloredText)
return
} else if background.IsEmpty() || background.IsTransparent() {
coloredText := fmt.Sprintf(a.Ansi.colorSingle, foreground, text)
a.builder.WriteString(coloredText)
return
}
coloredText := fmt.Sprintf(a.Ansi.colorFull, background, foreground, text)
a.builder.WriteString(coloredText)
}
func (a *AnsiWriter) writeAndRemoveText(background, foreground AnsiColor, text, textToRemove, parentText string) string {
a.writeColoredText(background, foreground, text)
return strings.Replace(parentText, textToRemove, "", 1)
}
func (a *AnsiWriter) Write(background, foreground, text string) {
if len(text) == 0 {
return
}
bgAnsi, fgAnsi := a.asAnsiColors(background, foreground)
text = a.Ansi.formatText(text)
text = a.Ansi.GenerateHyperlink(text)
// first we match for any potentially valid colors enclosed in <>
// i.e., find color overrides
overrides := regex.FindAllNamedRegexMatch(colorRegex, text)
for _, override := range overrides {
fgOverride := override["foreground"]
bgOverride := override["background"]
if fgOverride == Transparent && len(bgOverride) == 0 {
bgOverride = background
}
bgOverrideAnsi, fgOverrideAnsi := a.asAnsiColors(bgOverride, fgOverride)
// set colors if they are empty
if bgOverrideAnsi.IsEmpty() {
bgOverrideAnsi = bgAnsi
}
if fgOverrideAnsi.IsEmpty() {
fgOverrideAnsi = fgAnsi
}
escapedTextSegment := override["text"]
innerText := override["content"]
textBeforeColorOverride := strings.Split(text, escapedTextSegment)[0]
text = a.writeAndRemoveText(bgAnsi, fgAnsi, textBeforeColorOverride, textBeforeColorOverride, text)
text = a.writeAndRemoveText(bgOverrideAnsi, fgOverrideAnsi, innerText, escapedTextSegment, text)
}
// color the remaining part of text with background and foreground
a.writeColoredText(bgAnsi, fgAnsi, text)
}
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

@ -1,30 +0,0 @@
package console
import (
"github.com/jandedobbeleer/oh-my-posh/color"
"github.com/jandedobbeleer/oh-my-posh/platform"
"github.com/jandedobbeleer/oh-my-posh/template"
)
type Title struct {
Env platform.Environment
Ansi *color.Ansi
Template string
}
func (t *Title) GetTitle() string {
title := t.getTitleTemplateText()
title = t.Ansi.TrimAnsi(title)
return t.Ansi.Title(title)
}
func (t *Title) getTitleTemplateText() string {
tmpl := &template.Text{
Template: t.Template,
Env: t.Env,
}
if text, err := tmpl.Render(); err == nil {
return text
}
return ""
}

View file

@ -1,129 +0,0 @@
package console
import (
"testing"
"github.com/jandedobbeleer/oh-my-posh/color"
"github.com/jandedobbeleer/oh-my-posh/mock"
"github.com/jandedobbeleer/oh-my-posh/platform"
"github.com/stretchr/testify/assert"
)
func TestGetTitle(t *testing.T) {
cases := []struct {
Template string
Root bool
User string
Cwd string
PathSeparator string
ShellName string
Expected string
}{
{
Template: "{{.Env.USERDOMAIN}} :: {{.PWD}}{{if .Root}} :: Admin{{end}} :: {{.Shell}}",
Cwd: "C:\\vagrant",
PathSeparator: "\\",
ShellName: "PowerShell",
Root: true,
Expected: "\x1b]0;MyCompany :: C:\\vagrant :: Admin :: PowerShell\a",
},
{
Template: "{{.Folder}}{{if .Root}} :: Admin{{end}} :: {{.Shell}}",
Cwd: "C:\\vagrant",
PathSeparator: "\\",
ShellName: "PowerShell",
Expected: "\x1b]0;vagrant :: PowerShell\a",
},
{
Template: "{{.UserName}}@{{.HostName}}{{if .Root}} :: Admin{{end}} :: {{.Shell}}",
Root: true,
User: "MyUser",
PathSeparator: "\\",
ShellName: "PowerShell",
Expected: "\x1b]0;MyUser@MyHost :: Admin :: PowerShell\a",
},
}
for _, tc := range cases {
env := new(mock.MockedEnvironment)
env.On("Pwd").Return(tc.Cwd)
env.On("Home").Return("/usr/home")
env.On("PathSeparator").Return(tc.PathSeparator)
env.On("TemplateCache").Return(&platform.TemplateCache{
Env: map[string]string{
"USERDOMAIN": "MyCompany",
},
Shell: tc.ShellName,
UserName: "MyUser",
Root: tc.Root,
HostName: "MyHost",
PWD: tc.Cwd,
Folder: "vagrant",
})
ansi := &color.Ansi{}
ansi.InitPlain()
ct := &Title{
Env: env,
Ansi: ansi,
Template: tc.Template,
}
got := ct.GetTitle()
assert.Equal(t, tc.Expected, got)
}
}
func TestGetConsoleTitleIfGethostnameReturnsError(t *testing.T) {
cases := []struct {
Template string
Root bool
User string
Cwd string
PathSeparator string
ShellName string
Expected string
}{
{
Template: "Not using Host only {{.UserName}} and {{.Shell}}",
User: "MyUser",
PathSeparator: "\\",
ShellName: "PowerShell",
Expected: "\x1b]0;Not using Host only MyUser and PowerShell\a",
},
{
Template: "{{.UserName}}@{{.HostName}} :: {{.Shell}}",
User: "MyUser",
PathSeparator: "\\",
ShellName: "PowerShell",
Expected: "\x1b]0;MyUser@ :: PowerShell\a",
},
{
Template: "\x1b[93m[\x1b[39m\x1b[96mconsole-title\x1b[39m\x1b[96m ≡\x1b[39m\x1b[31m +0\x1b[39m\x1b[31m ~1\x1b[39m\x1b[31m -0\x1b[39m\x1b[31m !\x1b[39m\x1b[93m]\x1b[39m",
Expected: "\x1b]0;[console-title ≡ +0 ~1 -0 !]\a",
},
}
for _, tc := range cases {
env := new(mock.MockedEnvironment)
env.On("Pwd").Return(tc.Cwd)
env.On("Home").Return("/usr/home")
env.On("TemplateCache").Return(&platform.TemplateCache{
Env: map[string]string{
"USERDOMAIN": "MyCompany",
},
Shell: tc.ShellName,
UserName: "MyUser",
Root: tc.Root,
HostName: "",
})
ansi := &color.Ansi{}
ansi.InitPlain()
ct := &Title{
Env: env,
Ansi: ansi,
Template: tc.Template,
}
got := ct.GetTitle()
assert.Equal(t, tc.Expected, got)
}
}

View file

@ -53,26 +53,22 @@ type Block struct {
env platform.Environment
writer color.Writer
ansi *color.Ansi
activeSegment *Segment
previousActiveSegment *Segment
}
func (b *Block) Init(env platform.Environment, writer color.Writer, ansi *color.Ansi) {
func (b *Block) Init(env platform.Environment, writer color.Writer) {
b.env = env
b.writer = writer
b.ansi = ansi
b.executeSegmentLogic()
}
func (b *Block) InitPlain(env platform.Environment, config *Config) {
b.ansi = &color.Ansi{}
b.ansi.InitPlain()
b.writer = &color.AnsiWriter{
Ansi: b.ansi,
TerminalBackground: shell.ConsoleBackgroundColor(env, config.TerminalBackground),
AnsiColors: config.MakeColors(),
}
b.writer.Init(shell.GENERIC)
b.env = env
b.executeSegmentLogic()
}

View file

@ -6,19 +6,16 @@ import (
"time"
"github.com/jandedobbeleer/oh-my-posh/color"
"github.com/jandedobbeleer/oh-my-posh/console"
"github.com/jandedobbeleer/oh-my-posh/platform"
"github.com/jandedobbeleer/oh-my-posh/shell"
"github.com/jandedobbeleer/oh-my-posh/template"
)
type Engine struct {
Config *Config
Env platform.Environment
Writer color.Writer
Ansi *color.Ansi
ConsoleTitle *console.Title
Plain bool
Config *Config
Env platform.Environment
Writer color.Writer
Plain bool
console strings.Builder
currentLineLength int
@ -30,13 +27,6 @@ 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 {
text := e.console.String()
e.console.Reset()
@ -71,9 +61,9 @@ func (e *Engine) PrintPrimary() string {
e.renderBlock(block)
}
if len(e.Config.ConsoleTitleTemplate) > 0 {
e.writeANSI(e.ConsoleTitle.GetTitle())
title := e.getTitleTemplateText()
e.write(e.Writer.FormatTitle(title))
}
e.writeANSI(e.Ansi.ColorReset())
if e.Config.FinalSpace {
e.write(" ")
}
@ -88,7 +78,7 @@ func (e *Engine) printPWD() {
cwd := e.Env.Pwd()
// Backwards compatibility for deprecated OSC99
if e.Config.OSC99 {
e.writeANSI(e.Ansi.ConsolePwd(color.OSC99, "", "", cwd))
e.write(e.Writer.ConsolePwd(color.OSC99, "", "", cwd))
return
}
// Allow template logic to define when to enable the PWD (when supported)
@ -102,13 +92,13 @@ func (e *Engine) printPWD() {
}
user := e.Env.User()
host, _ := e.Env.Host()
e.writeANSI(e.Ansi.ConsolePwd(pwdType, user, host, cwd))
e.write(e.Writer.ConsolePwd(pwdType, user, host, cwd))
}
func (e *Engine) newline() {
// WARP terminal will remove \n from the prompt, so we hack a newline in
if e.isWarp() {
e.write(e.Ansi.LineBreak())
e.write(e.Writer.LineBreak())
} else {
e.write("\n")
}
@ -140,6 +130,17 @@ func (e *Engine) shouldFill(block *Block, length int) (string, bool) {
return strings.Repeat(filler, repeat), true
}
func (e *Engine) getTitleTemplateText() string {
tmpl := &template.Text{
Template: e.Config.ConsoleTitleTemplate,
Env: e.Env,
}
if text, err := tmpl.Render(); err == nil {
return text
}
return ""
}
func (e *Engine) renderBlock(block *Block) {
defer func() {
// Due to a bug in PowerShell, the end of the line needs to be cleared.
@ -147,7 +148,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.Env.Shell() == shell.PWSH || e.Env.Shell() == shell.PWSH5 {
e.writeANSI(e.Ansi.ClearAfter())
e.write(e.Writer.ClearAfter())
}
}()
// when in bash, for rprompt blocks we need to write plain
@ -155,7 +156,7 @@ func (e *Engine) renderBlock(block *Block) {
if e.Env.Shell() == shell.BASH && (block.Type == RPrompt || block.Alignment == Right) {
block.InitPlain(e.Env, e.Config)
} else {
block.Init(e.Env, e.Writer, e.Ansi)
block.Init(e.Env, e.Writer)
}
if !block.Enabled() {
return
@ -171,7 +172,7 @@ func (e *Engine) renderBlock(block *Block) {
e.newline()
case Prompt:
if block.VerticalOffset != 0 {
e.writeANSI(e.Ansi.ChangeLine(block.VerticalOffset))
e.write(e.Writer.ChangeLine(block.VerticalOffset))
}
if block.Alignment == Left {
@ -208,17 +209,16 @@ func (e *Engine) renderBlock(block *Block) {
return
}
// this can contain ANSI escape sequences
ansi := e.Ansi
writer := e.Writer
if e.Env.Shell() == shell.BASH {
ansi = &color.Ansi{}
ansi.InitPlain()
writer.Init(shell.GENERIC)
}
prompt := ansi.CarriageForward()
prompt += ansi.GetCursorForRightWrite(length, block.HorizontalOffset)
prompt := writer.CarriageForward()
prompt += writer.GetCursorForRightWrite(length, block.HorizontalOffset)
prompt += text
e.currentLineLength = 0
if e.Env.Shell() == shell.BASH {
prompt = e.Ansi.FormatText(prompt)
prompt = e.Writer.FormatText(prompt)
}
e.write(prompt)
case RPrompt:
@ -234,9 +234,7 @@ func (e *Engine) PrintDebug(startTime time.Time, version string) string {
e.write("\n\x1b[1mSegments:\x1b[0m\n\n")
// console title timing
titleStartTime := time.Now()
title := e.ConsoleTitle.GetTitle()
title = strings.TrimPrefix(title, "\x1b]0;")
title = strings.TrimSuffix(title, "\a")
title := e.getTitleTemplateText()
segmentTiming := &SegmentTiming{
name: "ConsoleTitle",
nameLength: 12,
@ -247,7 +245,7 @@ func (e *Engine) PrintDebug(startTime time.Time, version string) string {
segmentTimings = append(segmentTimings, segmentTiming)
// loop each segments of each blocks
for _, block := range e.Config.Blocks {
block.Init(e.Env, e.Writer, e.Ansi)
block.Init(e.Env, e.Writer)
longestSegmentName, timings := block.Debug()
segmentTimings = append(segmentTimings, timings...)
if longestSegmentName > largestSegmentNameLength {
@ -278,11 +276,11 @@ func (e *Engine) print() string {
}
// Warp doesn't support RPROMPT so we need to write it manually
if e.isWarp() {
e.write(e.Ansi.SaveCursorPosition())
e.write(e.Ansi.CarriageForward())
e.write(e.Ansi.GetCursorForRightWrite(e.rpromptLength, 0))
e.write(e.Writer.SaveCursorPosition())
e.write(e.Writer.CarriageForward())
e.write(e.Writer.GetCursorForRightWrite(e.rpromptLength, 0))
e.write(e.rprompt)
e.write(e.Ansi.RestoreCursorPosition())
e.write(e.Writer.RestoreCursorPosition())
// escape double quotes contained in the prompt
prompt := fmt.Sprintf("PS1=\"%s\"", strings.ReplaceAll(e.string(), `"`, `\"`))
return prompt
@ -291,29 +289,29 @@ func (e *Engine) print() string {
prompt := fmt.Sprintf("PS1=\"%s\"", strings.ReplaceAll(e.string(), `"`, `\"`))
prompt += fmt.Sprintf("\nRPROMPT=\"%s\"", e.rprompt)
return prompt
case shell.PWSH, shell.PWSH5, shell.PLAIN, shell.NU:
case shell.PWSH, shell.PWSH5, shell.GENERIC, shell.NU:
if !e.canWriteRightBlock(true) {
break
}
e.write(e.Ansi.SaveCursorPosition())
e.write(e.Ansi.CarriageForward())
e.write(e.Ansi.GetCursorForRightWrite(e.rpromptLength, 0))
e.write(e.Writer.SaveCursorPosition())
e.write(e.Writer.CarriageForward())
e.write(e.Writer.GetCursorForRightWrite(e.rpromptLength, 0))
e.write(e.rprompt)
e.write(e.Ansi.RestoreCursorPosition())
e.write(e.Writer.RestoreCursorPosition())
case shell.BASH:
if !e.canWriteRightBlock(true) {
break
}
// 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
ansi := &color.Ansi{}
ansi.InitPlain()
prompt := ansi.SaveCursorPosition()
prompt += ansi.CarriageForward()
prompt += ansi.GetCursorForRightWrite(e.rpromptLength, 0)
writer := &color.AnsiWriter{}
writer.Init(shell.GENERIC)
prompt := writer.SaveCursorPosition()
prompt += writer.CarriageForward()
prompt += writer.GetCursorForRightWrite(e.rpromptLength, 0)
prompt += e.rprompt
prompt += ansi.RestoreCursorPosition()
prompt = e.Ansi.FormatText(prompt)
prompt += writer.RestoreCursorPosition()
prompt = e.Writer.FormatText(prompt)
e.write(prompt)
}
@ -345,8 +343,8 @@ func (e *Engine) PrintTooltip(tip string) string {
Segments: []*Segment{tooltip},
}
switch e.Env.Shell() {
case shell.ZSH, shell.CMD, shell.FISH, shell.PLAIN:
block.Init(e.Env, e.Writer, e.Ansi)
case shell.ZSH, shell.CMD, shell.FISH, shell.GENERIC:
block.Init(e.Env, e.Writer)
if !block.Enabled() {
return ""
}
@ -358,9 +356,9 @@ func (e *Engine) PrintTooltip(tip string) string {
return ""
}
text, length := block.RenderSegments()
e.write(e.Ansi.ClearAfter())
e.write(e.Ansi.CarriageForward())
e.write(e.Ansi.GetCursorForRightWrite(length, 0))
e.write(e.Writer.ClearAfter())
e.write(e.Writer.CarriageForward())
e.write(e.Writer.GetCursorForRightWrite(length, 0))
e.write(text)
return e.string()
}
@ -434,7 +432,7 @@ func (e *Engine) PrintExtraPrompt(promptType ExtraPromptType) string {
return prompt
}
return str
case shell.PWSH, shell.PWSH5, shell.CMD, shell.BASH, shell.FISH, shell.NU, shell.PLAIN:
case shell.PWSH, shell.PWSH5, shell.CMD, shell.BASH, shell.FISH, shell.NU, shell.GENERIC:
// Return the string and empty our buffer
str, _ := e.Writer.String()
return str
@ -455,7 +453,7 @@ func (e *Engine) PrintRPrompt() string {
if block == nil {
return ""
}
block.Init(e.Env, e.Writer, e.Ansi)
block.Init(e.Env, e.Writer)
if !block.Enabled() {
return ""
}

View file

@ -5,7 +5,6 @@ import (
"testing"
"github.com/jandedobbeleer/oh-my-posh/color"
"github.com/jandedobbeleer/oh-my-posh/console"
"github.com/jandedobbeleer/oh-my-posh/mock"
"github.com/jandedobbeleer/oh-my-posh/platform"
"github.com/jandedobbeleer/oh-my-posh/shell"
@ -71,15 +70,16 @@ func TestPrintPWD(t *testing.T) {
Env: make(map[string]string),
Shell: "shell",
})
ansi := &color.Ansi{}
ansi.InitPlain()
writer := &color.AnsiWriter{}
writer.Init(shell.GENERIC)
engine := &Engine{
Env: env,
Config: &Config{
PWD: tc.PWD,
OSC99: tc.OSC99,
},
Ansi: ansi,
Writer: writer,
}
engine.printPWD()
got := engine.print()
@ -101,25 +101,16 @@ func engineRender() {
cfg := LoadConfig(env)
defer testClearDefaultConfig()
ansi := &color.Ansi{}
ansi.InitPlain()
writerColors := cfg.MakeColors()
writer := &color.AnsiWriter{
Ansi: ansi,
TerminalBackground: shell.ConsoleBackgroundColor(env, cfg.TerminalBackground),
AnsiColors: writerColors,
}
consoleTitle := &console.Title{
Env: env,
Ansi: ansi,
Template: cfg.ConsoleTitleTemplate,
}
writer.Init(shell.GENERIC)
engine := &Engine{
Config: cfg,
Env: env,
Writer: writer,
ConsoleTitle: consoleTitle,
Ansi: ansi,
Config: cfg,
Env: env,
Writer: writer,
}
engine.PrintPrimary()
@ -130,3 +121,127 @@ func BenchmarkEngineRenderPalette(b *testing.B) {
engineRender()
}
}
func TestGetTitle(t *testing.T) {
cases := []struct {
Template string
Root bool
User string
Cwd string
PathSeparator string
ShellName string
Expected string
}{
{
Template: "{{.Env.USERDOMAIN}} :: {{.PWD}}{{if .Root}} :: Admin{{end}} :: {{.Shell}}",
Cwd: "C:\\vagrant",
PathSeparator: "\\",
ShellName: "PowerShell",
Root: true,
Expected: "\x1b]0;MyCompany :: C:\\vagrant :: Admin :: PowerShell\a",
},
{
Template: "{{.Folder}}{{if .Root}} :: Admin{{end}} :: {{.Shell}}",
Cwd: "C:\\vagrant",
PathSeparator: "\\",
ShellName: "PowerShell",
Expected: "\x1b]0;vagrant :: PowerShell\a",
},
{
Template: "{{.UserName}}@{{.HostName}}{{if .Root}} :: Admin{{end}} :: {{.Shell}}",
Root: true,
User: "MyUser",
PathSeparator: "\\",
ShellName: "PowerShell",
Expected: "\x1b]0;MyUser@MyHost :: Admin :: PowerShell\a",
},
}
for _, tc := range cases {
env := new(mock.MockedEnvironment)
env.On("Pwd").Return(tc.Cwd)
env.On("Home").Return("/usr/home")
env.On("PathSeparator").Return(tc.PathSeparator)
env.On("TemplateCache").Return(&platform.TemplateCache{
Env: map[string]string{
"USERDOMAIN": "MyCompany",
},
Shell: tc.ShellName,
UserName: "MyUser",
Root: tc.Root,
HostName: "MyHost",
PWD: tc.Cwd,
Folder: "vagrant",
})
ansi := &color.AnsiWriter{}
ansi.Init(shell.GENERIC)
engine := &Engine{
Config: &Config{
ConsoleTitleTemplate: tc.Template,
},
Writer: ansi,
Env: env,
}
title := engine.getTitleTemplateText()
got := ansi.FormatTitle(title)
assert.Equal(t, tc.Expected, got)
}
}
func TestGetConsoleTitleIfGethostnameReturnsError(t *testing.T) {
cases := []struct {
Template string
Root bool
User string
Cwd string
PathSeparator string
ShellName string
Expected string
}{
{
Template: "Not using Host only {{.UserName}} and {{.Shell}}",
User: "MyUser",
PathSeparator: "\\",
ShellName: "PowerShell",
Expected: "\x1b]0;Not using Host only MyUser and PowerShell\a",
},
{
Template: "{{.UserName}}@{{.HostName}} :: {{.Shell}}",
User: "MyUser",
PathSeparator: "\\",
ShellName: "PowerShell",
Expected: "\x1b]0;MyUser@ :: PowerShell\a",
},
{
Template: "\x1b[93m[\x1b[39m\x1b[96mconsole-title\x1b[39m\x1b[96m ≡\x1b[39m\x1b[31m +0\x1b[39m\x1b[31m ~1\x1b[39m\x1b[31m -0\x1b[39m\x1b[31m !\x1b[39m\x1b[93m]\x1b[39m",
Expected: "\x1b]0;[console-title ≡ +0 ~1 -0 !]\a",
},
}
for _, tc := range cases {
env := new(mock.MockedEnvironment)
env.On("Pwd").Return(tc.Cwd)
env.On("Home").Return("/usr/home")
env.On("TemplateCache").Return(&platform.TemplateCache{
Env: map[string]string{
"USERDOMAIN": "MyCompany",
},
Shell: tc.ShellName,
UserName: "MyUser",
Root: tc.Root,
HostName: "",
})
ansi := &color.AnsiWriter{}
ansi.Init(shell.GENERIC)
engine := &Engine{
Config: &Config{
ConsoleTitleTemplate: tc.Template,
},
Writer: ansi,
Env: env,
}
title := engine.getTitleTemplateText()
got := ansi.FormatTitle(title)
assert.Equal(t, tc.Expected, got)
}
}

View file

@ -110,7 +110,7 @@ type ImageRenderer struct {
CursorPadding int
RPromptOffset int
BgColor string
Ansi *color.Ansi
Ansi *color.AnsiWriter
Path string

View file

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

View file

@ -2,7 +2,6 @@ package engine
import (
"github.com/jandedobbeleer/oh-my-posh/color"
"github.com/jandedobbeleer/oh-my-posh/console"
"github.com/jandedobbeleer/oh-my-posh/platform"
"github.com/jandedobbeleer/oh-my-posh/shell"
)
@ -17,37 +16,19 @@ func New(flags *platform.Flags) *Engine {
env.Init()
cfg := LoadConfig(env)
ansi := &color.Ansi{}
var writer color.Writer
if flags.Plain {
ansi.InitPlain()
writer = &color.PlainWriter{
Ansi: ansi,
}
} else {
ansi.Init(env.Shell())
writerColors := cfg.MakeColors()
writer = &color.AnsiWriter{
Ansi: ansi,
TerminalBackground: shell.ConsoleBackgroundColor(env, cfg.TerminalBackground),
AnsiColors: writerColors,
}
}
consoleTitle := &console.Title{
Env: env,
Ansi: ansi,
Template: cfg.ConsoleTitleTemplate,
ansiWriter := &color.AnsiWriter{
TerminalBackground: shell.ConsoleBackgroundColor(env, cfg.TerminalBackground),
AnsiColors: cfg.MakeColors(),
Plain: flags.Plain,
}
ansiWriter.Init(env.Shell())
eng := &Engine{
Config: cfg,
Env: env,
Writer: writer,
ConsoleTitle: consoleTitle,
Ansi: ansi,
Plain: flags.Plain,
Config: cfg,
Env: env,
Writer: ansiWriter,
Plain: flags.Plain,
}
return eng

View file

@ -135,7 +135,7 @@ func TestParent(t *testing.T) {
env.On("Home").Return(tc.HomePath)
env.On("Pwd").Return(tc.Pwd)
env.On("Flags").Return(&platform.Flags{})
env.On("Shell").Return(shell.PLAIN)
env.On("Shell").Return(shell.GENERIC)
env.On("PathSeparator").Return(tc.PathSeparator)
env.On("GOOS").Return(tc.GOOS)
path := &Path{
@ -811,7 +811,7 @@ func TestFullAndFolderPath(t *testing.T) {
PSWD: tc.Pswd,
}
env.On("Flags").Return(args)
env.On("Shell").Return(shell.PLAIN)
env.On("Shell").Return(shell.GENERIC)
if len(tc.Template) == 0 {
tc.Template = "{{ if gt .StackCount 0 }}{{ .StackCount }} {{ end }}{{ .Path }}"
}
@ -870,7 +870,7 @@ func TestFullPathCustomMappedLocations(t *testing.T) {
PSWD: tc.Pwd,
}
env.On("Flags").Return(args)
env.On("Shell").Return(shell.PLAIN)
env.On("Shell").Return(shell.GENERIC)
env.On("TemplateCache").Return(&platform.TemplateCache{
Env: map[string]string{
"HOME": "/a/b/c",
@ -902,7 +902,7 @@ func TestFolderPathCustomMappedLocations(t *testing.T) {
PSWD: pwd,
}
env.On("Flags").Return(args)
env.On("Shell").Return(shell.PLAIN)
env.On("Shell").Return(shell.GENERIC)
path := &Path{
env: env,
props: properties.Map{

View file

@ -1,12 +1,12 @@
package shell
const (
ZSH = "zsh"
BASH = "bash"
PWSH = "pwsh"
FISH = "fish"
PWSH5 = "powershell"
CMD = "cmd"
NU = "nu"
PLAIN = "shell"
ZSH = "zsh"
BASH = "bash"
PWSH = "pwsh"
FISH = "fish"
PWSH5 = "powershell"
CMD = "cmd"
NU = "nu"
GENERIC = "shell"
)