oh-my-posh/src/color/ansi.go

350 lines
8.9 KiB
Go
Raw Normal View History

2022-01-26 04:09:21 -08:00
package color
2020-12-26 10:51:21 -08:00
import (
"fmt"
"oh-my-posh/regex"
2022-03-21 23:41:36 -07:00
"oh-my-posh/shell"
2020-12-26 10:51:21 -08:00
"strings"
)
2020-12-26 10:51:21 -08:00
const (
AnsiRegex = "[\u001B\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[a-zA-Z\\d]*)*)?\u0007)|(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PRZcf-ntqry=><~]))"
2022-07-13 04:53:55 -07:00
OSC99 string = "osc99"
OSC7 string = "osc7"
2020-12-26 10:51:21 -08:00
)
2022-01-26 04:09:21 -08:00
type Ansi struct {
title string
2020-12-26 10:51:21 -08:00
shell string
linechange string
left string
right string
creset string
clearBelow string
clearLine string
2020-12-26 10:51:21 -08:00
saveCursorPosition string
restoreCursorPosition string
colorSingle string
colorFull string
colorTransparent string
escapeLeft string
escapeRight string
hyperlink string
hyperlinkRegex string
osc99 string
osc7 string
2021-02-23 23:40:55 -08:00
bold string
italic string
underline string
2022-05-20 12:53:59 -07:00
overline string
2021-02-23 23:40:55 -08:00
strikethrough string
blink string
reverse string
dimmed string
2022-01-26 04:09:21 -08:00
format string
2020-12-26 10:51:21 -08:00
}
2022-03-21 23:41:36 -07:00
func (a *Ansi) Init(shellName string) {
a.shell = shellName
switch shellName {
case shell.ZSH:
2022-01-26 04:09:21 -08:00
a.format = "%%{%s%%}"
2020-12-26 10:51:21 -08:00
a.linechange = "%%{\x1b[%d%s%%}"
2021-06-15 12:23:08 -07:00
a.right = "%%{\x1b[%dC%%}"
a.left = "%%{\x1b[%dD%%}"
2020-12-26 10:51:21 -08:00
a.creset = "%{\x1b[0m%}"
a.clearBelow = "%{\x1b[0J%}"
a.clearLine = "%{\x1b[K%}"
2020-12-26 10:51:21 -08:00
a.saveCursorPosition = "%{\x1b7%}"
a.restoreCursorPosition = "%{\x1b8%}"
a.title = "%%{\x1b]0;%s\007%%}"
2020-12-26 10:51:21 -08:00
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%%}"
2020-12-26 10:51:21 -08:00
a.escapeLeft = "%{"
a.escapeRight = "%}"
2022-04-14 12:12:09 -07:00
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\\%%}"
2021-02-23 23:40:55 -08:00
a.bold = "%%{\x1b[1m%%}%s%%{\x1b[22m%%}"
a.italic = "%%{\x1b[3m%%}%s%%{\x1b[23m%%}"
a.underline = "%%{\x1b[4m%%}%s%%{\x1b[24m%%}"
2022-05-20 12:53:59 -07:00
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%%}"
2021-02-23 23:40:55 -08:00
a.strikethrough = "%%{\x1b[9m%%}%s%%{\x1b[29m%%}"
2022-03-21 23:41:36 -07:00
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\\]"
2020-12-26 10:51:21 -08:00
default:
2022-01-26 04:09:21 -08:00
a.format = "%s"
2020-12-26 10:51:21 -08:00
a.linechange = "\x1b[%d%s"
2021-06-15 12:23:08 -07:00
a.right = "\x1b[%dC"
a.left = "\x1b[%dD"
2020-12-26 10:51:21 -08:00
a.creset = "\x1b[0m"
a.clearBelow = "\x1b[0J"
a.clearLine = "\x1b[K"
2020-12-26 10:51:21 -08:00
a.saveCursorPosition = "\x1b7"
a.restoreCursorPosition = "\x1b8"
a.title = "\x1b]0;%s\007"
2020-12-26 10:51:21 -08:00
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"
2020-12-26 10:51:21 -08:00
a.escapeLeft = ""
a.escapeRight = ""
a.hyperlink = "\x1b]8;;%s\x1b\\%s\x1b]8;;\x1b\\"
2022-04-03 11:38:02 -07:00
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\\"
2021-02-23 23:40:55 -08:00
a.bold = "\x1b[1m%s\x1b[22m"
a.italic = "\x1b[3m%s\x1b[23m"
a.underline = "\x1b[4m%s\x1b[24m"
2022-05-20 12:53:59 -07:00
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"
2021-02-23 23:40:55 -08:00
a.strikethrough = "\x1b[9m%s\x1b[29m"
2020-12-26 10:51:21 -08:00
}
}
func (a *Ansi) InitPlain() {
a.Init(shell.PLAIN)
2020-12-26 10:51:21 -08:00
}
2022-04-03 11:38:02 -07:00
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
2022-05-15 10:47:22 -07:00
results := regex.FindNamedRegexMatch("(?P<ALL>(?:\\[(?P<TEXT>.+)\\])(?:\\((?P<URL>.*)\\)))", text)
if len(results) != 3 {
return text
}
2022-05-15 10:47:22 -07:00
linkText := a.escapeLinkTextForFishShell(results["TEXT"])
// build hyperlink ansi
2022-05-15 10:47:22 -07:00
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
}
2021-02-23 23:40:55 -08:00
2022-01-26 04:09:21 -08:00
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"])
2022-05-20 12:53:59 -07:00
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)
2021-02-23 23:40:55 -08:00
}
}
2022-05-20 12:53:59 -07:00
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)
2021-02-23 23:40:55 -08:00
}
return text
}
2021-04-20 12:30:46 -07:00
2022-01-26 04:09:21 -08:00
func (a *Ansi) CarriageForward() string {
2021-06-15 12:23:08 -07:00
return fmt.Sprintf(a.right, 1000)
}
func (a *Ansi) GetCursorForRightWrite(length, offset int) string {
strippedLen := length + (-offset)
2021-06-15 12:23:08 -07:00
return fmt.Sprintf(a.left, strippedLen)
2021-04-20 12:30:46 -07:00
}
2022-01-26 04:09:21 -08:00
func (a *Ansi) ChangeLine(numberOfLines int) string {
2021-04-20 12:30:46 -07:00
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 += "\\"
}
2022-07-13 04:53:55 -07:00
switch pwdType {
case OSC7:
return fmt.Sprintf(a.osc7, hostName, pwd)
2022-07-13 04:53:55 -07:00
case OSC99:
fallthrough
default:
return fmt.Sprintf(a.osc99, pwd)
2022-07-13 04:53:55 -07:00
}
2021-04-20 12:30:46 -07:00
}
2022-01-26 04:09:21 -08:00
func (a *Ansi) ClearAfter() string {
return a.clearLine + a.clearBelow
}
2022-01-26 04:09:21 -08:00
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)
}
2022-01-26 04:09:21 -08:00
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
}