oh-my-posh/src/ansi.go
lnu e618163da3 feat(owm): support all units + hyperlink + http timeout
new http_timeout param for segments executing http request
doc updated
json schema updated(missing data for owm + timeout)
hyperlink replacement regex updated to support http/https, fix an inssue
when a segment with hyperlink was on the right prompt(pwsh)
2021-08-19 09:04:52 +02:00

208 lines
6.5 KiB
Go

package main
import (
"fmt"
"strings"
)
const (
ansiRegex = "[\u001B\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[a-zA-Z\\d]*)*)?\u0007)|(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PRZcf-ntqry=><~]))"
)
type ansiUtils struct {
shell string
linechange string
left string
right string
creset string
clearBelow string
clearLine string
saveCursorPosition string
restoreCursorPosition string
title string
colorSingle string
colorFull string
colorTransparent string
escapeLeft string
escapeRight string
hyperlink string
osc99 string
bold string
italic string
underline string
strikethrough string
bashFormat string
shellReservedKeywords []shellKeyWordReplacement
}
type shellKeyWordReplacement struct {
text string
replacement string
}
func (a *ansiUtils) init(shell string) {
a.shell = shell
a.bashFormat = "\\[%s\\]"
switch shell {
case zsh:
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.osc99 = "%%{\x1b]9;9;\"%s\"\x1b\\%%}"
a.bold = "%%{\x1b[1m%%}%s%%{\x1b[22m%%}"
a.italic = "%%{\x1b[3m%%}%s%%{\x1b[23m%%}"
a.underline = "%%{\x1b[4m%%}%s%%{\x1b[24m%%}"
a.strikethrough = "%%{\x1b[9m%%}%s%%{\x1b[29m%%}"
// escape double quotes and variable expansion
a.shellReservedKeywords = append(a.shellReservedKeywords, shellKeyWordReplacement{"\\", "\\\\"}, shellKeyWordReplacement{"%", "%%"})
case bash:
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.osc99 = "\\[\x1b]9;9;\"%s\"\x1b\\\\\\]"
a.bold = "\\[\x1b[1m\\]%s\\[\x1b[22m\\]"
a.italic = "\\[\x1b[3m\\]%s\\[\x1b[23m\\]"
a.underline = "\\[\x1b[4m\\]%s\\[\x1b[24m\\]"
a.strikethrough = "\\[\x1b[9m\\]%s\\[\x1b[29m\\]"
// escape backslashes to avoid replacements
// https://tldp.org/HOWTO/Bash-Prompt-HOWTO/bash-prompt-escape-sequences.html
a.shellReservedKeywords = append(a.shellReservedKeywords, shellKeyWordReplacement{"\\", "\\\\"})
default:
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.osc99 = "\x1b]9;9;\"%s\"\x1b\\"
a.bold = "\x1b[1m%s\x1b[22m"
a.italic = "\x1b[3m%s\x1b[23m"
a.underline = "\x1b[4m%s\x1b[24m"
a.strikethrough = "\x1b[9m%s\x1b[29m"
}
// common replacement for all shells
a.shellReservedKeywords = append(a.shellReservedKeywords, shellKeyWordReplacement{"`", "'"})
}
func (a *ansiUtils) lenWithoutANSI(text string) int {
if len(text) == 0 {
return 0
}
// replace hyperlinks(file/http/https)
matches := findAllNamedRegexMatch(`(?P<STR>\x1b]8;;(file|http|https):\/\/(.+?)\x1b\\(?P<URL>.+?)\x1b]8;;\x1b\\)`, text)
for _, match := range matches {
text = strings.ReplaceAll(text, match[str], match[url])
}
// replace console title
matches = findAllNamedRegexMatch(`(?P<STR>\x1b\]0;(.+)\007)`, text)
for _, match := range matches {
text = strings.ReplaceAll(text, match[str], "")
}
stripped := replaceAllString(ansiRegex, text, "")
stripped = strings.ReplaceAll(stripped, a.escapeLeft, "")
stripped = strings.ReplaceAll(stripped, a.escapeRight, "")
runeText := []rune(stripped)
return len(runeText)
}
func (a *ansiUtils) generateHyperlink(text string) string {
// hyperlink matching
results := findNamedRegexMatch("(?P<all>(?:\\[(?P<name>.+)\\])(?:\\((?P<url>.*)\\)))", text)
if len(results) != 3 {
return text
}
// build hyperlink ansi
hyperlink := fmt.Sprintf(a.hyperlink, results["url"], results["name"])
// replace original text by the new one
return strings.Replace(text, results["all"], hyperlink, 1)
}
func (a *ansiUtils) formatText(text string) string {
results := findAllNamedRegexMatch("(?P<context><(?P<format>[buis])>(?P<text>[^<]+)</[buis]>)", text)
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 "i":
formatted = fmt.Sprintf(a.italic, result["text"])
case "s":
formatted = fmt.Sprintf(a.strikethrough, result["text"])
}
text = strings.Replace(text, result["context"], formatted, 1)
}
return text
}
func (a *ansiUtils) carriageForward() string {
return fmt.Sprintf(a.right, 1000)
}
func (a *ansiUtils) getCursorForRightWrite(text string, offset int) string {
strippedLen := a.lenWithoutANSI(text) + -offset
return fmt.Sprintf(a.left, strippedLen)
}
func (a *ansiUtils) changeLine(numberOfLines int) string {
position := "B"
if numberOfLines < 0 {
position = "F"
numberOfLines = -numberOfLines
}
return fmt.Sprintf(a.linechange, numberOfLines, position)
}
func (a *ansiUtils) consolePwd(pwd string) string {
if strings.HasSuffix(pwd, ":") {
pwd += "\\"
}
return fmt.Sprintf(a.osc99, pwd)
}
func (a *ansiUtils) clearAfter() string {
return a.clearLine + a.clearBelow
}
func (a *ansiUtils) escapeText(text string) string {
// what to escape/replace is different per shell
for _, s := range a.shellReservedKeywords {
text = strings.ReplaceAll(text, s.text, s.replacement)
}
return text
}