mirror of
https://github.com/JanDeDobbeleer/oh-my-posh.git
synced 2024-12-31 22:07:25 -08:00
refactor(ansi): rewrite ansi and writer
This commit is contained in:
parent
e957e5f8cc
commit
005445b9fe
|
@ -4,7 +4,6 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/jandedobbeleer/oh-my-posh/color"
|
"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/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"
|
||||||
|
@ -52,31 +51,21 @@ Exports the config to an image file using customized output options.`,
|
||||||
Version: cliVersion,
|
Version: cliVersion,
|
||||||
CmdFlags: &platform.Flags{
|
CmdFlags: &platform.Flags{
|
||||||
Config: config,
|
Config: config,
|
||||||
Shell: shell.PLAIN,
|
Shell: shell.GENERIC,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
env.Init()
|
env.Init()
|
||||||
defer env.Close()
|
defer env.Close()
|
||||||
cfg := engine.LoadConfig(env)
|
cfg := engine.LoadConfig(env)
|
||||||
ansi := &color.Ansi{}
|
|
||||||
ansi.InitPlain()
|
|
||||||
writerColors := cfg.MakeColors()
|
writerColors := cfg.MakeColors()
|
||||||
writer := &color.AnsiWriter{
|
writer := &color.AnsiWriter{
|
||||||
Ansi: ansi,
|
|
||||||
TerminalBackground: shell.ConsoleBackgroundColor(env, cfg.TerminalBackground),
|
TerminalBackground: shell.ConsoleBackgroundColor(env, cfg.TerminalBackground),
|
||||||
AnsiColors: writerColors,
|
AnsiColors: writerColors,
|
||||||
}
|
}
|
||||||
consoleTitle := &console.Title{
|
|
||||||
Env: env,
|
|
||||||
Ansi: ansi,
|
|
||||||
Template: cfg.ConsoleTitleTemplate,
|
|
||||||
}
|
|
||||||
eng := &engine.Engine{
|
eng := &engine.Engine{
|
||||||
Config: cfg,
|
Config: cfg,
|
||||||
Env: env,
|
Env: env,
|
||||||
Writer: writer,
|
Writer: writer,
|
||||||
ConsoleTitle: consoleTitle,
|
|
||||||
Ansi: ansi,
|
|
||||||
}
|
}
|
||||||
prompt := eng.PrintPrimary()
|
prompt := eng.PrintPrimary()
|
||||||
imageCreator := &engine.ImageRenderer{
|
imageCreator := &engine.ImageRenderer{
|
||||||
|
@ -85,7 +74,7 @@ Exports the config to an image file using customized output options.`,
|
||||||
CursorPadding: cursorPadding,
|
CursorPadding: cursorPadding,
|
||||||
RPromptOffset: rPromptOffset,
|
RPromptOffset: rPromptOffset,
|
||||||
BgColor: bgColor,
|
BgColor: bgColor,
|
||||||
Ansi: ansi,
|
Ansi: writer,
|
||||||
}
|
}
|
||||||
if outputImage != "" {
|
if outputImage != "" {
|
||||||
imageCreator.Path = cleanOutputPath(outputImage, env)
|
imageCreator.Path = cleanOutputPath(outputImage, env)
|
||||||
|
|
|
@ -5,7 +5,6 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/jandedobbeleer/oh-my-posh/color"
|
"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/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,26 +32,17 @@ var debugCmd = &cobra.Command{
|
||||||
env.Init()
|
env.Init()
|
||||||
defer env.Close()
|
defer env.Close()
|
||||||
cfg := engine.LoadConfig(env)
|
cfg := engine.LoadConfig(env)
|
||||||
ansi := &color.Ansi{}
|
|
||||||
ansi.InitPlain()
|
|
||||||
writerColors := cfg.MakeColors()
|
writerColors := cfg.MakeColors()
|
||||||
writer := &color.AnsiWriter{
|
writer := &color.AnsiWriter{
|
||||||
Ansi: ansi,
|
|
||||||
TerminalBackground: shell.ConsoleBackgroundColor(env, cfg.TerminalBackground),
|
TerminalBackground: shell.ConsoleBackgroundColor(env, cfg.TerminalBackground),
|
||||||
AnsiColors: writerColors,
|
AnsiColors: writerColors,
|
||||||
}
|
}
|
||||||
consoleTitle := &console.Title{
|
writer.Init(shell.GENERIC)
|
||||||
Env: env,
|
|
||||||
Ansi: ansi,
|
|
||||||
Template: cfg.ConsoleTitleTemplate,
|
|
||||||
}
|
|
||||||
eng := &engine.Engine{
|
eng := &engine.Engine{
|
||||||
Config: cfg,
|
Config: cfg,
|
||||||
Env: env,
|
Env: env,
|
||||||
Writer: writer,
|
Writer: writer,
|
||||||
ConsoleTitle: consoleTitle,
|
Plain: plain,
|
||||||
Ansi: ansi,
|
|
||||||
Plain: plain,
|
|
||||||
}
|
}
|
||||||
fmt.Print(eng.PrintDebug(startTime, cliVersion))
|
fmt.Print(eng.PrintDebug(startTime, cliVersion))
|
||||||
},
|
},
|
||||||
|
|
|
@ -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
520
src/color/ansi_writer.go
Normal 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
|
||||||
|
}
|
129
src/color/ansi_writer_hyperlink.go
Normal file
129
src/color/ansi_writer_hyperlink.go
Normal 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
|
||||||
|
}
|
|
@ -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 := Ansi{}
|
a := AnsiWriter{}
|
||||||
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 := Ansi{}
|
a := AnsiWriter{}
|
||||||
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,38 +70,13 @@ 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 := Ansi{}
|
a := AnsiWriter{}
|
||||||
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
func TestGenerateFileLink(t *testing.T) {
|
||||||
cases := []struct {
|
cases := []struct {
|
||||||
Text string
|
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\\"},
|
{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 := Ansi{}
|
a := AnsiWriter{}
|
||||||
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)
|
|
@ -17,6 +17,31 @@ func TestWriteANSIColors(t *testing.T) {
|
||||||
Parent *Color
|
Parent *Color
|
||||||
TerminalBackground string
|
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",
|
Case: "No color override",
|
||||||
Input: "test",
|
Input: "test",
|
||||||
|
@ -47,28 +72,28 @@ func TestWriteANSIColors(t *testing.T) {
|
||||||
{
|
{
|
||||||
Case: "Inherit override foreground",
|
Case: "Inherit override foreground",
|
||||||
Input: "hello <parentForeground>world</>",
|
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"},
|
Colors: &Color{Foreground: "black", Background: "white"},
|
||||||
Parent: &Color{Foreground: "yellow", Background: "red"},
|
Parent: &Color{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[0m\x1b[41m\x1b[30mworld\x1b[0m",
|
Expected: "\x1b[47m\x1b[30mhello \x1b[41mworld\x1b[0m",
|
||||||
Colors: &Color{Foreground: "black", Background: "white"},
|
Colors: &Color{Foreground: "black", Background: "white"},
|
||||||
Parent: &Color{Foreground: "yellow", Background: "red"},
|
Parent: &Color{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[0m\x1b[41m\x1b[30mworld\x1b[0m",
|
Expected: "\x1b[47m\x1b[30mhello \x1b[41mworld\x1b[0m",
|
||||||
Colors: &Color{Foreground: "black", Background: "white"},
|
Colors: &Color{Foreground: "black", Background: "white"},
|
||||||
Parent: &Color{Foreground: "yellow", Background: "red"},
|
Parent: &Color{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[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"},
|
Colors: &Color{Foreground: "black", Background: "white"},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -80,21 +105,21 @@ func TestWriteANSIColors(t *testing.T) {
|
||||||
{
|
{
|
||||||
Case: "Inherit override both",
|
Case: "Inherit override both",
|
||||||
Input: "hello <parentForeground,parentBackground>world</>",
|
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"},
|
Colors: &Color{Foreground: "black", Background: "white"},
|
||||||
Parent: &Color{Foreground: "yellow", Background: "red"},
|
Parent: &Color{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[0m\x1b[43m\x1b[31mworld\x1b[0m",
|
Expected: "\x1b[47m\x1b[30mhello \x1b[43m\x1b[31mworld\x1b[0m",
|
||||||
Colors: &Color{Foreground: "black", Background: "white"},
|
Colors: &Color{Foreground: "black", Background: "white"},
|
||||||
Parent: &Color{Foreground: "yellow", Background: "red"},
|
Parent: &Color{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[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"},
|
Colors: &Color{Foreground: "black", Background: "white"},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -106,15 +131,9 @@ func TestWriteANSIColors(t *testing.T) {
|
||||||
{
|
{
|
||||||
Case: "Transparent foreground override",
|
Case: "Transparent foreground override",
|
||||||
Input: "hello <#ffffff>world</>",
|
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},
|
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",
|
Case: "No foreground",
|
||||||
Input: "test",
|
Input: "test",
|
||||||
|
@ -130,7 +149,7 @@ func TestWriteANSIColors(t *testing.T) {
|
||||||
{
|
{
|
||||||
Case: "Transparent foreground, terminal background set",
|
Case: "Transparent foreground, terminal background set",
|
||||||
Input: "test",
|
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"},
|
Colors: &Color{Foreground: Transparent, Background: "#FF5733"},
|
||||||
TerminalBackground: "#212F3C",
|
TerminalBackground: "#212F3C",
|
||||||
},
|
},
|
||||||
|
@ -140,6 +159,18 @@ func TestWriteANSIColors(t *testing.T) {
|
||||||
Expected: "\x1b[47m\x1b[30mtest\x1b[0m",
|
Expected: "\x1b[47m\x1b[30mtest\x1b[0m",
|
||||||
Colors: &Color{Foreground: "black", Background: "white"},
|
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",
|
Case: "Foreground for background override",
|
||||||
Input: "<background>test</>",
|
Input: "<background>test</>",
|
||||||
|
@ -152,36 +183,22 @@ func TestWriteANSIColors(t *testing.T) {
|
||||||
Expected: "\x1b[40m\x1b[37mtest\x1b[0m",
|
Expected: "\x1b[40m\x1b[37mtest\x1b[0m",
|
||||||
Colors: &Color{Foreground: "black", Background: "white"},
|
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",
|
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: &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 {
|
for _, tc := range cases {
|
||||||
ansi := &Ansi{}
|
|
||||||
ansi.Init(shell.PWSH)
|
|
||||||
renderer := &AnsiWriter{
|
renderer := &AnsiWriter{
|
||||||
Ansi: ansi,
|
|
||||||
ParentColors: []*Color{tc.Parent},
|
ParentColors: []*Color{tc.Parent},
|
||||||
Colors: tc.Colors,
|
Colors: tc.Colors,
|
||||||
TerminalBackground: tc.TerminalBackground,
|
TerminalBackground: tc.TerminalBackground,
|
||||||
AnsiColors: &DefaultColors{},
|
AnsiColors: &DefaultColors{},
|
||||||
}
|
}
|
||||||
|
renderer.Init(shell.GENERIC)
|
||||||
renderer.Write(tc.Colors.Background, tc.Colors.Foreground, tc.Input)
|
renderer.Write(tc.Colors.Background, tc.Colors.Foreground, tc.Input)
|
||||||
got, _ := renderer.String()
|
got, _ := renderer.String()
|
||||||
assert.Equal(t, tc.Expected, got, tc.Case)
|
assert.Equal(t, tc.Expected, got, tc.Case)
|
|
@ -10,6 +10,40 @@ import (
|
||||||
"github.com/gookit/color"
|
"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) {
|
func MakeColors(palette Palette, cacheEnabled bool, accentColor string, env platform.Environment) (colors AnsiColors) {
|
||||||
defaultColors := &DefaultColors{}
|
defaultColors := &DefaultColors{}
|
||||||
defaultColors.SetAccentColor(env, accentColor)
|
defaultColors.SetAccentColor(env, accentColor)
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -12,37 +12,37 @@ func init() { //nolint:gochecknoinits
|
||||||
runewidth.DefaultCondition.EastAsianWidth = false
|
runewidth.DefaultCondition.EastAsianWidth = false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ansi *Ansi) MeasureText(text string) int {
|
func (a *AnsiWriter) MeasureText(text string) int {
|
||||||
// skip strings with ANSI
|
// skip strings with ANSI
|
||||||
if !strings.Contains(text, "\x1b") {
|
if !strings.Contains(text, "\x1b") {
|
||||||
text = ansi.TrimEscapeSequences(text)
|
text = a.TrimEscapeSequences(text)
|
||||||
length := runewidth.StringWidth(text)
|
length := runewidth.StringWidth(text)
|
||||||
return length
|
return length
|
||||||
}
|
}
|
||||||
if strings.Contains(text, "\x1b]8;;") {
|
if strings.Contains(text, "\x1b]8;;") {
|
||||||
matches := regex.FindAllNamedRegexMatch(ansi.hyperlinkRegex, text)
|
matches := regex.FindAllNamedRegexMatch(a.hyperlinkRegex, text)
|
||||||
for _, match := range matches {
|
for _, match := range matches {
|
||||||
text = strings.ReplaceAll(text, match["STR"], match["TEXT"])
|
text = strings.ReplaceAll(text, match["STR"], match["TEXT"])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
text = ansi.TrimAnsi(text)
|
text = a.TrimAnsi(text)
|
||||||
text = ansi.TrimEscapeSequences(text)
|
text = a.TrimEscapeSequences(text)
|
||||||
length := runewidth.StringWidth(text)
|
length := runewidth.StringWidth(text)
|
||||||
return length
|
return length
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ansi *Ansi) TrimAnsi(text string) string {
|
func (a *AnsiWriter) 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 (ansi *Ansi) TrimEscapeSequences(text string) string {
|
func (a *AnsiWriter) TrimEscapeSequences(text string) string {
|
||||||
if len(text) == 0 {
|
if len(text) == 0 {
|
||||||
return text
|
return text
|
||||||
}
|
}
|
||||||
text = strings.ReplaceAll(text, ansi.escapeLeft, "")
|
text = strings.ReplaceAll(text, a.escapeLeft, "")
|
||||||
text = strings.ReplaceAll(text, ansi.escapeRight, "")
|
text = strings.ReplaceAll(text, a.escapeRight, "")
|
||||||
return text
|
return text
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,18 +33,18 @@ func TestMeasureText(t *testing.T) {
|
||||||
env.On("TemplateCache").Return(&platform.TemplateCache{
|
env.On("TemplateCache").Return(&platform.TemplateCache{
|
||||||
Env: make(map[string]string),
|
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 _, shell := range shells {
|
||||||
for _, tc := range cases {
|
for _, tc := range cases {
|
||||||
ansi := &Ansi{}
|
ansiWriter := &AnsiWriter{}
|
||||||
ansi.Init(shell)
|
ansiWriter.Init(shell)
|
||||||
tmpl := &template.Text{
|
tmpl := &template.Text{
|
||||||
Template: tc.Template,
|
Template: tc.Template,
|
||||||
Env: env,
|
Env: env,
|
||||||
}
|
}
|
||||||
text, _ := tmpl.Render()
|
text, _ := tmpl.Render()
|
||||||
text = ansi.GenerateHyperlink(text)
|
text = ansiWriter.GenerateHyperlink(text)
|
||||||
got := ansi.MeasureText(text)
|
got := ansiWriter.MeasureText(text)
|
||||||
assert.Equal(t, tc.Expected, got, fmt.Sprintf("%s: %s", shell, tc.Case))
|
assert.Equal(t, tc.Expected, got, fmt.Sprintf("%s: %s", shell, tc.Case))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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 ""
|
|
||||||
}
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -53,26 +53,22 @@ type Block struct {
|
||||||
|
|
||||||
env platform.Environment
|
env platform.Environment
|
||||||
writer color.Writer
|
writer color.Writer
|
||||||
ansi *color.Ansi
|
|
||||||
activeSegment *Segment
|
activeSegment *Segment
|
||||||
previousActiveSegment *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.env = env
|
||||||
b.writer = writer
|
b.writer = writer
|
||||||
b.ansi = ansi
|
|
||||||
b.executeSegmentLogic()
|
b.executeSegmentLogic()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Block) InitPlain(env platform.Environment, config *Config) {
|
func (b *Block) InitPlain(env platform.Environment, config *Config) {
|
||||||
b.ansi = &color.Ansi{}
|
|
||||||
b.ansi.InitPlain()
|
|
||||||
b.writer = &color.AnsiWriter{
|
b.writer = &color.AnsiWriter{
|
||||||
Ansi: b.ansi,
|
|
||||||
TerminalBackground: shell.ConsoleBackgroundColor(env, config.TerminalBackground),
|
TerminalBackground: shell.ConsoleBackgroundColor(env, config.TerminalBackground),
|
||||||
AnsiColors: config.MakeColors(),
|
AnsiColors: config.MakeColors(),
|
||||||
}
|
}
|
||||||
|
b.writer.Init(shell.GENERIC)
|
||||||
b.env = env
|
b.env = env
|
||||||
b.executeSegmentLogic()
|
b.executeSegmentLogic()
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,19 +6,16 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/jandedobbeleer/oh-my-posh/color"
|
"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/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"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Engine struct {
|
type Engine struct {
|
||||||
Config *Config
|
Config *Config
|
||||||
Env platform.Environment
|
Env platform.Environment
|
||||||
Writer color.Writer
|
Writer color.Writer
|
||||||
Ansi *color.Ansi
|
Plain bool
|
||||||
ConsoleTitle *console.Title
|
|
||||||
Plain bool
|
|
||||||
|
|
||||||
console strings.Builder
|
console strings.Builder
|
||||||
currentLineLength int
|
currentLineLength int
|
||||||
|
@ -30,13 +27,6 @@ func (e *Engine) write(text string) {
|
||||||
e.console.WriteString(text)
|
e.console.WriteString(text)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *Engine) writeANSI(text string) {
|
|
||||||
if e.Plain {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
e.console.WriteString(text)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *Engine) string() string {
|
func (e *Engine) string() string {
|
||||||
text := e.console.String()
|
text := e.console.String()
|
||||||
e.console.Reset()
|
e.console.Reset()
|
||||||
|
@ -71,9 +61,9 @@ func (e *Engine) PrintPrimary() string {
|
||||||
e.renderBlock(block)
|
e.renderBlock(block)
|
||||||
}
|
}
|
||||||
if len(e.Config.ConsoleTitleTemplate) > 0 {
|
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 {
|
if e.Config.FinalSpace {
|
||||||
e.write(" ")
|
e.write(" ")
|
||||||
}
|
}
|
||||||
|
@ -88,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.writeANSI(e.Ansi.ConsolePwd(color.OSC99, "", "", cwd))
|
e.write(e.Writer.ConsolePwd(color.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)
|
||||||
|
@ -102,13 +92,13 @@ func (e *Engine) printPWD() {
|
||||||
}
|
}
|
||||||
user := e.Env.User()
|
user := e.Env.User()
|
||||||
host, _ := e.Env.Host()
|
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() {
|
func (e *Engine) newline() {
|
||||||
// WARP terminal will remove \n from the prompt, so we hack a newline in
|
// WARP terminal will remove \n from the prompt, so we hack a newline in
|
||||||
if e.isWarp() {
|
if e.isWarp() {
|
||||||
e.write(e.Ansi.LineBreak())
|
e.write(e.Writer.LineBreak())
|
||||||
} else {
|
} else {
|
||||||
e.write("\n")
|
e.write("\n")
|
||||||
}
|
}
|
||||||
|
@ -140,6 +130,17 @@ func (e *Engine) shouldFill(block *Block, length int) (string, bool) {
|
||||||
return strings.Repeat(filler, repeat), true
|
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) {
|
func (e *Engine) renderBlock(block *Block) {
|
||||||
defer func() {
|
defer func() {
|
||||||
// Due to a bug in PowerShell, the end of the line needs to be cleared.
|
// 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,
|
// 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.
|
// but can hopefully one day be removed when this is resolved natively.
|
||||||
if e.Env.Shell() == shell.PWSH || e.Env.Shell() == shell.PWSH5 {
|
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
|
// 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) {
|
if e.Env.Shell() == shell.BASH && (block.Type == RPrompt || block.Alignment == Right) {
|
||||||
block.InitPlain(e.Env, e.Config)
|
block.InitPlain(e.Env, e.Config)
|
||||||
} else {
|
} else {
|
||||||
block.Init(e.Env, e.Writer, e.Ansi)
|
block.Init(e.Env, e.Writer)
|
||||||
}
|
}
|
||||||
if !block.Enabled() {
|
if !block.Enabled() {
|
||||||
return
|
return
|
||||||
|
@ -171,7 +172,7 @@ func (e *Engine) renderBlock(block *Block) {
|
||||||
e.newline()
|
e.newline()
|
||||||
case Prompt:
|
case Prompt:
|
||||||
if block.VerticalOffset != 0 {
|
if block.VerticalOffset != 0 {
|
||||||
e.writeANSI(e.Ansi.ChangeLine(block.VerticalOffset))
|
e.write(e.Writer.ChangeLine(block.VerticalOffset))
|
||||||
}
|
}
|
||||||
|
|
||||||
if block.Alignment == Left {
|
if block.Alignment == Left {
|
||||||
|
@ -208,17 +209,16 @@ func (e *Engine) renderBlock(block *Block) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// this can contain ANSI escape sequences
|
// this can contain ANSI escape sequences
|
||||||
ansi := e.Ansi
|
writer := e.Writer
|
||||||
if e.Env.Shell() == shell.BASH {
|
if e.Env.Shell() == shell.BASH {
|
||||||
ansi = &color.Ansi{}
|
writer.Init(shell.GENERIC)
|
||||||
ansi.InitPlain()
|
|
||||||
}
|
}
|
||||||
prompt := ansi.CarriageForward()
|
prompt := writer.CarriageForward()
|
||||||
prompt += ansi.GetCursorForRightWrite(length, block.HorizontalOffset)
|
prompt += writer.GetCursorForRightWrite(length, block.HorizontalOffset)
|
||||||
prompt += text
|
prompt += text
|
||||||
e.currentLineLength = 0
|
e.currentLineLength = 0
|
||||||
if e.Env.Shell() == shell.BASH {
|
if e.Env.Shell() == shell.BASH {
|
||||||
prompt = e.Ansi.FormatText(prompt)
|
prompt = e.Writer.FormatText(prompt)
|
||||||
}
|
}
|
||||||
e.write(prompt)
|
e.write(prompt)
|
||||||
case RPrompt:
|
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")
|
e.write("\n\x1b[1mSegments:\x1b[0m\n\n")
|
||||||
// console title timing
|
// console title timing
|
||||||
titleStartTime := time.Now()
|
titleStartTime := time.Now()
|
||||||
title := e.ConsoleTitle.GetTitle()
|
title := e.getTitleTemplateText()
|
||||||
title = strings.TrimPrefix(title, "\x1b]0;")
|
|
||||||
title = strings.TrimSuffix(title, "\a")
|
|
||||||
segmentTiming := &SegmentTiming{
|
segmentTiming := &SegmentTiming{
|
||||||
name: "ConsoleTitle",
|
name: "ConsoleTitle",
|
||||||
nameLength: 12,
|
nameLength: 12,
|
||||||
|
@ -247,7 +245,7 @@ func (e *Engine) PrintDebug(startTime time.Time, version string) string {
|
||||||
segmentTimings = append(segmentTimings, segmentTiming)
|
segmentTimings = append(segmentTimings, segmentTiming)
|
||||||
// loop each segments of each blocks
|
// loop each segments of each blocks
|
||||||
for _, block := range e.Config.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()
|
longestSegmentName, timings := block.Debug()
|
||||||
segmentTimings = append(segmentTimings, timings...)
|
segmentTimings = append(segmentTimings, timings...)
|
||||||
if longestSegmentName > largestSegmentNameLength {
|
if longestSegmentName > largestSegmentNameLength {
|
||||||
|
@ -278,11 +276,11 @@ func (e *Engine) print() string {
|
||||||
}
|
}
|
||||||
// Warp doesn't support RPROMPT so we need to write it manually
|
// Warp doesn't support RPROMPT so we need to write it manually
|
||||||
if e.isWarp() {
|
if e.isWarp() {
|
||||||
e.write(e.Ansi.SaveCursorPosition())
|
e.write(e.Writer.SaveCursorPosition())
|
||||||
e.write(e.Ansi.CarriageForward())
|
e.write(e.Writer.CarriageForward())
|
||||||
e.write(e.Ansi.GetCursorForRightWrite(e.rpromptLength, 0))
|
e.write(e.Writer.GetCursorForRightWrite(e.rpromptLength, 0))
|
||||||
e.write(e.rprompt)
|
e.write(e.rprompt)
|
||||||
e.write(e.Ansi.RestoreCursorPosition())
|
e.write(e.Writer.RestoreCursorPosition())
|
||||||
// escape double quotes contained in the prompt
|
// escape double quotes contained in the prompt
|
||||||
prompt := fmt.Sprintf("PS1=\"%s\"", strings.ReplaceAll(e.string(), `"`, `\"`))
|
prompt := fmt.Sprintf("PS1=\"%s\"", strings.ReplaceAll(e.string(), `"`, `\"`))
|
||||||
return prompt
|
return prompt
|
||||||
|
@ -291,29 +289,29 @@ func (e *Engine) print() string {
|
||||||
prompt := fmt.Sprintf("PS1=\"%s\"", strings.ReplaceAll(e.string(), `"`, `\"`))
|
prompt := fmt.Sprintf("PS1=\"%s\"", strings.ReplaceAll(e.string(), `"`, `\"`))
|
||||||
prompt += fmt.Sprintf("\nRPROMPT=\"%s\"", e.rprompt)
|
prompt += fmt.Sprintf("\nRPROMPT=\"%s\"", e.rprompt)
|
||||||
return prompt
|
return prompt
|
||||||
case shell.PWSH, shell.PWSH5, shell.PLAIN, shell.NU:
|
case shell.PWSH, shell.PWSH5, shell.GENERIC, shell.NU:
|
||||||
if !e.canWriteRightBlock(true) {
|
if !e.canWriteRightBlock(true) {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
e.write(e.Ansi.SaveCursorPosition())
|
e.write(e.Writer.SaveCursorPosition())
|
||||||
e.write(e.Ansi.CarriageForward())
|
e.write(e.Writer.CarriageForward())
|
||||||
e.write(e.Ansi.GetCursorForRightWrite(e.rpromptLength, 0))
|
e.write(e.Writer.GetCursorForRightWrite(e.rpromptLength, 0))
|
||||||
e.write(e.rprompt)
|
e.write(e.rprompt)
|
||||||
e.write(e.Ansi.RestoreCursorPosition())
|
e.write(e.Writer.RestoreCursorPosition())
|
||||||
case shell.BASH:
|
case shell.BASH:
|
||||||
if !e.canWriteRightBlock(true) {
|
if !e.canWriteRightBlock(true) {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
// 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
|
||||||
ansi := &color.Ansi{}
|
writer := &color.AnsiWriter{}
|
||||||
ansi.InitPlain()
|
writer.Init(shell.GENERIC)
|
||||||
prompt := ansi.SaveCursorPosition()
|
prompt := writer.SaveCursorPosition()
|
||||||
prompt += ansi.CarriageForward()
|
prompt += writer.CarriageForward()
|
||||||
prompt += ansi.GetCursorForRightWrite(e.rpromptLength, 0)
|
prompt += writer.GetCursorForRightWrite(e.rpromptLength, 0)
|
||||||
prompt += e.rprompt
|
prompt += e.rprompt
|
||||||
prompt += ansi.RestoreCursorPosition()
|
prompt += writer.RestoreCursorPosition()
|
||||||
prompt = e.Ansi.FormatText(prompt)
|
prompt = e.Writer.FormatText(prompt)
|
||||||
e.write(prompt)
|
e.write(prompt)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -345,8 +343,8 @@ func (e *Engine) PrintTooltip(tip string) string {
|
||||||
Segments: []*Segment{tooltip},
|
Segments: []*Segment{tooltip},
|
||||||
}
|
}
|
||||||
switch e.Env.Shell() {
|
switch e.Env.Shell() {
|
||||||
case shell.ZSH, shell.CMD, shell.FISH, shell.PLAIN:
|
case shell.ZSH, shell.CMD, shell.FISH, shell.GENERIC:
|
||||||
block.Init(e.Env, e.Writer, e.Ansi)
|
block.Init(e.Env, e.Writer)
|
||||||
if !block.Enabled() {
|
if !block.Enabled() {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
@ -358,9 +356,9 @@ func (e *Engine) PrintTooltip(tip string) string {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
text, length := block.RenderSegments()
|
text, length := block.RenderSegments()
|
||||||
e.write(e.Ansi.ClearAfter())
|
e.write(e.Writer.ClearAfter())
|
||||||
e.write(e.Ansi.CarriageForward())
|
e.write(e.Writer.CarriageForward())
|
||||||
e.write(e.Ansi.GetCursorForRightWrite(length, 0))
|
e.write(e.Writer.GetCursorForRightWrite(length, 0))
|
||||||
e.write(text)
|
e.write(text)
|
||||||
return e.string()
|
return e.string()
|
||||||
}
|
}
|
||||||
|
@ -434,7 +432,7 @@ func (e *Engine) PrintExtraPrompt(promptType ExtraPromptType) string {
|
||||||
return prompt
|
return prompt
|
||||||
}
|
}
|
||||||
return str
|
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
|
// Return the string and empty our buffer
|
||||||
str, _ := e.Writer.String()
|
str, _ := e.Writer.String()
|
||||||
return str
|
return str
|
||||||
|
@ -455,7 +453,7 @@ func (e *Engine) PrintRPrompt() string {
|
||||||
if block == nil {
|
if block == nil {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
block.Init(e.Env, e.Writer, e.Ansi)
|
block.Init(e.Env, e.Writer)
|
||||||
if !block.Enabled() {
|
if !block.Enabled() {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,6 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/jandedobbeleer/oh-my-posh/color"
|
"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/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"
|
||||||
|
@ -71,15 +70,16 @@ func TestPrintPWD(t *testing.T) {
|
||||||
Env: make(map[string]string),
|
Env: make(map[string]string),
|
||||||
Shell: "shell",
|
Shell: "shell",
|
||||||
})
|
})
|
||||||
ansi := &color.Ansi{}
|
|
||||||
ansi.InitPlain()
|
writer := &color.AnsiWriter{}
|
||||||
|
writer.Init(shell.GENERIC)
|
||||||
engine := &Engine{
|
engine := &Engine{
|
||||||
Env: env,
|
Env: env,
|
||||||
Config: &Config{
|
Config: &Config{
|
||||||
PWD: tc.PWD,
|
PWD: tc.PWD,
|
||||||
OSC99: tc.OSC99,
|
OSC99: tc.OSC99,
|
||||||
},
|
},
|
||||||
Ansi: ansi,
|
Writer: writer,
|
||||||
}
|
}
|
||||||
engine.printPWD()
|
engine.printPWD()
|
||||||
got := engine.print()
|
got := engine.print()
|
||||||
|
@ -101,25 +101,16 @@ func engineRender() {
|
||||||
cfg := LoadConfig(env)
|
cfg := LoadConfig(env)
|
||||||
defer testClearDefaultConfig()
|
defer testClearDefaultConfig()
|
||||||
|
|
||||||
ansi := &color.Ansi{}
|
|
||||||
ansi.InitPlain()
|
|
||||||
writerColors := cfg.MakeColors()
|
writerColors := cfg.MakeColors()
|
||||||
writer := &color.AnsiWriter{
|
writer := &color.AnsiWriter{
|
||||||
Ansi: ansi,
|
|
||||||
TerminalBackground: shell.ConsoleBackgroundColor(env, cfg.TerminalBackground),
|
TerminalBackground: shell.ConsoleBackgroundColor(env, cfg.TerminalBackground),
|
||||||
AnsiColors: writerColors,
|
AnsiColors: writerColors,
|
||||||
}
|
}
|
||||||
consoleTitle := &console.Title{
|
writer.Init(shell.GENERIC)
|
||||||
Env: env,
|
|
||||||
Ansi: ansi,
|
|
||||||
Template: cfg.ConsoleTitleTemplate,
|
|
||||||
}
|
|
||||||
engine := &Engine{
|
engine := &Engine{
|
||||||
Config: cfg,
|
Config: cfg,
|
||||||
Env: env,
|
Env: env,
|
||||||
Writer: writer,
|
Writer: writer,
|
||||||
ConsoleTitle: consoleTitle,
|
|
||||||
Ansi: ansi,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
engine.PrintPrimary()
|
engine.PrintPrimary()
|
||||||
|
@ -130,3 +121,127 @@ func BenchmarkEngineRenderPalette(b *testing.B) {
|
||||||
engineRender()
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -110,7 +110,7 @@ type ImageRenderer struct {
|
||||||
CursorPadding int
|
CursorPadding int
|
||||||
RPromptOffset int
|
RPromptOffset int
|
||||||
BgColor string
|
BgColor string
|
||||||
Ansi *color.Ansi
|
Ansi *color.AnsiWriter
|
||||||
|
|
||||||
Path string
|
Path string
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,7 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/jandedobbeleer/oh-my-posh/color"
|
"github.com/jandedobbeleer/oh-my-posh/color"
|
||||||
|
"github.com/jandedobbeleer/oh-my-posh/shell"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
@ -32,8 +33,8 @@ func runImageTest(config, content string) (string, error) {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
defer os.Remove(file.Name())
|
defer os.Remove(file.Name())
|
||||||
ansi := &color.Ansi{}
|
ansi := &color.AnsiWriter{}
|
||||||
ansi.InitPlain()
|
ansi.Init(shell.GENERIC)
|
||||||
image := &ImageRenderer{
|
image := &ImageRenderer{
|
||||||
AnsiString: content,
|
AnsiString: content,
|
||||||
Ansi: ansi,
|
Ansi: ansi,
|
||||||
|
|
|
@ -2,7 +2,6 @@ package engine
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/jandedobbeleer/oh-my-posh/color"
|
"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/platform"
|
||||||
"github.com/jandedobbeleer/oh-my-posh/shell"
|
"github.com/jandedobbeleer/oh-my-posh/shell"
|
||||||
)
|
)
|
||||||
|
@ -17,37 +16,19 @@ func New(flags *platform.Flags) *Engine {
|
||||||
|
|
||||||
env.Init()
|
env.Init()
|
||||||
cfg := LoadConfig(env)
|
cfg := LoadConfig(env)
|
||||||
ansi := &color.Ansi{}
|
|
||||||
|
|
||||||
var writer color.Writer
|
ansiWriter := &color.AnsiWriter{
|
||||||
if flags.Plain {
|
TerminalBackground: shell.ConsoleBackgroundColor(env, cfg.TerminalBackground),
|
||||||
ansi.InitPlain()
|
AnsiColors: cfg.MakeColors(),
|
||||||
writer = &color.PlainWriter{
|
Plain: flags.Plain,
|
||||||
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.Init(env.Shell())
|
||||||
|
|
||||||
eng := &Engine{
|
eng := &Engine{
|
||||||
Config: cfg,
|
Config: cfg,
|
||||||
Env: env,
|
Env: env,
|
||||||
Writer: writer,
|
Writer: ansiWriter,
|
||||||
ConsoleTitle: consoleTitle,
|
Plain: flags.Plain,
|
||||||
Ansi: ansi,
|
|
||||||
Plain: flags.Plain,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return eng
|
return eng
|
||||||
|
|
|
@ -135,7 +135,7 @@ func TestParent(t *testing.T) {
|
||||||
env.On("Home").Return(tc.HomePath)
|
env.On("Home").Return(tc.HomePath)
|
||||||
env.On("Pwd").Return(tc.Pwd)
|
env.On("Pwd").Return(tc.Pwd)
|
||||||
env.On("Flags").Return(&platform.Flags{})
|
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("PathSeparator").Return(tc.PathSeparator)
|
||||||
env.On("GOOS").Return(tc.GOOS)
|
env.On("GOOS").Return(tc.GOOS)
|
||||||
path := &Path{
|
path := &Path{
|
||||||
|
@ -811,7 +811,7 @@ func TestFullAndFolderPath(t *testing.T) {
|
||||||
PSWD: tc.Pswd,
|
PSWD: tc.Pswd,
|
||||||
}
|
}
|
||||||
env.On("Flags").Return(args)
|
env.On("Flags").Return(args)
|
||||||
env.On("Shell").Return(shell.PLAIN)
|
env.On("Shell").Return(shell.GENERIC)
|
||||||
if len(tc.Template) == 0 {
|
if len(tc.Template) == 0 {
|
||||||
tc.Template = "{{ if gt .StackCount 0 }}{{ .StackCount }} {{ end }}{{ .Path }}"
|
tc.Template = "{{ if gt .StackCount 0 }}{{ .StackCount }} {{ end }}{{ .Path }}"
|
||||||
}
|
}
|
||||||
|
@ -870,7 +870,7 @@ func TestFullPathCustomMappedLocations(t *testing.T) {
|
||||||
PSWD: tc.Pwd,
|
PSWD: tc.Pwd,
|
||||||
}
|
}
|
||||||
env.On("Flags").Return(args)
|
env.On("Flags").Return(args)
|
||||||
env.On("Shell").Return(shell.PLAIN)
|
env.On("Shell").Return(shell.GENERIC)
|
||||||
env.On("TemplateCache").Return(&platform.TemplateCache{
|
env.On("TemplateCache").Return(&platform.TemplateCache{
|
||||||
Env: map[string]string{
|
Env: map[string]string{
|
||||||
"HOME": "/a/b/c",
|
"HOME": "/a/b/c",
|
||||||
|
@ -902,7 +902,7 @@ func TestFolderPathCustomMappedLocations(t *testing.T) {
|
||||||
PSWD: pwd,
|
PSWD: pwd,
|
||||||
}
|
}
|
||||||
env.On("Flags").Return(args)
|
env.On("Flags").Return(args)
|
||||||
env.On("Shell").Return(shell.PLAIN)
|
env.On("Shell").Return(shell.GENERIC)
|
||||||
path := &Path{
|
path := &Path{
|
||||||
env: env,
|
env: env,
|
||||||
props: properties.Map{
|
props: properties.Map{
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
package shell
|
package shell
|
||||||
|
|
||||||
const (
|
const (
|
||||||
ZSH = "zsh"
|
ZSH = "zsh"
|
||||||
BASH = "bash"
|
BASH = "bash"
|
||||||
PWSH = "pwsh"
|
PWSH = "pwsh"
|
||||||
FISH = "fish"
|
FISH = "fish"
|
||||||
PWSH5 = "powershell"
|
PWSH5 = "powershell"
|
||||||
CMD = "cmd"
|
CMD = "cmd"
|
||||||
NU = "nu"
|
NU = "nu"
|
||||||
PLAIN = "shell"
|
GENERIC = "shell"
|
||||||
)
|
)
|
||||||
|
|
Loading…
Reference in a new issue