refactor(writer): restructure terminal writer

This commit is contained in:
Jan De Dobbeleer 2024-07-01 07:37:54 +02:00 committed by Jan De Dobbeleer
parent 74aaa9b9a2
commit 8e2c8eb6b1
9 changed files with 256 additions and 226 deletions

View file

@ -3,10 +3,10 @@ package cli
import ( import (
"fmt" "fmt"
"github.com/jandedobbeleer/oh-my-posh/src/terminal"
"github.com/jandedobbeleer/oh-my-posh/src/engine" "github.com/jandedobbeleer/oh-my-posh/src/engine"
"github.com/jandedobbeleer/oh-my-posh/src/platform" "github.com/jandedobbeleer/oh-my-posh/src/platform"
"github.com/jandedobbeleer/oh-my-posh/src/shell" "github.com/jandedobbeleer/oh-my-posh/src/shell"
"github.com/jandedobbeleer/oh-my-posh/src/terminal"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@ -69,9 +69,9 @@ Exports the config to an image file using customized output options.`,
writerColors := cfg.MakeColors() writerColors := cfg.MakeColors()
writer := &terminal.Writer{ writer := &terminal.Writer{
TerminalBackground: shell.ConsoleBackgroundColor(env, cfg.TerminalBackground), BackgroundColor: shell.ConsoleBackgroundColor(env, cfg.TerminalBackground),
AnsiColors: writerColors, AnsiColors: writerColors,
TrueColor: env.CmdFlags.TrueColor, TrueColor: env.CmdFlags.TrueColor,
} }
writer.Init(shell.GENERIC) writer.Init(shell.GENERIC)
eng := &engine.Engine{ eng := &engine.Engine{

View file

@ -4,11 +4,11 @@ import (
"fmt" "fmt"
"time" "time"
"github.com/jandedobbeleer/oh-my-posh/src/terminal"
"github.com/jandedobbeleer/oh-my-posh/src/build" "github.com/jandedobbeleer/oh-my-posh/src/build"
"github.com/jandedobbeleer/oh-my-posh/src/engine" "github.com/jandedobbeleer/oh-my-posh/src/engine"
"github.com/jandedobbeleer/oh-my-posh/src/platform" "github.com/jandedobbeleer/oh-my-posh/src/platform"
"github.com/jandedobbeleer/oh-my-posh/src/shell" "github.com/jandedobbeleer/oh-my-posh/src/shell"
"github.com/jandedobbeleer/oh-my-posh/src/terminal"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@ -42,10 +42,10 @@ var debugCmd = &cobra.Command{
writerColors := cfg.MakeColors() writerColors := cfg.MakeColors()
writer := &terminal.Writer{ writer := &terminal.Writer{
TerminalBackground: shell.ConsoleBackgroundColor(env, cfg.TerminalBackground), BackgroundColor: shell.ConsoleBackgroundColor(env, cfg.TerminalBackground),
AnsiColors: writerColors, AnsiColors: writerColors,
Plain: plain, Plain: plain,
TrueColor: env.CmdFlags.TrueColor, TrueColor: env.CmdFlags.TrueColor,
} }
writer.Init(shell.GENERIC) writer.Init(shell.GENERIC)

View file

@ -69,9 +69,9 @@ func (b *Block) Init(env platform.Environment, writer *terminal.Writer) {
func (b *Block) InitPlain(env platform.Environment, config *Config) { func (b *Block) InitPlain(env platform.Environment, config *Config) {
b.writer = &terminal.Writer{ b.writer = &terminal.Writer{
TerminalBackground: shell.ConsoleBackgroundColor(env, config.TerminalBackground), BackgroundColor: shell.ConsoleBackgroundColor(env, config.TerminalBackground),
AnsiColors: config.MakeColors(), AnsiColors: config.MakeColors(),
TrueColor: env.Flags().TrueColor, TrueColor: env.Flags().TrueColor,
} }
b.writer.Init(shell.GENERIC) b.writer.Init(shell.GENERIC)

View file

@ -116,9 +116,9 @@ func engineRender() {
writerColors := cfg.MakeColors() writerColors := cfg.MakeColors()
writer := &terminal.Writer{ writer := &terminal.Writer{
TerminalBackground: shell.ConsoleBackgroundColor(env, cfg.TerminalBackground), BackgroundColor: shell.ConsoleBackgroundColor(env, cfg.TerminalBackground),
AnsiColors: writerColors, AnsiColors: writerColors,
TrueColor: env.CmdFlags.TrueColor, TrueColor: env.CmdFlags.TrueColor,
} }
writer.Init(shell.GENERIC) writer.Init(shell.GENERIC)
engine := &Engine{ engine := &Engine{

View file

@ -25,10 +25,10 @@ func New(flags *platform.Flags) *Engine {
flags.HasTransient = cfg.TransientPrompt != nil flags.HasTransient = cfg.TransientPrompt != nil
ansiWriter := &terminal.Writer{ ansiWriter := &terminal.Writer{
TerminalBackground: shell.ConsoleBackgroundColor(env, cfg.TerminalBackground), BackgroundColor: shell.ConsoleBackgroundColor(env, cfg.TerminalBackground),
AnsiColors: cfg.MakeColors(), AnsiColors: cfg.MakeColors(),
Plain: flags.Plain, Plain: flags.Plain,
TrueColor: env.CmdFlags.TrueColor, TrueColor: env.CmdFlags.TrueColor,
} }
ansiWriter.Init(env.Shell()) ansiWriter.Init(env.Shell())

View file

@ -112,7 +112,7 @@ func (e *Engine) Primary() string {
prompt += strings.Repeat(" ", space) prompt += strings.Repeat(" ", space)
prompt += e.rprompt prompt += e.rprompt
prompt += writer.RestoreCursorPosition() prompt += writer.RestoreCursorPosition()
prompt = e.Writer.FormatText(prompt) prompt = e.Writer.EscapeText(prompt)
e.write(prompt) e.write(prompt)
} }

View file

@ -39,11 +39,11 @@ func (w *Writer) RenderItermFeatures(features ITermFeatures, sh, pwd, user, host
continue continue
} }
result.WriteString(w.iTermPromptMark) result.WriteString(w.formats.iTermPromptMark)
case CurrentDir: case CurrentDir:
result.WriteString(fmt.Sprintf(w.iTermCurrentDir, pwd)) result.WriteString(fmt.Sprintf(w.formats.iTermCurrentDir, pwd))
case RemoteHost: case RemoteHost:
result.WriteString(fmt.Sprintf(w.iTermRemoteHost, user, host)) result.WriteString(fmt.Sprintf(w.formats.iTermRemoteHost, user, host))
} }
} }

View file

@ -13,6 +13,18 @@ func init() {
runewidth.DefaultCondition.EastAsianWidth = false runewidth.DefaultCondition.EastAsianWidth = false
} }
type Colors struct {
Background string `json:"background" toml:"background"`
Foreground string `json:"foreground" toml:"foreground"`
}
type style struct {
AnchorStart string
AnchorEnd string
Start string
End string
}
var ( var (
knownStyles = []*style{ knownStyles = []*style{
{AnchorStart: `<b>`, AnchorEnd: `</b>`, Start: "\x1b[1m", End: "\x1b[22m"}, {AnchorStart: `<b>`, AnchorEnd: `</b>`, Start: "\x1b[1m", End: "\x1b[22m"},
@ -24,20 +36,36 @@ var (
{AnchorStart: `<f>`, AnchorEnd: `</f>`, Start: "\x1b[5m", End: "\x1b[25m"}, {AnchorStart: `<f>`, AnchorEnd: `</f>`, Start: "\x1b[5m", End: "\x1b[25m"},
{AnchorStart: `<r>`, AnchorEnd: `</r>`, Start: "\x1b[7m", End: "\x1b[27m"}, {AnchorStart: `<r>`, AnchorEnd: `</r>`, Start: "\x1b[7m", End: "\x1b[27m"},
} }
resetStyle = &style{AnchorStart: "RESET", AnchorEnd: `</>`, End: "\x1b[0m"} resetStyle = &style{AnchorStart: "RESET", AnchorEnd: `</>`, End: "\x1b[0m"}
backgroundStyle = &style{AnchorStart: "BACKGROUND", AnchorEnd: `</>`, End: "\x1b[49m"} backgroundStyle = &style{AnchorStart: "BACKGROUND", AnchorEnd: `</>`, End: "\x1b[49m"}
) )
type style struct { type shellFormats struct {
AnchorStart string escape string
AnchorEnd string left string
Start string linechange string
End string clearBelow string
} clearLine string
type Colors struct { title string
Background string `json:"background" toml:"background"`
Foreground string `json:"foreground" toml:"foreground"` saveCursorPosition string
restoreCursorPosition string
osc99 string
osc7 string
osc51 string
escapeSequences map[rune]rune
hyperlinkStart string
hyperlinkCenter string
hyperlinkEnd string
iTermPromptMark string
iTermCurrentDir string
iTermRemoteHost string
} }
const ( const (
@ -54,11 +82,11 @@ const (
// Foreground takes the current segment's foreground color // Foreground takes the current segment's foreground color
Foreground = "foreground" Foreground = "foreground"
AnchorRegex = `^(?P<ANCHOR><(?P<FG>[^,<>]+)?,?(?P<BG>[^<>]+)?>)` AnchorRegex = `^(?P<ANCHOR><(?P<FG>[^,<>]+)?,?(?P<BG>[^<>]+)?>)`
colorise = "\x1b[%sm" colorise = "\x1b[%sm"
transparent = "\x1b[0m\x1b[%s;49m\x1b[7m" transparentStart = "\x1b[0m\x1b[%s;49m\x1b[7m"
transparentEnd = "\x1b[27m" transparentEnd = "\x1b[27m"
backgroundEnd = "\x1b[49m" backgroundEnd = "\x1b[49m"
AnsiRegex = "[\u001B\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[a-zA-Z\\d]*)*)?\u0007)|(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PRZcf-ntqry=><~]))" AnsiRegex = "[\u001B\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[a-zA-Z\\d]*)*)?\u0007)|(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PRZcf-ntqry=><~]))"
@ -78,10 +106,10 @@ const (
// Writer writes colorized ANSI strings // Writer writes colorized ANSI strings
type Writer struct { type Writer struct {
TerminalBackground string BackgroundColor string
Colors *Colors CurrentColors *Colors
ParentColors []*Colors ParentColors []*Colors
AnsiColors ColorString AnsiColors ColorString
Plain bool Plain bool
TrueColor bool TrueColor bool
@ -90,112 +118,96 @@ type Writer struct {
builder strings.Builder builder strings.Builder
length int length int
foreground Color foregroundColor Color
background Color backgroundColor Color
current ColorHistory currentColor ColorHistory
runes []rune runes []rune
transparent bool
invisible bool
hyperlink bool
shell string isTransparent bool
format string isInvisible bool
left string isHyperlink bool
title string
linechange string
clearBelow string
clearLine string
saveCursorPosition string
restoreCursorPosition string
escapeLeft string
escapeRight string
osc99 string
osc7 string
osc51 string
lastRune rune lastRune rune
escapeSequences map[rune]rune
hyperlinkStart string shellName string
hyperlinkCenter string
hyperlinkEnd string
iTermPromptMark string formats *shellFormats
iTermCurrentDir string
iTermRemoteHost string
} }
func (w *Writer) Init(shellName string) { func (w *Writer) Init(shellName string) {
w.shell = shellName w.shellName = shellName
w.format = "%s"
switch w.shell { switch w.shellName {
case shell.BASH: case shell.BASH:
w.format = "\\[%s\\]" w.formats = &shellFormats{
w.linechange = "\\[\x1b[%d%s\\]" escape: "\\[%s\\]",
w.left = "\\[\x1b[%dD\\]" linechange: "\\[\x1b[%d%s\\]",
w.clearBelow = "\\[\x1b[0J\\]" left: "\\[\x1b[%dD\\]",
w.clearLine = "\\[\x1b[K\\]" clearBelow: "\\[\x1b[0J\\]",
w.saveCursorPosition = "\\[\x1b7\\]" clearLine: "\\[\x1b[K\\]",
w.restoreCursorPosition = "\\[\x1b8\\]" saveCursorPosition: "\\[\x1b7\\]",
w.title = "\\[\x1b]0;%s\007\\]" restoreCursorPosition: "\\[\x1b8\\]",
w.escapeLeft = "\\[" title: "\\[\x1b]0;%s\007\\]",
w.escapeRight = "\\]" hyperlinkStart: "\\[\x1b]8;;",
w.hyperlinkStart = "\\[\x1b]8;;" hyperlinkCenter: "\x1b\\\\\\]",
w.hyperlinkCenter = "\x1b\\\\\\]" hyperlinkEnd: "\\[\x1b]8;;\x1b\\\\\\]",
w.hyperlinkEnd = "\\[\x1b]8;;\x1b\\\\\\]" osc99: "\\[\x1b]9;9;%s\x1b\\\\\\]",
w.osc99 = "\\[\x1b]9;9;%s\x1b\\\\\\]" osc7: "\\[\x1b]7;file://%s/%s\x1b\\\\\\]",
w.osc7 = "\\[\x1b]7;file://%s/%s\x1b\\\\\\]" osc51: "\\[\x1b]51;A;%s@%s:%s\x1b\\\\\\]",
w.osc51 = "\\[\x1b]51;A;%s@%s:%s\x1b\\\\\\]" iTermPromptMark: "\\[$(iterm2_prompt_mark)\\]",
w.iTermPromptMark = "\\[$(iterm2_prompt_mark)\\]" iTermCurrentDir: "\\[\x1b]1337;CurrentDir=%s\x07\\]",
w.iTermCurrentDir = "\\[\x1b]1337;CurrentDir=%s\x07\\]" iTermRemoteHost: "\\[\x1b]1337;RemoteHost=%s@%s\x07\\]",
w.iTermRemoteHost = "\\[\x1b]1337;RemoteHost=%s@%s\x07\\]" escapeSequences: map[rune]rune{
w.escapeSequences = map[rune]rune{ 96: 92, // backtick
96: 92, // backtick 92: 92, // backslash
92: 92, // backslash },
} }
case shell.ZSH, shell.TCSH: case shell.ZSH, shell.TCSH:
w.format = "%%{%s%%}" w.formats = &shellFormats{
w.linechange = "%%{\x1b[%d%s%%}" escape: "%%{%s%%}",
w.left = "%%{\x1b[%dD%%}" linechange: "%%{\x1b[%d%s%%}",
w.clearBelow = "%{\x1b[0J%}" left: "%%{\x1b[%dD%%}",
w.clearLine = "%{\x1b[K%}" clearBelow: "%{\x1b[0J%}",
w.saveCursorPosition = "%{\x1b7%}" clearLine: "%{\x1b[K%}",
w.restoreCursorPosition = "%{\x1b8%}" saveCursorPosition: "%{\x1b7%}",
w.title = "%%{\x1b]0;%s\007%%}" restoreCursorPosition: "%{\x1b8%}",
w.escapeLeft = "%{" title: "%%{\x1b]0;%s\007%%}",
w.escapeRight = "%}" hyperlinkStart: "%{\x1b]8;;",
w.hyperlinkStart = "%{\x1b]8;;" hyperlinkCenter: "\x1b\\%}",
w.hyperlinkCenter = "\x1b\\%}" hyperlinkEnd: "%{\x1b]8;;\x1b\\%}",
w.hyperlinkEnd = "%{\x1b]8;;\x1b\\%}" osc99: "%%{\x1b]9;9;%s\x1b\\%%}",
w.osc99 = "%%{\x1b]9;9;%s\x1b\\%%}" osc7: "%%{\x1b]7;file://%s/%s\x1b\\%%}",
w.osc7 = "%%{\x1b]7;file://%s/%s\x1b\\%%}" osc51: "%%{\x1b]51;A%s@%s:%s\x1b\\%%}",
w.osc51 = "%%{\x1b]51;A%s@%s:%s\x1b\\%%}" iTermPromptMark: "%{$(iterm2_prompt_mark)%}",
w.iTermPromptMark = "%{$(iterm2_prompt_mark)%}" iTermCurrentDir: "%%{\x1b]1337;CurrentDir=%s\x07%%}",
w.iTermCurrentDir = "%%{\x1b]1337;CurrentDir=%s\x07%%}" iTermRemoteHost: "%%{\x1b]1337;RemoteHost=%s@%s\x07%%}",
w.iTermRemoteHost = "%%{\x1b]1337;RemoteHost=%s@%s\x07%%}" }
default: default:
w.linechange = "\x1b[%d%s" w.formats = &shellFormats{
w.left = "\x1b[%dD" escape: "%s",
w.clearBelow = "\x1b[0J" linechange: "\x1b[%d%s",
w.clearLine = "\x1b[K" left: "\x1b[%dD",
w.saveCursorPosition = "\x1b7" clearBelow: "\x1b[0J",
w.restoreCursorPosition = "\x1b8" clearLine: "\x1b[K",
w.title = "\x1b]0;%s\007" saveCursorPosition: "\x1b7",
// when in fish on Linux, it seems hyperlinks ending with \\ print a \ restoreCursorPosition: "\x1b8",
// unlike on macOS. However, this is a fish bug, so do not try to fix it here: title: "\x1b]0;%s\007",
// https://github.com/JanDeDobbeleer/oh-my-posh/pull/3288#issuecomment-1369137068 // when in fish on Linux, it seems hyperlinks ending with \\ print a \
w.hyperlinkStart = "\x1b]8;;" // unlike on macOS. However, this is a fish bug, so do not try to fix it here:
w.hyperlinkCenter = "\x1b\\" // https://github.com/JanDeDobbeleer/oh-my-posh/pull/3288#issuecomment-1369137068
w.hyperlinkEnd = "\x1b]8;;\x1b\\" hyperlinkStart: "\x1b]8;;",
w.osc99 = "\x1b]9;9;%s\x1b\\" hyperlinkCenter: "\x1b\\",
w.osc7 = "\x1b]7;file://%s/%s\x1b\\" hyperlinkEnd: "\x1b]8;;\x1b\\",
w.osc51 = "\x1b]51;A%s@%s:%s\x1b\\" osc99: "\x1b]9;9;%s\x1b\\",
w.iTermCurrentDir = "\x1b]1337;CurrentDir=%s\x07" osc7: "\x1b]7;file://%s/%s\x1b\\",
w.iTermRemoteHost = "\x1b]1337;RemoteHost=%s@%s\x07" osc51: "\x1b]51;A%s@%s:%s\x1b\\",
iTermCurrentDir: "\x1b]1337;CurrentDir=%s\x07",
iTermRemoteHost: "\x1b]1337;RemoteHost=%s@%s\x07",
}
} }
if shellName == shell.ZSH { if shellName == shell.ZSH {
w.escapeSequences = map[rune]rune{ w.formats.escapeSequences = map[rune]rune{
96: 92, // backtick 96: 92, // backtick
37: 37, // % 37: 37, // %
} }
@ -203,7 +215,7 @@ func (w *Writer) Init(shellName string) {
} }
func (w *Writer) SetColors(background, foreground string) { func (w *Writer) SetColors(background, foreground string) {
w.Colors = &Colors{ w.CurrentColors = &Colors{
Background: background, Background: background,
Foreground: foreground, Foreground: foreground,
} }
@ -213,6 +225,7 @@ func (w *Writer) SetParentColors(background, foreground string) {
if w.ParentColors == nil { if w.ParentColors == nil {
w.ParentColors = make([]*Colors, 0) w.ParentColors = make([]*Colors, 0)
} }
w.ParentColors = append([]*Colors{{ w.ParentColors = append([]*Colors{{
Background: background, Background: background,
Foreground: foreground, Foreground: foreground,
@ -231,7 +244,7 @@ func (w *Writer) ChangeLine(numberOfLines int) string {
numberOfLines = -numberOfLines numberOfLines = -numberOfLines
} }
return fmt.Sprintf(w.linechange, numberOfLines, position) return fmt.Sprintf(w.formats.linechange, numberOfLines, position)
} }
func (w *Writer) ConsolePwd(pwdType, userName, hostName, pwd string) string { func (w *Writer) ConsolePwd(pwdType, userName, hostName, pwd string) string {
@ -245,13 +258,13 @@ func (w *Writer) ConsolePwd(pwdType, userName, hostName, pwd string) string {
switch pwdType { switch pwdType {
case OSC7: case OSC7:
return fmt.Sprintf(w.osc7, hostName, pwd) return fmt.Sprintf(w.formats.osc7, hostName, pwd)
case OSC51: case OSC51:
return fmt.Sprintf(w.osc51, userName, hostName, pwd) return fmt.Sprintf(w.formats.osc51, userName, hostName, pwd)
case OSC99: case OSC99:
fallthrough fallthrough
default: default:
return fmt.Sprintf(w.osc99, pwd) return fmt.Sprintf(w.formats.osc99, pwd)
} }
} }
@ -260,17 +273,18 @@ func (w *Writer) ClearAfter() string {
return "" return ""
} }
return w.clearLine + w.clearBelow return w.formats.clearLine + w.formats.clearBelow
} }
func (w *Writer) FormatTitle(title string) string { func (w *Writer) FormatTitle(title string) string {
title = w.trimAnsi(title) title = w.trimAnsi(title)
if w.Plain { if w.Plain {
return title return title
} }
// we have to do this to prevent bash/zsh from misidentifying escape sequences // we have to do this to prevent bash/zsh from misidentifying escape sequences
switch w.shell { switch w.shellName {
case shell.BASH: case shell.BASH:
title = strings.NewReplacer("`", "\\`", `\`, `\\`).Replace(title) title = strings.NewReplacer("`", "\\`", `\`, `\\`).Replace(title)
case shell.ZSH: case shell.ZSH:
@ -280,40 +294,42 @@ func (w *Writer) FormatTitle(title string) string {
return "" return ""
} }
return fmt.Sprintf(w.title, title) return fmt.Sprintf(w.formats.title, title)
} }
func (w *Writer) FormatText(text string) string { func (w *Writer) EscapeText(text string) string {
return fmt.Sprintf(w.format, text) return fmt.Sprintf(w.formats.escape, text)
} }
func (w *Writer) SaveCursorPosition() string { func (w *Writer) SaveCursorPosition() string {
return w.saveCursorPosition return w.formats.saveCursorPosition
} }
func (w *Writer) RestoreCursorPosition() string { func (w *Writer) RestoreCursorPosition() string {
return w.restoreCursorPosition return w.formats.restoreCursorPosition
} }
func (w *Writer) PromptStart() string { func (w *Writer) PromptStart() string {
return fmt.Sprintf(w.format, "\x1b]133;A\007") return fmt.Sprintf(w.formats.escape, "\x1b]133;A\007")
} }
func (w *Writer) CommandStart() string { func (w *Writer) CommandStart() string {
return fmt.Sprintf(w.format, "\x1b]133;B\007") return fmt.Sprintf(w.formats.escape, "\x1b]133;B\007")
} }
func (w *Writer) CommandFinished(code int, ignore bool) string { func (w *Writer) CommandFinished(code int, ignore bool) string {
if ignore { if ignore {
return fmt.Sprintf(w.format, "\x1b]133;D\007") return fmt.Sprintf(w.formats.escape, "\x1b]133;D\007")
} }
mark := fmt.Sprintf("\x1b]133;D;%d\007", code) mark := fmt.Sprintf("\x1b]133;D;%d\007", code)
return fmt.Sprintf(w.format, mark)
return fmt.Sprintf(w.formats.escape, mark)
} }
func (w *Writer) LineBreak() string { func (w *Writer) LineBreak() string {
cr := fmt.Sprintf(w.left, 1000) cr := fmt.Sprintf(w.formats.left, 1000)
lf := fmt.Sprintf(w.linechange, 1, "B") lf := fmt.Sprintf(w.formats.linechange, 1, "B")
return cr + lf return cr + lf
} }
@ -322,11 +338,13 @@ func (w *Writer) Write(background, foreground, text string) {
return return
} }
w.background, w.foreground = w.asAnsiColors(background, foreground) w.backgroundColor, w.foregroundColor = w.asAnsiColors(background, foreground)
// default to white foreground // default to white foreground
if w.foreground.IsEmpty() { if w.foregroundColor.IsEmpty() {
w.foreground = w.AnsiColors.ToColor("white", false, w.TrueColor) w.foregroundColor = w.AnsiColors.ToColor("white", false, w.TrueColor)
} }
// validate if we start with a color override // validate if we start with a color override
match := regex.FindNamedRegexMatch(AnchorRegex, text) match := regex.FindNamedRegexMatch(AnchorRegex, text)
if len(match) != 0 && match[ANCHOR] != hyperLinkStart { if len(match) != 0 && match[ANCHOR] != hyperLinkStart {
@ -335,12 +353,13 @@ func (w *Writer) Write(background, foreground, text string) {
if match[ANCHOR] != style.AnchorStart { if match[ANCHOR] != style.AnchorStart {
continue continue
} }
w.writeEscapedAnsiString(style.Start) w.writeEscapedAnsiString(style.Start)
colorOverride = false colorOverride = false
} }
if colorOverride { if colorOverride {
w.current.Add(w.asAnsiColors(match[BG], match[FG])) w.currentColor.Add(w.asAnsiColors(match[BG], match[FG]))
} }
} }
@ -348,8 +367,8 @@ func (w *Writer) Write(background, foreground, text string) {
// print the hyperlink part AFTER the coloring // print the hyperlink part AFTER the coloring
if match[ANCHOR] == hyperLinkStart { if match[ANCHOR] == hyperLinkStart {
w.hyperlink = true w.isHyperlink = true
w.builder.WriteString(w.hyperlinkStart) w.builder.WriteString(w.formats.hyperlinkStart)
} }
text = text[len(match[ANCHOR]):] text = text[len(match[ANCHOR]):]
@ -371,15 +390,15 @@ func (w *Writer) Write(background, foreground, text string) {
// check for hyperlinks first // check for hyperlinks first
switch match[ANCHOR] { switch match[ANCHOR] {
case hyperLinkStart: case hyperLinkStart:
w.hyperlink = true w.isHyperlink = true
i += len([]rune(match[ANCHOR])) - 1 i += len([]rune(match[ANCHOR])) - 1
w.builder.WriteString(w.hyperlinkStart) w.builder.WriteString(w.formats.hyperlinkStart)
continue continue
case hyperLinkText: case hyperLinkText:
w.hyperlink = false w.isHyperlink = false
i += len([]rune(match[ANCHOR])) - 1 i += len([]rune(match[ANCHOR])) - 1
hyperlinkTextPosition = i hyperlinkTextPosition = i
w.builder.WriteString(w.hyperlinkCenter) w.builder.WriteString(w.formats.hyperlinkCenter)
continue continue
case hyperLinkTextEnd: case hyperLinkTextEnd:
// this implies there's no text in the hyperlink // this implies there's no text in the hyperlink
@ -391,7 +410,7 @@ func (w *Writer) Write(background, foreground, text string) {
continue continue
case hyperLinkEnd: case hyperLinkEnd:
i += len([]rune(match[ANCHOR])) - 1 i += len([]rune(match[ANCHOR])) - 1
w.builder.WriteString(w.hyperlinkEnd) w.builder.WriteString(w.formats.hyperlinkEnd)
continue continue
} }
@ -407,7 +426,7 @@ func (w *Writer) Write(background, foreground, text string) {
w.writeEscapedAnsiString(resetStyle.End) w.writeEscapedAnsiString(resetStyle.End)
// pop last color from the stack // pop last color from the stack
w.current.Pop() w.currentColor.Pop()
} }
func (w *Writer) String() (string, int) { func (w *Writer) String() (string, int) {
@ -424,8 +443,8 @@ func (w *Writer) writeEscapedAnsiString(text string) {
return return
} }
if len(w.format) != 0 { if len(w.formats.escape) != 0 {
text = fmt.Sprintf(w.format, text) text = fmt.Sprintf(w.formats.escape, text)
} }
w.builder.WriteString(text) w.builder.WriteString(text)
@ -436,17 +455,17 @@ func (w *Writer) getAnsiFromColorString(colorString string, isBackground bool) C
} }
func (w *Writer) write(s rune) { func (w *Writer) write(s rune) {
if w.invisible { if w.isInvisible {
return return
} }
if w.hyperlink { if w.isHyperlink {
w.builder.WriteRune(s) w.builder.WriteRune(s)
return return
} }
if !w.Interactive { if !w.Interactive {
for special, escape := range w.escapeSequences { for special, escape := range w.formats.escapeSequences {
if s == special && w.lastRune != escape { if s == special && w.lastRune != escape {
w.builder.WriteRune(escape) w.builder.WriteRune(escape)
} }
@ -460,28 +479,28 @@ func (w *Writer) write(s rune) {
func (w *Writer) writeSegmentColors() { func (w *Writer) writeSegmentColors() {
// use correct starting colors // use correct starting colors
bg := w.background bg := w.backgroundColor
fg := w.foreground fg := w.foregroundColor
if !w.current.Background().IsEmpty() { if !w.currentColor.Background().IsEmpty() {
bg = w.current.Background() bg = w.currentColor.Background()
} }
if !w.current.Foreground().IsEmpty() { if !w.currentColor.Foreground().IsEmpty() {
fg = w.current.Foreground() fg = w.currentColor.Foreground()
} }
// ignore processing fully tranparent colors // ignore processing fully tranparent colors
w.invisible = fg.IsTransparent() && bg.IsTransparent() w.isInvisible = fg.IsTransparent() && bg.IsTransparent()
if w.invisible { if w.isInvisible {
return return
} }
if fg.IsTransparent() && len(w.TerminalBackground) != 0 { //nolint: gocritic if fg.IsTransparent() && len(w.BackgroundColor) != 0 { //nolint: gocritic
background := w.getAnsiFromColorString(w.TerminalBackground, false) background := w.getAnsiFromColorString(w.BackgroundColor, false)
w.writeEscapedAnsiString(fmt.Sprintf(colorise, background)) w.writeEscapedAnsiString(fmt.Sprintf(colorise, background))
w.writeEscapedAnsiString(fmt.Sprintf(colorise, bg.ToForeground())) w.writeEscapedAnsiString(fmt.Sprintf(colorise, bg.ToForeground()))
} else if fg.IsTransparent() && !bg.IsEmpty() { } else if fg.IsTransparent() && !bg.IsEmpty() {
w.transparent = true w.isTransparent = true
w.writeEscapedAnsiString(fmt.Sprintf(transparent, bg)) w.writeEscapedAnsiString(fmt.Sprintf(transparentStart, bg))
} else { } else {
if !bg.IsEmpty() && !bg.IsTransparent() { if !bg.IsEmpty() && !bg.IsTransparent() {
w.writeEscapedAnsiString(fmt.Sprintf(colorise, bg)) w.writeEscapedAnsiString(fmt.Sprintf(colorise, bg))
@ -492,7 +511,7 @@ func (w *Writer) writeSegmentColors() {
} }
// set current colors // set current colors
w.current.Add(bg, fg) w.currentColor.Add(bg, fg)
} }
func (w *Writer) writeArchorOverride(match map[string]string, background string, i int) int { func (w *Writer) writeArchorOverride(match map[string]string, background string, i int) int {
@ -522,45 +541,45 @@ func (w *Writer) writeArchorOverride(match map[string]string, background string,
bg, fg := w.asAnsiColors(match[BG], match[FG]) bg, fg := w.asAnsiColors(match[BG], match[FG])
// ignore processing fully tranparent colors // ignore processing fully tranparent colors
w.invisible = fg.IsTransparent() && bg.IsTransparent() w.isInvisible = fg.IsTransparent() && bg.IsTransparent()
if w.invisible { if w.isInvisible {
return position return position
} }
// make sure we have colors // make sure we have colors
if fg.IsEmpty() { if fg.IsEmpty() {
fg = w.foreground fg = w.foregroundColor
} }
if bg.IsEmpty() { if bg.IsEmpty() {
bg = w.background bg = w.backgroundColor
} }
w.current.Add(bg, fg) w.currentColor.Add(bg, fg)
if w.current.Foreground().IsTransparent() && len(w.TerminalBackground) != 0 { if w.currentColor.Foreground().IsTransparent() && len(w.BackgroundColor) != 0 {
background := w.getAnsiFromColorString(w.TerminalBackground, false) background := w.getAnsiFromColorString(w.BackgroundColor, false)
w.writeEscapedAnsiString(fmt.Sprintf(colorise, background)) w.writeEscapedAnsiString(fmt.Sprintf(colorise, background))
w.writeEscapedAnsiString(fmt.Sprintf(colorise, w.current.Background().ToForeground())) w.writeEscapedAnsiString(fmt.Sprintf(colorise, w.currentColor.Background().ToForeground()))
return position return position
} }
if w.current.Foreground().IsTransparent() && !w.current.Background().IsTransparent() { if w.currentColor.Foreground().IsTransparent() && !w.currentColor.Background().IsTransparent() {
w.transparent = true w.isTransparent = true
w.writeEscapedAnsiString(fmt.Sprintf(transparent, w.current.Background())) w.writeEscapedAnsiString(fmt.Sprintf(transparentStart, w.currentColor.Background()))
return position return position
} }
if w.current.Background() != w.background { if w.currentColor.Background() != w.backgroundColor {
// end the colors in case we have a transparent background // end the colors in case we have a transparent background
if w.current.Background().IsTransparent() { if w.currentColor.Background().IsTransparent() {
w.writeEscapedAnsiString(backgroundEnd) w.writeEscapedAnsiString(backgroundEnd)
} else { } else {
w.writeEscapedAnsiString(fmt.Sprintf(colorise, w.current.Background())) w.writeEscapedAnsiString(fmt.Sprintf(colorise, w.currentColor.Background()))
} }
} }
if w.current.Foreground() != w.foreground { if w.currentColor.Foreground() != w.foregroundColor {
w.writeEscapedAnsiString(fmt.Sprintf(colorise, w.current.Foreground())) w.writeEscapedAnsiString(fmt.Sprintf(colorise, w.currentColor.Foreground()))
} }
return position return position
@ -572,23 +591,23 @@ func (w *Writer) endColorOverride(position int) int {
// do not restore colors at the end of the string, we print it anyways // do not restore colors at the end of the string, we print it anyways
if position == len(w.runes)-1 { if position == len(w.runes)-1 {
w.current.Pop() w.currentColor.Pop()
return position return position
} }
// reset colors to previous when we have more than 1 in stack // reset colors to previous when we have more than 1 in stack
// as soon as we have more than 1, we can pop the last one // as soon as we have more than 1, we can pop the last one
// and print the previous override as it wasn't ended yet // and print the previous override as it wasn't ended yet
if w.current.Len() > 1 { if w.currentColor.Len() > 1 {
fg := w.current.Foreground() fg := w.currentColor.Foreground()
bg := w.current.Background() bg := w.currentColor.Background()
w.current.Pop() w.currentColor.Pop()
previousBg := w.current.Background() previousBg := w.currentColor.Background()
previousFg := w.current.Foreground() previousFg := w.currentColor.Foreground()
if w.transparent { if w.isTransparent {
w.writeEscapedAnsiString(transparentEnd) w.writeEscapedAnsiString(transparentEnd)
} }
@ -609,30 +628,30 @@ func (w *Writer) endColorOverride(position int) int {
} }
// pop the last colors from the stack // pop the last colors from the stack
defer w.current.Pop() defer w.currentColor.Pop()
// do not reset when colors are identical // do not reset when colors are identical
if w.current.Background() == w.background && w.current.Foreground() == w.foreground { if w.currentColor.Background() == w.backgroundColor && w.currentColor.Foreground() == w.foregroundColor {
return position return position
} }
if w.transparent { if w.isTransparent {
w.writeEscapedAnsiString(transparentEnd) w.writeEscapedAnsiString(transparentEnd)
} }
if w.background.IsClear() { if w.backgroundColor.IsClear() {
w.writeEscapedAnsiString(backgroundStyle.End) w.writeEscapedAnsiString(backgroundStyle.End)
} }
if w.current.Background() != w.background && !w.background.IsClear() { if w.currentColor.Background() != w.backgroundColor && !w.backgroundColor.IsClear() {
w.writeEscapedAnsiString(fmt.Sprintf(colorise, w.background)) w.writeEscapedAnsiString(fmt.Sprintf(colorise, w.backgroundColor))
} }
if (w.current.Foreground() != w.foreground || w.transparent) && !w.foreground.IsClear() { if (w.currentColor.Foreground() != w.foregroundColor || w.isTransparent) && !w.foregroundColor.IsClear() {
w.writeEscapedAnsiString(fmt.Sprintf(colorise, w.foreground)) w.writeEscapedAnsiString(fmt.Sprintf(colorise, w.foregroundColor))
} }
w.transparent = false w.isTransparent = false
return position return position
} }
@ -640,14 +659,19 @@ func (w *Writer) asAnsiColors(background, foreground string) (Color, Color) {
if len(background) == 0 { if len(background) == 0 {
background = Background background = Background
} }
if len(foreground) == 0 { if len(foreground) == 0 {
foreground = Foreground foreground = Foreground
} }
background = w.expandKeyword(background) background = w.expandKeyword(background)
foreground = w.expandKeyword(foreground) foreground = w.expandKeyword(foreground)
inverted := foreground == Transparent && len(background) != 0 inverted := foreground == Transparent && len(background) != 0
backgroundAnsi := w.getAnsiFromColorString(background, !inverted) backgroundAnsi := w.getAnsiFromColorString(background, !inverted)
foregroundAnsi := w.getAnsiFromColorString(foreground, false) foregroundAnsi := w.getAnsiFromColorString(foreground, false)
return backgroundAnsi, foregroundAnsi return backgroundAnsi, foregroundAnsi
} }
@ -678,30 +702,36 @@ func (w *Writer) expandKeyword(keyword string) string {
return keyword return keyword
} }
} }
if len(keyword) == 0 { if len(keyword) == 0 {
return Transparent return Transparent
} }
return keyword return keyword
} }
resolveKeyword := func(keyword string) string { resolveKeyword := func(keyword string) string {
switch { switch {
case keyword == Background && w.Colors != nil: case keyword == Background && w.CurrentColors != nil:
return w.Colors.Background return w.CurrentColors.Background
case keyword == Foreground && w.Colors != nil: case keyword == Foreground && w.CurrentColors != nil:
return w.Colors.Foreground return w.CurrentColors.Foreground
case (keyword == ParentBackground || keyword == ParentForeground) && w.ParentColors != nil: case (keyword == ParentBackground || keyword == ParentForeground) && w.ParentColors != nil:
return resolveParentColor(keyword) return resolveParentColor(keyword)
default: default:
return Transparent return Transparent
} }
} }
for ok := w.isKeyword(keyword); ok; ok = w.isKeyword(keyword) { for ok := w.isKeyword(keyword); ok; ok = w.isKeyword(keyword) {
resolved := resolveKeyword(keyword) resolved := resolveKeyword(keyword)
if resolved == keyword { if resolved == keyword {
break break
} }
keyword = resolved keyword = resolved
} }
return keyword return keyword
} }

View file

@ -222,11 +222,11 @@ func TestWriteANSIColors(t *testing.T) {
for _, tc := range cases { for _, tc := range cases {
renderer := &Writer{ renderer := &Writer{
ParentColors: []*Colors{tc.Parent}, ParentColors: []*Colors{tc.Parent},
Colors: tc.Colors, CurrentColors: tc.Colors,
TerminalBackground: tc.TerminalBackground, BackgroundColor: tc.TerminalBackground,
AnsiColors: &DefaultColors{}, AnsiColors: &DefaultColors{},
TrueColor: true, TrueColor: true,
} }
renderer.Init(shell.GENERIC) renderer.Init(shell.GENERIC)
renderer.Write(tc.Colors.Background, tc.Colors.Foreground, tc.Input) renderer.Write(tc.Colors.Background, tc.Colors.Foreground, tc.Input)
@ -276,9 +276,9 @@ func TestWriteLength(t *testing.T) {
for _, tc := range cases { for _, tc := range cases {
renderer := &Writer{ renderer := &Writer{
ParentColors: []*Colors{}, ParentColors: []*Colors{},
Colors: tc.Colors, CurrentColors: tc.Colors,
AnsiColors: &DefaultColors{}, AnsiColors: &DefaultColors{},
} }
renderer.Init(shell.GENERIC) renderer.Init(shell.GENERIC)
renderer.Write(tc.Colors.Background, tc.Colors.Foreground, tc.Input) renderer.Write(tc.Colors.Background, tc.Colors.Foreground, tc.Input)