oh-my-posh/src/color/ansi.go
2022-11-16 09:20:43 +01:00

350 lines
8.9 KiB
Go

package color
import (
"fmt"
"oh-my-posh/regex"
"oh-my-posh/shell"
"strings"
)
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"
)
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
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.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.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 = ""
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.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, hostName, pwd string) string {
if strings.HasSuffix(pwd, ":") {
pwd += "\\"
}
switch pwdType {
case OSC7:
return fmt.Sprintf(a.osc7, 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
}