refactor: ansi module cleanup

This commit is contained in:
Jan De Dobbeleer 2021-04-20 21:30:46 +02:00 committed by Jan De Dobbeleer
parent c24ca82f17
commit b11b69f0e8
12 changed files with 162 additions and 182 deletions

View file

@ -9,7 +9,7 @@ const (
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=><~]))"
) )
type ansiFormats struct { type ansiUtils struct {
shell string shell string
linechange string linechange string
left string left string
@ -32,7 +32,7 @@ type ansiFormats struct {
strikethrough string strikethrough string
} }
func (a *ansiFormats) init(shell string) { func (a *ansiUtils) init(shell string) {
a.shell = shell a.shell = shell
switch shell { switch shell {
case zsh: case zsh:
@ -98,7 +98,7 @@ func (a *ansiFormats) init(shell string) {
} }
} }
func (a *ansiFormats) lenWithoutANSI(text string) int { func (a *ansiUtils) lenWithoutANSI(text string) int {
if len(text) == 0 { if len(text) == 0 {
return 0 return 0
} }
@ -119,7 +119,7 @@ func (a *ansiFormats) lenWithoutANSI(text string) int {
return len(runeText) return len(runeText)
} }
func (a *ansiFormats) generateHyperlink(text string) string { func (a *ansiUtils) generateHyperlink(text string) string {
// hyperlink matching // hyperlink matching
results := findNamedRegexMatch("(?P<all>(?:\\[(?P<name>.+)\\])(?:\\((?P<url>.*)\\)))", text) results := findNamedRegexMatch("(?P<all>(?:\\[(?P<name>.+)\\])(?:\\((?P<url>.*)\\)))", text)
if len(results) != 3 { if len(results) != 3 {
@ -131,7 +131,7 @@ func (a *ansiFormats) generateHyperlink(text string) string {
return strings.Replace(text, results["all"], hyperlink, 1) return strings.Replace(text, results["all"], hyperlink, 1)
} }
func (a *ansiFormats) formatText(text string) string { func (a *ansiUtils) formatText(text string) string {
results := findAllNamedRegexMatch("(?P<context><(?P<format>[buis])>(?P<text>[^<]+)</[buis]>)", text) results := findAllNamedRegexMatch("(?P<context><(?P<format>[buis])>(?P<text>[^<]+)</[buis]>)", text)
for _, result := range results { for _, result := range results {
var formatted string var formatted string
@ -149,3 +149,25 @@ func (a *ansiFormats) formatText(text string) string {
} }
return text return text
} }
func (a *ansiUtils) carriageForward() string {
return fmt.Sprintf(a.left, 1000)
}
func (a *ansiUtils) getCursorForRightWrite(text string, offset int) string {
strippedLen := a.lenWithoutANSI(text) + -offset
return fmt.Sprintf(a.right, 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 {
return fmt.Sprintf(a.osc99, pwd)
}

View file

@ -43,10 +43,16 @@ func getColorFromName(colorName string, isBackground bool) (string, error) {
return "", errors.New("color name does not exist") return "", errors.New("color name does not exist")
} }
type colorWriter interface {
write(background, foreground, text string)
string() string
reset()
}
// AnsiColor writes colorized strings // AnsiColor writes colorized strings
type AnsiColor struct { type AnsiColor struct {
builder strings.Builder builder strings.Builder
formats *ansiFormats ansi *ansiUtils
terminalBackground string terminalBackground string
} }
@ -75,24 +81,24 @@ func (a *AnsiColor) writeColoredText(background, foreground, text string) {
if foreground == Transparent && background != "" && a.terminalBackground != "" { if foreground == Transparent && background != "" && a.terminalBackground != "" {
bgAnsiColor := a.getAnsiFromColorString(background, true) bgAnsiColor := a.getAnsiFromColorString(background, true)
fgAnsiColor := a.getAnsiFromColorString(a.terminalBackground, false) fgAnsiColor := a.getAnsiFromColorString(a.terminalBackground, false)
coloredText := fmt.Sprintf(a.formats.colorFull, bgAnsiColor, fgAnsiColor, text) coloredText := fmt.Sprintf(a.ansi.colorFull, bgAnsiColor, fgAnsiColor, text)
a.builder.WriteString(coloredText) a.builder.WriteString(coloredText)
return return
} }
if foreground == Transparent && background != "" { if foreground == Transparent && background != "" {
ansiColor := a.getAnsiFromColorString(background, false) ansiColor := a.getAnsiFromColorString(background, false)
coloredText := fmt.Sprintf(a.formats.colorTransparent, ansiColor, text) coloredText := fmt.Sprintf(a.ansi.colorTransparent, ansiColor, text)
a.builder.WriteString(coloredText) a.builder.WriteString(coloredText)
return return
} else if background == "" || background == Transparent { } else if background == "" || background == Transparent {
ansiColor := a.getAnsiFromColorString(foreground, false) ansiColor := a.getAnsiFromColorString(foreground, false)
coloredText := fmt.Sprintf(a.formats.colorSingle, ansiColor, text) coloredText := fmt.Sprintf(a.ansi.colorSingle, ansiColor, text)
a.builder.WriteString(coloredText) a.builder.WriteString(coloredText)
return return
} }
bgAnsiColor := a.getAnsiFromColorString(background, true) bgAnsiColor := a.getAnsiFromColorString(background, true)
fgAnsiColor := a.getAnsiFromColorString(foreground, false) fgAnsiColor := a.getAnsiFromColorString(foreground, false)
coloredText := fmt.Sprintf(a.formats.colorFull, bgAnsiColor, fgAnsiColor, text) coloredText := fmt.Sprintf(a.ansi.colorFull, bgAnsiColor, fgAnsiColor, text)
a.builder.WriteString(coloredText) a.builder.WriteString(coloredText)
} }
@ -102,7 +108,7 @@ func (a *AnsiColor) writeAndRemoveText(background, foreground, text, textToRemov
} }
func (a *AnsiColor) write(background, foreground, text string) { func (a *AnsiColor) write(background, foreground, text string) {
text = a.formats.formatText(text) text = a.ansi.formatText(text)
// first we match for any potentially valid colors enclosed in <> // first we match for any potentially valid colors enclosed in <>
match := findAllNamedRegexMatch(`<(?P<foreground>[^,>]+)?,?(?P<background>[^>]+)?>(?P<content>[^<]*)<\/>`, text) match := findAllNamedRegexMatch(`<(?P<foreground>[^,>]+)?,?(?P<background>[^>]+)?>(?P<content>[^<]*)<\/>`, text)
for i := range match { for i := range match {

View file

@ -12,10 +12,10 @@ const (
) )
func TestWriteAndRemoveText(t *testing.T) { func TestWriteAndRemoveText(t *testing.T) {
formats := &ansiFormats{} ansi := &ansiUtils{}
formats.init("pwsh") ansi.init("pwsh")
renderer := &AnsiColor{ renderer := &AnsiColor{
formats: formats, ansi: ansi,
} }
text := renderer.writeAndRemoveText("#193549", "#fff", "This is white, ", "This is white, ", inputText) text := renderer.writeAndRemoveText("#193549", "#fff", "This is white, ", "This is white, ", inputText)
assert.Equal(t, "<#ff5733>this is orange</>, white again", text) assert.Equal(t, "<#ff5733>this is orange</>, white again", text)
@ -23,10 +23,10 @@ func TestWriteAndRemoveText(t *testing.T) {
} }
func TestWriteAndRemoveTextColored(t *testing.T) { func TestWriteAndRemoveTextColored(t *testing.T) {
formats := &ansiFormats{} ansi := &ansiUtils{}
formats.init("pwsh") ansi.init("pwsh")
renderer := &AnsiColor{ renderer := &AnsiColor{
formats: formats, ansi: ansi,
} }
text := renderer.writeAndRemoveText("#193549", "#ff5733", "this is orange", "<#ff5733>this is orange</>", inputText) text := renderer.writeAndRemoveText("#193549", "#ff5733", "this is orange", "<#ff5733>this is orange</>", inputText)
assert.Equal(t, "This is white, , white again", text) assert.Equal(t, "This is white, , white again", text)
@ -34,20 +34,20 @@ func TestWriteAndRemoveTextColored(t *testing.T) {
} }
func TestWriteColorOverride(t *testing.T) { func TestWriteColorOverride(t *testing.T) {
formats := &ansiFormats{} ansi := &ansiUtils{}
formats.init("pwsh") ansi.init("pwsh")
renderer := &AnsiColor{ renderer := &AnsiColor{
formats: formats, ansi: ansi,
} }
renderer.write("#193549", "#ff5733", inputText) renderer.write("#193549", "#ff5733", inputText)
assert.NotContains(t, renderer.string(), "<#ff5733>") assert.NotContains(t, renderer.string(), "<#ff5733>")
} }
func TestWriteColorOverrideBackground(t *testing.T) { func TestWriteColorOverrideBackground(t *testing.T) {
formats := &ansiFormats{} ansi := &ansiUtils{}
formats.init("pwsh") ansi.init("pwsh")
renderer := &AnsiColor{ renderer := &AnsiColor{
formats: formats, ansi: ansi,
} }
text := "This is white, <,#000000>this is black</>, white again" text := "This is white, <,#000000>this is black</>, white again"
renderer.write("#193549", "#ff5733", text) renderer.write("#193549", "#ff5733", text)
@ -55,10 +55,10 @@ func TestWriteColorOverrideBackground(t *testing.T) {
} }
func TestWriteColorOverrideBackground16(t *testing.T) { func TestWriteColorOverrideBackground16(t *testing.T) {
formats := &ansiFormats{} ansi := &ansiUtils{}
formats.init("pwsh") ansi.init("pwsh")
renderer := &AnsiColor{ renderer := &AnsiColor{
formats: formats, ansi: ansi,
} }
text := "This is default <,white> this background is changed</> default again" text := "This is default <,white> this background is changed</> default again"
renderer.write("#193549", "#ff5733", text) renderer.write("#193549", "#ff5733", text)
@ -68,10 +68,10 @@ func TestWriteColorOverrideBackground16(t *testing.T) {
} }
func TestWriteColorOverrideBoth(t *testing.T) { func TestWriteColorOverrideBoth(t *testing.T) {
formats := &ansiFormats{} ansi := &ansiUtils{}
formats.init("pwsh") ansi.init("pwsh")
renderer := &AnsiColor{ renderer := &AnsiColor{
formats: formats, ansi: ansi,
} }
text := "This is white, <#000000,#ffffff>this is black</>, white again" text := "This is white, <#000000,#ffffff>this is black</>, white again"
renderer.write("#193549", "#ff5733", text) renderer.write("#193549", "#ff5733", text)
@ -80,10 +80,10 @@ func TestWriteColorOverrideBoth(t *testing.T) {
} }
func TestWriteColorOverrideBoth16(t *testing.T) { func TestWriteColorOverrideBoth16(t *testing.T) {
formats := &ansiFormats{} ansi := &ansiUtils{}
formats.init("pwsh") ansi.init("pwsh")
renderer := &AnsiColor{ renderer := &AnsiColor{
formats: formats, ansi: ansi,
} }
text := "This is white, <black,white>this is black</>, white again" text := "This is white, <black,white>this is black</>, white again"
renderer.write("#193549", "#ff5733", text) renderer.write("#193549", "#ff5733", text)
@ -92,10 +92,10 @@ func TestWriteColorOverrideBoth16(t *testing.T) {
} }
func TestWriteColorOverrideDouble(t *testing.T) { func TestWriteColorOverrideDouble(t *testing.T) {
formats := &ansiFormats{} ansi := &ansiUtils{}
formats.init("pwsh") ansi.init("pwsh")
renderer := &AnsiColor{ renderer := &AnsiColor{
formats: formats, ansi: ansi,
} }
text := "<#ffffff>jan</>@<#ffffff>Jans-MBP</>" text := "<#ffffff>jan</>@<#ffffff>Jans-MBP</>"
renderer.write("#193549", "#ff5733", text) renderer.write("#193549", "#ff5733", text)
@ -104,10 +104,10 @@ func TestWriteColorOverrideDouble(t *testing.T) {
} }
func TestWriteColorTransparent(t *testing.T) { func TestWriteColorTransparent(t *testing.T) {
formats := &ansiFormats{} ansi := &ansiUtils{}
formats.init("pwsh") ansi.init("pwsh")
renderer := &AnsiColor{ renderer := &AnsiColor{
formats: formats, ansi: ansi,
} }
text := "This is white" text := "This is white"
renderer.writeColoredText("#193549", Transparent, text) renderer.writeColoredText("#193549", Transparent, text)
@ -115,10 +115,10 @@ func TestWriteColorTransparent(t *testing.T) {
} }
func TestWriteColorName(t *testing.T) { func TestWriteColorName(t *testing.T) {
formats := &ansiFormats{} ansi := &ansiUtils{}
formats.init("pwsh") ansi.init("pwsh")
renderer := &AnsiColor{ renderer := &AnsiColor{
formats: formats, ansi: ansi,
} }
text := "This is white, <red>this is red</>, white again" text := "This is white, <red>this is red</>, white again"
renderer.write("#193549", "red", text) renderer.write("#193549", "red", text)
@ -126,10 +126,10 @@ func TestWriteColorName(t *testing.T) {
} }
func TestWriteColorInvalid(t *testing.T) { func TestWriteColorInvalid(t *testing.T) {
formats := &ansiFormats{} ansi := &ansiUtils{}
formats.init("pwsh") ansi.init("pwsh")
renderer := &AnsiColor{ renderer := &AnsiColor{
formats: formats, ansi: ansi,
} }
text := "This is white, <invalid>this is orange</>, white again" text := "This is white, <invalid>this is orange</>, white again"
renderer.write("#193549", "invalid", text) renderer.write("#193549", "invalid", text)

View file

@ -1,61 +0,0 @@
package main
import (
"fmt"
"strings"
)
// AnsiRenderer exposes functionality using ANSI
type AnsiRenderer struct {
builder strings.Builder
formats *ansiFormats
}
func (r *AnsiRenderer) carriageForward() {
r.builder.WriteString(fmt.Sprintf(r.formats.left, 1000))
}
func (r *AnsiRenderer) setCursorForRightWrite(text string, offset int) {
strippedLen := r.formats.lenWithoutANSI(text) + -offset
r.builder.WriteString(fmt.Sprintf(r.formats.right, strippedLen))
}
func (r *AnsiRenderer) changeLine(numberOfLines int) {
position := "B"
if numberOfLines < 0 {
position = "F"
numberOfLines = -numberOfLines
}
r.builder.WriteString(fmt.Sprintf(r.formats.linechange, numberOfLines, position))
}
func (r *AnsiRenderer) creset() {
r.builder.WriteString(r.formats.creset)
}
func (r *AnsiRenderer) write(text string) {
r.builder.WriteString(text)
// Due to a bug in Powershell, the end of the line needs to be cleared.
// If this doesn't happen, the portion after the prompt gets colored in the background
// 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.
if r.formats.shell == pwsh || r.formats.shell == powershell5 {
r.builder.WriteString(r.formats.clearEOL)
}
}
func (r *AnsiRenderer) string() string {
return r.builder.String()
}
func (r *AnsiRenderer) saveCursorPosition() {
r.builder.WriteString(r.formats.saveCursorPosition)
}
func (r *AnsiRenderer) restoreCursorPosition() {
r.builder.WriteString(r.formats.restoreCursorPosition)
}
func (r *AnsiRenderer) osc99(pwd string) {
r.builder.WriteString(fmt.Sprintf(r.formats.osc99, pwd))
}

View file

@ -17,7 +17,7 @@ func TestLenWithoutAnsi(t *testing.T) {
{Text: "\\[\x1b[44m\\]hello\\[\x1b[0m\\]", ShellName: bash, Expected: 5}, {Text: "\\[\x1b[44m\\]hello\\[\x1b[0m\\]", ShellName: bash, Expected: 5},
} }
for _, tc := range cases { for _, tc := range cases {
a := ansiFormats{} a := ansiUtils{}
a.init(tc.ShellName) a.init(tc.ShellName)
strippedLength := a.lenWithoutANSI(tc.Text) strippedLength := a.lenWithoutANSI(tc.Text)
assert.Equal(t, 5, strippedLength) assert.Equal(t, 5, strippedLength)
@ -35,7 +35,7 @@ func TestGenerateHyperlinkNoUrl(t *testing.T) {
{Text: "sample text with no url", ShellName: bash, Expected: "sample text with no url"}, {Text: "sample text with no url", ShellName: bash, Expected: "sample text with no url"},
} }
for _, tc := range cases { for _, tc := range cases {
a := ansiFormats{} a := ansiUtils{}
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)
@ -53,7 +53,7 @@ func TestGenerateHyperlinkWithUrl(t *testing.T) {
{Text: "[google](http://www.google.be)", ShellName: bash, Expected: "\\[\x1b]8;;http://www.google.be\x1b\\\\\\]google\\[\x1b]8;;\x1b\\\\\\]"}, {Text: "[google](http://www.google.be)", ShellName: bash, Expected: "\\[\x1b]8;;http://www.google.be\x1b\\\\\\]google\\[\x1b]8;;\x1b\\\\\\]"},
} }
for _, tc := range cases { for _, tc := range cases {
a := ansiFormats{} a := ansiUtils{}
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)
@ -71,7 +71,7 @@ func TestGenerateHyperlinkWithUrlNoName(t *testing.T) {
{Text: "[](http://www.google.be)", ShellName: bash, Expected: "[](http://www.google.be)"}, {Text: "[](http://www.google.be)", ShellName: bash, Expected: "[](http://www.google.be)"},
} }
for _, tc := range cases { for _, tc := range cases {
a := ansiFormats{} a := ansiUtils{}
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)
@ -91,7 +91,7 @@ func TestFormatText(t *testing.T) {
{Case: "strikethrough", Text: "This <s>is</s> white", Expected: "This \x1b[9mis\x1b[29m white"}, {Case: "strikethrough", Text: "This <s>is</s> white", Expected: "This \x1b[9mis\x1b[29m white"},
} }
for _, tc := range cases { for _, tc := range cases {
a := ansiFormats{} a := ansiUtils{}
a.init("") a.init("")
formattedText := a.formatText(tc.Text) formattedText := a.formatText(tc.Text)
assert.Equal(t, tc.Expected, formattedText, tc.Case) assert.Equal(t, tc.Expected, formattedText, tc.Case)

View file

@ -35,14 +35,16 @@ type Block struct {
Newline bool `config:"newline"` Newline bool `config:"newline"`
env environmentInfo env environmentInfo
color *AnsiColor writer colorWriter
ansi *ansiUtils
activeSegment *Segment activeSegment *Segment
previousActiveSegment *Segment previousActiveSegment *Segment
} }
func (b *Block) init(env environmentInfo, color *AnsiColor) { func (b *Block) init(env environmentInfo, writer colorWriter, ansi *ansiUtils) {
b.env = env b.env = env
b.color = color b.writer = writer
b.ansi = ansi
} }
func (b *Block) enabled() bool { func (b *Block) enabled() bool {
@ -71,6 +73,7 @@ func (b *Block) setStringValues() {
} }
func (b *Block) renderSegments() string { func (b *Block) renderSegments() string {
defer b.writer.reset()
for _, segment := range b.Segments { for _, segment := range b.Segments {
if !segment.active { if !segment.active {
continue continue
@ -82,7 +85,7 @@ func (b *Block) renderSegments() string {
if b.previousActiveSegment != nil && b.previousActiveSegment.Style == Powerline { if b.previousActiveSegment != nil && b.previousActiveSegment.Style == Powerline {
b.writePowerLineSeparator(Transparent, b.previousActiveSegment.background(), true) b.writePowerLineSeparator(Transparent, b.previousActiveSegment.background(), true)
} }
return b.color.string() return b.writer.string()
} }
func (b *Block) endPowerline() { func (b *Block) endPowerline() {
@ -100,10 +103,10 @@ func (b *Block) writePowerLineSeparator(background, foreground string, end bool)
symbol = b.previousActiveSegment.PowerlineSymbol symbol = b.previousActiveSegment.PowerlineSymbol
} }
if b.activeSegment.InvertPowerline { if b.activeSegment.InvertPowerline {
b.color.write(foreground, background, symbol) b.writer.write(foreground, background, symbol)
return return
} }
b.color.write(background, foreground, symbol) b.writer.write(background, foreground, symbol)
} }
func (b *Block) getPowerlineColor(foreground bool) string { func (b *Block) getPowerlineColor(foreground bool) string {
@ -141,17 +144,17 @@ func (b *Block) renderPlainSegment(text string) {
} }
func (b *Block) renderDiamondSegment(text string) { func (b *Block) renderDiamondSegment(text string) {
b.color.write(Transparent, b.activeSegment.background(), b.activeSegment.LeadingDiamond) b.writer.write(Transparent, b.activeSegment.background(), b.activeSegment.LeadingDiamond)
b.renderText(text) b.renderText(text)
b.color.write(Transparent, b.activeSegment.background(), b.activeSegment.TrailingDiamond) b.writer.write(Transparent, b.activeSegment.background(), b.activeSegment.TrailingDiamond)
} }
func (b *Block) renderText(text string) { func (b *Block) renderText(text string) {
text = b.color.formats.generateHyperlink(text) text = b.ansi.generateHyperlink(text)
defaultValue := " " defaultValue := " "
prefix := b.activeSegment.getValue(Prefix, defaultValue) prefix := b.activeSegment.getValue(Prefix, defaultValue)
postfix := b.activeSegment.getValue(Postfix, defaultValue) postfix := b.activeSegment.getValue(Postfix, defaultValue)
b.color.write(b.activeSegment.background(), b.activeSegment.foreground(), fmt.Sprintf("%s%s%s", prefix, text, postfix)) b.writer.write(b.activeSegment.background(), b.activeSegment.foreground(), fmt.Sprintf("%s%s%s", prefix, text, postfix))
} }
func (b *Block) debug() (int, []*SegmentTiming) { func (b *Block) debug() (int, []*SegmentTiming) {
@ -183,8 +186,8 @@ func (b *Block) debug() (int, []*SegmentTiming) {
if b.activeSegment.Style == Powerline { if b.activeSegment.Style == Powerline {
b.writePowerLineSeparator(Transparent, b.activeSegment.background(), true) b.writePowerLineSeparator(Transparent, b.activeSegment.background(), true)
} }
segmentTiming.stringValue = b.color.string() segmentTiming.stringValue = b.writer.string()
b.color.builder.Reset() b.writer.reset()
} }
segmentTimings = append(segmentTimings, &segmentTiming) segmentTimings = append(segmentTimings, &segmentTiming)
} }

View file

@ -6,9 +6,9 @@ import (
) )
type consoleTitle struct { type consoleTitle struct {
env environmentInfo env environmentInfo
config *Config config *Config
formats *ansiFormats ansi *ansiUtils
} }
// ConsoleTitleStyle defines how to show the title in the console window // ConsoleTitleStyle defines how to show the title in the console window
@ -37,7 +37,7 @@ func (t *consoleTitle) getConsoleTitle() string {
default: default:
title = base(t.getPwd(), t.env) title = base(t.getPwd(), t.env)
} }
return fmt.Sprintf(t.formats.title, title) return fmt.Sprintf(t.ansi.title, title)
} }
func (t *consoleTitle) getTemplateText() string { func (t *consoleTitle) getTemplateText() string {

View file

@ -62,12 +62,12 @@ func TestGetConsoleTitle(t *testing.T) {
env.On("getenv", "USERDOMAIN").Return("MyCompany") env.On("getenv", "USERDOMAIN").Return("MyCompany")
env.On("getCurrentUser", nil).Return("MyUser") env.On("getCurrentUser", nil).Return("MyUser")
env.On("getHostName", nil).Return("MyHost", nil) env.On("getHostName", nil).Return("MyHost", nil)
formats := &ansiFormats{} ansi := &ansiUtils{}
formats.init(tc.ShellName) ansi.init(tc.ShellName)
ct := &consoleTitle{ ct := &consoleTitle{
env: env, env: env,
config: config, config: config,
formats: formats, ansi: ansi,
} }
got := ct.getConsoleTitle() got := ct.getConsoleTitle()
assert.Equal(t, tc.Expected, got) assert.Equal(t, tc.Expected, got)
@ -117,12 +117,12 @@ func TestGetConsoleTitleIfGethostnameReturnsError(t *testing.T) {
env.On("getenv", "USERDOMAIN").Return("MyCompany") env.On("getenv", "USERDOMAIN").Return("MyCompany")
env.On("getCurrentUser", nil).Return("MyUser") env.On("getCurrentUser", nil).Return("MyUser")
env.On("getHostName", nil).Return("", fmt.Errorf("I have a bad feeling about this")) env.On("getHostName", nil).Return("", fmt.Errorf("I have a bad feeling about this"))
formats := &ansiFormats{} ansi := &ansiUtils{}
formats.init(tc.ShellName) ansi.init(tc.ShellName)
ct := &consoleTitle{ ct := &consoleTitle{
env: env, env: env,
config: config, config: config,
formats: formats, ansi: ansi,
} }
got := ct.getConsoleTitle() got := ct.getConsoleTitle()
assert.Equal(t, tc.Expected, got) assert.Equal(t, tc.Expected, got)

View file

@ -9,25 +9,39 @@ import (
type engine struct { type engine struct {
config *Config config *Config
env environmentInfo env environmentInfo
color *AnsiColor colorWriter colorWriter
renderer *AnsiRenderer ansi *ansiUtils
consoleTitle *consoleTitle consoleTitle *consoleTitle
// activeBlock *Block
// activeSegment *Segment console strings.Builder
// previousActiveSegment *Segment
rprompt string rprompt string
} }
func (e *engine) write(text string) {
e.console.WriteString(text)
// Due to a bug in Powershell, the end of the line needs to be cleared.
// If this doesn't happen, the portion after the prompt gets colored in the background
// 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.
if e.ansi.shell == pwsh || e.ansi.shell == powershell5 {
e.console.WriteString(e.ansi.clearEOL)
}
}
func (e *engine) string() string {
return e.console.String()
}
func (e *engine) render() string { func (e *engine) render() string {
for _, block := range e.config.Blocks { for _, block := range e.config.Blocks {
e.renderBlock(block) e.renderBlock(block)
} }
if e.config.ConsoleTitle { if e.config.ConsoleTitle {
e.renderer.write(e.consoleTitle.getConsoleTitle()) e.write(e.consoleTitle.getConsoleTitle())
} }
e.renderer.creset() e.write(e.ansi.creset)
if e.config.FinalSpace { if e.config.FinalSpace {
e.renderer.write(" ") e.write(" ")
} }
if !e.config.OSC99 { if !e.config.OSC99 {
@ -37,38 +51,37 @@ func (e *engine) render() string {
if e.env.isWsl() { if e.env.isWsl() {
cwd, _ = e.env.runCommand("wslpath", "-m", cwd) cwd, _ = e.env.runCommand("wslpath", "-m", cwd)
} }
e.renderer.osc99(cwd) e.write(e.ansi.consolePwd(cwd))
return e.print() return e.print()
} }
func (e *engine) renderBlock(block *Block) { func (e *engine) renderBlock(block *Block) {
block.init(e.env, e.color) block.init(e.env, e.colorWriter, e.ansi)
block.setStringValues() block.setStringValues()
defer e.color.reset()
if !block.enabled() { if !block.enabled() {
return return
} }
if block.Newline { if block.Newline {
e.renderer.write("\n") e.write("\n")
} }
switch block.Type { switch block.Type {
// This is deprecated but leave if to not break current configs // This is deprecated but leave if to not break current configs
// It is encouraged to used "newline": true on block level // It is encouraged to used "newline": true on block level
// rather than the standalone the linebreak block // rather than the standalone the linebreak block
case LineBreak: case LineBreak:
e.renderer.write("\n") e.write("\n")
case Prompt: case Prompt:
if block.VerticalOffset != 0 { if block.VerticalOffset != 0 {
e.renderer.changeLine(block.VerticalOffset) e.write(e.ansi.changeLine(block.VerticalOffset))
} }
switch block.Alignment { switch block.Alignment {
case Right: case Right:
e.renderer.carriageForward() e.write(e.ansi.carriageForward())
blockText := block.renderSegments() blockText := block.renderSegments()
e.renderer.setCursorForRightWrite(blockText, block.HorizontalOffset) e.write(e.ansi.getCursorForRightWrite(blockText, block.HorizontalOffset))
e.renderer.write(blockText) e.write(blockText)
case Left: case Left:
e.renderer.write(block.renderSegments()) e.write(block.renderSegments())
} }
case RPrompt: case RPrompt:
e.rprompt = block.renderSegments() e.rprompt = block.renderSegments()
@ -79,7 +92,7 @@ func (e *engine) renderBlock(block *Block) {
func (e *engine) debug() string { func (e *engine) debug() string {
var segmentTimings []*SegmentTiming var segmentTimings []*SegmentTiming
largestSegmentNameLength := 0 largestSegmentNameLength := 0
e.renderer.write("\n\x1b[1mHere are the timings of segments in your prompt:\x1b[0m\n\n") e.write("\n\x1b[1mHere are the timings of segments in your prompt:\x1b[0m\n\n")
// console title timing // console title timing
start := time.Now() start := time.Now()
@ -96,7 +109,7 @@ func (e *engine) debug() 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.color) block.init(e.env, e.colorWriter, e.ansi)
longestSegmentName, timings := block.debug() longestSegmentName, timings := block.debug()
segmentTimings = append(segmentTimings, timings...) segmentTimings = append(segmentTimings, timings...)
if longestSegmentName > largestSegmentNameLength { if longestSegmentName > largestSegmentNameLength {
@ -112,9 +125,9 @@ func (e *engine) debug() string {
duration += segment.stringDuration.Milliseconds() duration += segment.stringDuration.Milliseconds()
} }
segmentName := fmt.Sprintf("%s(%t)", segment.name, segment.enabled) segmentName := fmt.Sprintf("%s(%t)", segment.name, segment.enabled)
e.renderer.write(fmt.Sprintf("%-*s - %3d ms - %s\n", largestSegmentNameLength, segmentName, duration, segment.stringValue)) e.write(fmt.Sprintf("%-*s - %3d ms - %s\n", largestSegmentNameLength, segmentName, duration, segment.stringValue))
} }
return e.renderer.string() return e.string()
} }
func (e *engine) print() string { func (e *engine) print() string {
@ -122,18 +135,18 @@ func (e *engine) print() string {
case zsh: case zsh:
if *e.env.getArgs().Eval { if *e.env.getArgs().Eval {
// escape double quotes contained in the prompt // escape double quotes contained in the prompt
prompt := fmt.Sprintf("PS1=\"%s\"", strings.ReplaceAll(e.renderer.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 pwsh, powershell5, bash, shelly: case pwsh, powershell5, bash, shelly:
if e.rprompt != "" { if e.rprompt != "" {
e.renderer.saveCursorPosition() e.write(e.ansi.saveCursorPosition)
e.renderer.carriageForward() e.write(e.ansi.carriageForward())
e.renderer.setCursorForRightWrite(e.rprompt, 0) e.write(e.ansi.getCursorForRightWrite(e.rprompt, 0))
e.renderer.write(e.rprompt) e.write(e.rprompt)
e.renderer.restoreCursorPosition() e.write(e.ansi.restoreCursorPosition)
} }
} }
return e.renderer.string() return e.string()
} }

View file

@ -96,7 +96,7 @@ func NewRGBColor(ansiColor string) *RGB {
type ImageRenderer struct { type ImageRenderer struct {
ansiString string ansiString string
author string author string
formats *ansiFormats ansi *ansiUtils
factor float64 factor float64
@ -189,7 +189,7 @@ func (ir *ImageRenderer) fontHeight() float64 {
func (ir *ImageRenderer) calculateWidth() int { func (ir *ImageRenderer) calculateWidth() int {
longest := 0 longest := 0
for _, line := range strings.Split(ir.ansiString, "\n") { for _, line := range strings.Split(ir.ansiString, "\n") {
length := ir.formats.lenWithoutANSI(line) length := ir.ansi.lenWithoutANSI(line)
if length > longest { if length > longest {
longest = length longest = length
} }

View file

@ -15,11 +15,11 @@ func runImageTest(content string) error {
return err return err
} }
defer os.Remove(file.Name()) defer os.Remove(file.Name())
formats := &ansiFormats{} ansi := &ansiUtils{}
formats.init(shelly) ansi.init(shelly)
image := &ImageRenderer{ image := &ImageRenderer{
ansiString: content, ansiString: content,
formats: formats, ansi: ansi,
} }
image.init() image.init()
err = image.SavePNG(poshImagePath) err = image.SavePNG(poshImagePath)

View file

@ -173,26 +173,23 @@ func main() {
return return
} }
formats := &ansiFormats{} ansi := &ansiUtils{}
formats.init(env.getShellName()) ansi.init(env.getShellName())
renderer := &AnsiRenderer{
formats: formats,
}
colorer := &AnsiColor{ colorer := &AnsiColor{
formats: formats, ansi: ansi,
terminalBackground: getConsoleBackgroundColor(env, cfg.TerminalBackground), terminalBackground: getConsoleBackgroundColor(env, cfg.TerminalBackground),
} }
title := &consoleTitle{ title := &consoleTitle{
env: env, env: env,
config: cfg, config: cfg,
formats: formats, ansi: ansi,
} }
engine := &engine{ engine := &engine{
config: cfg, config: cfg,
env: env, env: env,
color: colorer, colorWriter: colorer,
renderer: renderer,
consoleTitle: title, consoleTitle: title,
ansi: ansi,
} }
if *args.Debug { if *args.Debug {
@ -209,7 +206,7 @@ func main() {
author: *args.Author, author: *args.Author,
cursorPadding: *args.CursorPadding, cursorPadding: *args.CursorPadding,
rPromptOffset: *args.RPromptOffset, rPromptOffset: *args.RPromptOffset,
formats: formats, ansi: ansi,
} }
imageCreator.init() imageCreator.init()
match := findNamedRegexMatch(`.*(\/|\\)(?P<STR>.+).omp.(json|yaml|toml)`, *args.Config) match := findNamedRegexMatch(`.*(\/|\\)(?P<STR>.+).omp.(json|yaml|toml)`, *args.Config)