refactor: escape color sequences on zsh

This commit is contained in:
Jan De Dobbeleer 2020-09-23 09:33:54 +02:00 committed by Jan De Dobbeleer
parent 7d8892020e
commit f478255bbf
8 changed files with 240 additions and 171 deletions

View file

@ -1,63 +0,0 @@
package main
import (
"bytes"
"fmt"
"regexp"
"strings"
"github.com/gookit/color"
)
//ColorWriter writes colorized strings
type ColorWriter struct {
Buffer *bytes.Buffer
}
const (
//Transparent implies a transparent color
Transparent string = "transparent"
)
func (w *ColorWriter) writeColoredText(background string, foreground string, text string) {
var coloredText string
if foreground == Transparent {
style := color.HEX(background, false)
colorCodes := style.Code()
// this takes the colors and inverts them so the foreground becomes transparent
coloredText = fmt.Sprintf("\x1b[%s;49m\x1b[7m%s\x1b[m\x1b[0m", colorCodes, text)
} else if background == "" || background == Transparent {
style := color.HEX(foreground)
coloredText = style.Sprint(text)
} else {
style := color.HEXStyle(foreground, background)
coloredText = style.Sprint(text)
}
w.Buffer.WriteString(coloredText)
}
func (w *ColorWriter) writeAndRemoveText(background string, foreground string, text string, textToRemove string, parentText string) string {
w.writeColoredText(background, foreground, text)
return strings.Replace(parentText, textToRemove, "", 1)
}
func (w *ColorWriter) write(background string, foreground string, text string) {
r := regexp.MustCompile(`<\s*(?P<color>#[A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})>(?P<text>.*?)<\s*/\s*>`)
match := r.FindAllStringSubmatch(text, -1)
for i := range match {
// get the text before the color override and write that first
textBeforeColorOverride := strings.Split(text, match[i][0])[0]
text = w.writeAndRemoveText(background, foreground, textBeforeColorOverride, textBeforeColorOverride, text)
text = w.writeAndRemoveText(background, match[i][1], match[i][2], match[i][0], text)
}
// color the remaining part of text with background and foreground
w.writeColoredText(background, foreground, text)
}
func (w *ColorWriter) string() string {
return w.Buffer.String()
}
func (w *ColorWriter) reset() {
w.Buffer.Reset()
}

View file

@ -1,46 +0,0 @@
package main
import (
"bytes"
"testing"
"github.com/stretchr/testify/assert"
)
func TestWriteAndRemoveText(t *testing.T) {
writer := &ColorWriter{
Buffer: new(bytes.Buffer),
}
inputText := "This is white, <#ff5733>this is orange</>, white again"
text := writer.writeAndRemoveText("#193549", "#fff", "This is white, ", "This is white, ", inputText)
assert.Equal(t, "<#ff5733>this is orange</>, white again", text)
assert.NotContains(t, writer.string(), "<#ff5733>")
}
func TestWriteAndRemoveTextColored(t *testing.T) {
writer := &ColorWriter{
Buffer: new(bytes.Buffer),
}
inputText := "This is white, <#ff5733>this is orange</>, white again"
text := writer.writeAndRemoveText("#193549", "#ff5733", "this is orange", "<#ff5733>this is orange</>", inputText)
assert.Equal(t, "This is white, , white again", text)
assert.NotContains(t, writer.string(), "<#ff5733>")
}
func TestWriteColorOverride(t *testing.T) {
writer := &ColorWriter{
Buffer: new(bytes.Buffer),
}
text := "This is white, <#ff5733>this is orange</>, white again"
writer.write("#193549", "#ff5733", text)
assert.NotContains(t, writer.string(), "<#ff5733>")
}
func TestWriteColorTransparent(t *testing.T) {
writer := &ColorWriter{
Buffer: new(bytes.Buffer),
}
text := "This is white"
writer.writeColoredText("#193549", Transparent, text)
t.Log(writer.string())
}

View file

@ -2,15 +2,12 @@ package main
import (
"fmt"
"regexp"
"golang.org/x/text/unicode/norm"
)
type engine struct {
settings *Settings
env environmentInfo
renderer *ColorWriter
renderer *Renderer
activeBlock *Block
activeSegment *Segment
previousActiveSegment *Segment
@ -107,37 +104,22 @@ func (e *engine) renderBlockSegments(block *Block) string {
return e.renderer.string()
}
func (e *engine) lenWithoutANSI(str string) int {
ansi := "[\u001B\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[a-zA-Z\\d]*)*)?\u0007)|(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PRZcf-ntqry=><~]))"
re := regexp.MustCompile(ansi)
stripped := re.ReplaceAllString(str, "")
var i norm.Iter
i.InitString(norm.NFD, stripped)
var count int
for !i.Done() {
i.Next()
count++
}
return count
}
func (e *engine) render() {
for _, block := range e.settings.Blocks {
// if line break, append a line break
if block.Type == LineBreak {
fmt.Printf("\x1b[%dC ", 1000)
fmt.Print(e.renderer.lineBreak())
continue
}
if block.LineOffset < 0 {
fmt.Printf("\x1b[%dF", -block.LineOffset)
} else if block.LineOffset > 0 {
fmt.Printf("\x1b[%dB", block.LineOffset)
if block.LineOffset != 0 {
fmt.Print(e.renderer.changeLine(block.LineOffset))
}
switch block.Alignment {
case Right:
fmt.Printf("\x1b[%dC", 1000)
fmt.Print(e.renderer.carriageReturn())
blockText := e.renderBlockSegments(block)
fmt.Printf("\x1b[%dD", e.lenWithoutANSI(blockText)+e.settings.RightSegmentOffset)
cursorMove := e.renderer.setCursorForRightWrite(blockText, e.settings.RightSegmentOffset)
fmt.Print(cursorMove)
fmt.Print(blockText)
default:
fmt.Print(e.renderBlockSegments(block))

View file

@ -1,31 +0,0 @@
package main
import (
"bytes"
"testing"
"github.com/stretchr/testify/assert"
)
func TestLenWithoutANSI(t *testing.T) {
block := &Block{
Type: Prompt,
Alignment: Right,
Segments: []*Segment{
{
Type: Time,
Style: Plain,
Background: "#B8B80A",
Foreground: "#ffffff",
},
},
}
engine := &engine{
renderer: &ColorWriter{
Buffer: new(bytes.Buffer),
},
}
blockText := engine.renderBlockSegments(block)
strippedLength := engine.lenWithoutANSI(blockText)
assert.Equal(t, 10, strippedLength)
}

12
main.go
View file

@ -38,12 +38,18 @@ func main() {
fmt.Println(string(theme))
return
}
colorWriter := &Renderer{
Buffer: new(bytes.Buffer),
}
var shell string
if parentProcess, err := env.getParentProcess(); err != nil {
shell = parentProcess.Executable()
}
colorWriter.init(shell)
engine := &engine{
settings: settings,
env: env,
renderer: &ColorWriter{
Buffer: new(bytes.Buffer),
},
renderer: colorWriter,
}
engine.render()
}

151
renderer.go Executable file
View file

@ -0,0 +1,151 @@
package main
import (
"bytes"
"fmt"
"regexp"
"strings"
"github.com/gookit/color"
"golang.org/x/text/unicode/norm"
)
type formats struct {
single string
full string
transparent string
linebreak string
linechange string
left string
right string
rANSI string
}
//Shell indicates the shell we're currently in
type Shell string
//Renderer writes colorized strings
type Renderer struct {
Buffer *bytes.Buffer
formats *formats
shell Shell
}
const (
//Transparent implies a transparent color
Transparent string = "transparent"
zsh Shell = "zsh"
universal Shell = "any"
)
func (r *Renderer) init(shell string) {
r.formats = &formats{
rANSI: "[\u001B\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[a-zA-Z\\d]*)*)?\u0007)|(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PRZcf-ntqry=><~]))",
}
switch shell {
case "zsh":
r.formats.single = "%%{\x1b[%sm%%}%s%%{\x1b[0m%%}"
r.formats.full = "%%{\x1b[%sm\x1b[%sm%%}%s%%{\x1b[0m%%}"
r.formats.transparent = "%%{\x1b[%s;49m\x1b[7m%%}%s%%{\x1b[m\x1b[0m%%}"
r.formats.linebreak = "\n"
r.formats.linechange = "%%{\x1b[%d%s%%}"
r.formats.left = "%%{\x1b[%d%%}"
r.formats.right = "%%{\x1b[%dD%%}"
r.shell = zsh
default:
r.formats.single = "\x1b[%sm%s\x1b[0m"
r.formats.full = "\x1b[%sm\x1b[%sm%s\x1b[0m"
r.formats.transparent = "\x1b[%s;49m\x1b[7m%s\x1b[m\x1b[0m"
r.formats.linebreak = "\x1b[1000C "
r.formats.linechange = "\x1b[%d%s"
r.formats.left = "\x1b[%dC"
r.formats.right = "\x1b[%dD"
r.shell = universal
}
}
func (r *Renderer) getAnsiFromHex(hexColor string, isBackground bool) string {
style := color.HEX(hexColor, isBackground)
return style.Code()
}
func (r *Renderer) writeColoredText(background string, foreground string, text string) {
var coloredText string
if foreground == Transparent && background != "" {
ansiColor := r.getAnsiFromHex(background, false)
coloredText = fmt.Sprintf(r.formats.transparent, ansiColor, text)
} else if background == "" || background == Transparent {
ansiColor := r.getAnsiFromHex(foreground, false)
coloredText = fmt.Sprintf(r.formats.single, ansiColor, text)
} else if foreground != "" && background != "" {
bgAnsiColor := r.getAnsiFromHex(background, true)
fgAnsiColor := r.getAnsiFromHex(foreground, false)
coloredText = fmt.Sprintf(r.formats.full, bgAnsiColor, fgAnsiColor, text)
}
r.Buffer.WriteString(coloredText)
}
func (r *Renderer) writeAndRemoveText(background string, foreground string, text string, textToRemove string, parentText string) string {
r.writeColoredText(background, foreground, text)
return strings.Replace(parentText, textToRemove, "", 1)
}
func (r *Renderer) write(background string, foreground string, text string) {
rex := regexp.MustCompile(`<\s*(?P<color>#[A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})>(?P<text>.*?)<\s*/\s*>`)
match := rex.FindAllStringSubmatch(text, -1)
for i := range match {
// get the text before the color override and write that first
textBeforeColorOverride := strings.Split(text, match[i][0])[0]
text = r.writeAndRemoveText(background, foreground, textBeforeColorOverride, textBeforeColorOverride, text)
text = r.writeAndRemoveText(background, match[i][1], match[i][2], match[i][0], text)
}
// color the remaining part of text with background and foreground
r.writeColoredText(background, foreground, text)
}
func (r *Renderer) lenWithoutANSI(str string) int {
re := regexp.MustCompile(r.formats.rANSI)
stripped := re.ReplaceAllString(str, "")
if r.shell == zsh {
stripped = strings.Replace(stripped, "%{", "", -1)
stripped = strings.Replace(stripped, "%}", "", -1)
}
var i norm.Iter
i.InitString(norm.NFD, stripped)
var count int
for !i.Done() {
i.Next()
count++
}
return count
}
func (r *Renderer) lineBreak() string {
return r.formats.linebreak
}
func (r *Renderer) carriageReturn() string {
return fmt.Sprintf(r.formats.left, 1000)
}
func (r *Renderer) setCursorForRightWrite(text string, offset int) string {
strippedLen := r.lenWithoutANSI(text) + offset
return fmt.Sprintf(r.formats.right, strippedLen)
}
func (r *Renderer) changeLine(numberOfLines int) string {
position := "B"
if numberOfLines < 0 {
position = "F"
numberOfLines = -numberOfLines
}
return fmt.Sprintf(r.formats.linechange, numberOfLines, position)
}
func (r *Renderer) string() string {
return r.Buffer.String()
}
func (r *Renderer) reset() {
r.Buffer.Reset()
}

70
renderer_test.go Executable file
View file

@ -0,0 +1,70 @@
package main
import (
"bytes"
"testing"
"github.com/stretchr/testify/assert"
)
func TestWriteAndRemoveText(t *testing.T) {
renderer := &Renderer{
Buffer: new(bytes.Buffer),
}
renderer.init("pwsh")
inputText := "This is white, <#ff5733>this is orange</>, white again"
text := renderer.writeAndRemoveText("#193549", "#fff", "This is white, ", "This is white, ", inputText)
assert.Equal(t, "<#ff5733>this is orange</>, white again", text)
assert.NotContains(t, renderer.string(), "<#ff5733>")
}
func TestWriteAndRemoveTextColored(t *testing.T) {
renderer := &Renderer{
Buffer: new(bytes.Buffer),
}
renderer.init("pwsh")
inputText := "This is white, <#ff5733>this is orange</>, white again"
text := renderer.writeAndRemoveText("#193549", "#ff5733", "this is orange", "<#ff5733>this is orange</>", inputText)
assert.Equal(t, "This is white, , white again", text)
assert.NotContains(t, renderer.string(), "<#ff5733>")
}
func TestWriteColorOverride(t *testing.T) {
renderer := &Renderer{
Buffer: new(bytes.Buffer),
}
renderer.init("pwsh")
text := "This is white, <#ff5733>this is orange</>, white again"
renderer.write("#193549", "#ff5733", text)
assert.NotContains(t, renderer.string(), "<#ff5733>")
}
func TestWriteColorTransparent(t *testing.T) {
renderer := &Renderer{
Buffer: new(bytes.Buffer),
}
renderer.init("pwsh")
text := "This is white"
renderer.writeColoredText("#193549", Transparent, text)
t.Log(renderer.string())
}
func TestLenWithoutANSI(t *testing.T) {
text := "\x1b[44mhello\x1b[0m"
renderer := &Renderer{
Buffer: new(bytes.Buffer),
}
renderer.init("pwsh")
strippedLength := renderer.lenWithoutANSI(text)
assert.Equal(t, 5, strippedLength)
}
func TestLenWithoutANSIZsh(t *testing.T) {
text := "%{\x1b[44m%}hello%{\x1b[0m%}"
renderer := &Renderer{
Buffer: new(bytes.Buffer),
}
renderer.init("zsh")
strippedLength := renderer.lenWithoutANSI(text)
assert.Equal(t, 5, strippedLength)
}

View file

@ -9,14 +9,14 @@ type command struct {
}
const (
//Shell to execute command in
Shell Property = "shell"
//ExecutableShell to execute command in
ExecutableShell Property = "shell"
//Command to execute
Command Property = "command"
)
func (c *command) enabled() bool {
shell := c.props.getString(Shell, "bash")
shell := c.props.getString(ExecutableShell, "bash")
command := c.props.getString(Command, "echo no command specified")
if strings.Contains(command, "||") {
commands := strings.Split(command, "||")