feat: zsh rprompt compatibility

This commit is contained in:
Jan De Dobbeleer 2020-12-18 08:59:45 +01:00 committed by Jan De Dobbeleer
parent adb205fe66
commit 3ca2cb5ef3
10 changed files with 395 additions and 340 deletions

150
ansi_color.go Normal file
View file

@ -0,0 +1,150 @@
package main
import (
"bytes"
"errors"
"fmt"
"strings"
"github.com/gookit/color"
)
type colorFormats struct {
single string
full string
transparent string
}
var (
// Map for color names and their respective foreground [0] or background [1] color codes
colorMap map[string][2]string = map[string][2]string{
"black": {"30", "40"},
"red": {"31", "41"},
"green": {"32", "42"},
"yellow": {"33", "43"},
"blue": {"34", "44"},
"magenta": {"35", "45"},
"cyan": {"36", "46"},
"white": {"37", "47"},
"default": {"39", "49"},
"darkGray": {"90", "100"},
"lightRed": {"91", "101"},
"lightGreen": {"92", "102"},
"lightYellow": {"93", "103"},
"lightBlue": {"94", "104"},
"lightMagenta": {"95", "105"},
"lightCyan": {"96", "106"},
"lightWhite": {"97", "107"},
}
)
// Returns the color code for a given color name
func getColorFromName(colorName string, isBackground bool) (string, error) {
colorMapOffset := 0
if isBackground {
colorMapOffset = 1
}
if colorCodes, found := colorMap[colorName]; found {
return colorCodes[colorMapOffset], nil
}
return "", errors.New("color name does not exist")
}
// AnsiColor writes colorized strings
type AnsiColor struct {
buffer *bytes.Buffer
formats *colorFormats
}
const (
// Transparent implies a transparent color
Transparent = "transparent"
)
func (a *AnsiColor) init(shell string) {
a.formats = &colorFormats{}
switch shell {
case zsh:
a.formats.single = "%%{\x1b[%sm%%}%s%%{\x1b[0m%%}"
a.formats.full = "%%{\x1b[%sm\x1b[%sm%%}%s%%{\x1b[0m%%}"
a.formats.transparent = "%%{\x1b[%s;49m\x1b[7m%%}%s%%{\x1b[m\x1b[0m%%}"
case bash:
a.formats.single = "\\[\x1b[%sm\\]%s\\[\x1b[0m\\]"
a.formats.full = "\\[\x1b[%sm\x1b[%sm\\]%s\\[\x1b[0m\\]"
a.formats.transparent = "\\[\x1b[%s;49m\x1b[7m\\]%s\\[\x1b[m\x1b[0m\\]"
default:
a.formats.single = "\x1b[%sm%s\x1b[0m"
a.formats.full = "\x1b[%sm\x1b[%sm%s\x1b[0m"
a.formats.transparent = "\x1b[%s;49m\x1b[7m%s\x1b[m\x1b[0m"
}
}
// Gets the ANSI color code for a given color string.
// This can include a valid hex color in the format `#FFFFFF`,
// but also a name of one of the first 16 ANSI colors like `lightBlue`.
func (a *AnsiColor) getAnsiFromColorString(colorString string, isBackground bool) string {
colorFromName, err := getColorFromName(colorString, isBackground)
if err == nil {
return colorFromName
}
style := color.HEX(colorString, isBackground)
return style.Code()
}
func (a *AnsiColor) writeColoredText(background, foreground, text string) {
var coloredText string
if foreground == Transparent && background != "" {
ansiColor := a.getAnsiFromColorString(background, false)
coloredText = fmt.Sprintf(a.formats.transparent, ansiColor, text)
} else if background == "" || background == Transparent {
ansiColor := a.getAnsiFromColorString(foreground, false)
coloredText = fmt.Sprintf(a.formats.single, ansiColor, text)
} else if foreground != "" && background != "" {
bgAnsiColor := a.getAnsiFromColorString(background, true)
fgAnsiColor := a.getAnsiFromColorString(foreground, false)
coloredText = fmt.Sprintf(a.formats.full, bgAnsiColor, fgAnsiColor, text)
}
a.buffer.WriteString(coloredText)
}
func (a *AnsiColor) writeAndRemoveText(background, foreground, text, textToRemove, parentText string) string {
a.writeColoredText(background, foreground, text)
return strings.Replace(parentText, textToRemove, "", 1)
}
func (a *AnsiColor) write(background, foreground, text string) {
// first we match for any potentially valid colors enclosed in <>
match := findAllNamedRegexMatch(`<(?P<foreground>[^,>]+)?,?(?P<background>[^>]+)?>(?P<content>[^<]*)<\/>`, text)
for i := range match {
extractedForegroundColor := match[i]["foreground"]
extractedBackgroundColor := match[i]["background"]
if col := a.getAnsiFromColorString(extractedForegroundColor, false); col == "" && extractedForegroundColor != Transparent && len(extractedBackgroundColor) == 0 {
continue // we skip invalid colors
}
if col := a.getAnsiFromColorString(extractedBackgroundColor, false); col == "" && extractedBackgroundColor != Transparent && len(extractedForegroundColor) == 0 {
continue // we skip invalid colors
}
// reuse function colors if only one was specified
if len(extractedBackgroundColor) == 0 {
extractedBackgroundColor = background
}
if len(extractedForegroundColor) == 0 {
extractedForegroundColor = foreground
}
escapedTextSegment := match[i]["text"]
innerText := match[i]["content"]
textBeforeColorOverride := strings.Split(text, escapedTextSegment)[0]
text = a.writeAndRemoveText(background, foreground, textBeforeColorOverride, textBeforeColorOverride, text)
text = a.writeAndRemoveText(extractedBackgroundColor, extractedForegroundColor, innerText, escapedTextSegment, text)
}
// color the remaining part of text with background and foreground
a.writeColoredText(background, foreground, text)
}
func (a *AnsiColor) string() string {
return a.buffer.String()
}
func (a *AnsiColor) reset() {
a.buffer.Reset()
}

View file

@ -13,8 +13,8 @@ const (
) )
func TestWriteAndRemoveText(t *testing.T) { func TestWriteAndRemoveText(t *testing.T) {
renderer := &Renderer{ renderer := &AnsiColor{
Buffer: new(bytes.Buffer), buffer: new(bytes.Buffer),
} }
renderer.init("pwsh") renderer.init("pwsh")
text := renderer.writeAndRemoveText("#193549", "#fff", "This is white, ", "This is white, ", inputText) text := renderer.writeAndRemoveText("#193549", "#fff", "This is white, ", "This is white, ", inputText)
@ -23,8 +23,8 @@ func TestWriteAndRemoveText(t *testing.T) {
} }
func TestWriteAndRemoveTextColored(t *testing.T) { func TestWriteAndRemoveTextColored(t *testing.T) {
renderer := &Renderer{ renderer := &AnsiColor{
Buffer: new(bytes.Buffer), buffer: new(bytes.Buffer),
} }
renderer.init("pwsh") renderer.init("pwsh")
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)
@ -33,8 +33,8 @@ func TestWriteAndRemoveTextColored(t *testing.T) {
} }
func TestWriteColorOverride(t *testing.T) { func TestWriteColorOverride(t *testing.T) {
renderer := &Renderer{ renderer := &AnsiColor{
Buffer: new(bytes.Buffer), buffer: new(bytes.Buffer),
} }
renderer.init("pwsh") renderer.init("pwsh")
renderer.write("#193549", "#ff5733", inputText) renderer.write("#193549", "#ff5733", inputText)
@ -42,8 +42,8 @@ func TestWriteColorOverride(t *testing.T) {
} }
func TestWriteColorOverrideBackground(t *testing.T) { func TestWriteColorOverrideBackground(t *testing.T) {
renderer := &Renderer{ renderer := &AnsiColor{
Buffer: new(bytes.Buffer), buffer: new(bytes.Buffer),
} }
text := "This is white, <,#000000>this is black</>, white again" text := "This is white, <,#000000>this is black</>, white again"
renderer.init("pwsh") renderer.init("pwsh")
@ -52,8 +52,8 @@ func TestWriteColorOverrideBackground(t *testing.T) {
} }
func TestWriteColorOverrideBackground16(t *testing.T) { func TestWriteColorOverrideBackground16(t *testing.T) {
renderer := &Renderer{ renderer := &AnsiColor{
Buffer: new(bytes.Buffer), buffer: new(bytes.Buffer),
} }
text := "This is default <,white> this background is changed</> default again" text := "This is default <,white> this background is changed</> default again"
renderer.init("pwsh") renderer.init("pwsh")
@ -64,8 +64,8 @@ func TestWriteColorOverrideBackground16(t *testing.T) {
} }
func TestWriteColorOverrideBoth(t *testing.T) { func TestWriteColorOverrideBoth(t *testing.T) {
renderer := &Renderer{ renderer := &AnsiColor{
Buffer: new(bytes.Buffer), buffer: new(bytes.Buffer),
} }
text := "This is white, <#000000,#ffffff>this is black</>, white again" text := "This is white, <#000000,#ffffff>this is black</>, white again"
renderer.init("pwsh") renderer.init("pwsh")
@ -75,8 +75,8 @@ func TestWriteColorOverrideBoth(t *testing.T) {
} }
func TestWriteColorOverrideBoth16(t *testing.T) { func TestWriteColorOverrideBoth16(t *testing.T) {
renderer := &Renderer{ renderer := &AnsiColor{
Buffer: new(bytes.Buffer), buffer: new(bytes.Buffer),
} }
text := "This is white, <black,white>this is black</>, white again" text := "This is white, <black,white>this is black</>, white again"
renderer.init("pwsh") renderer.init("pwsh")
@ -86,8 +86,8 @@ func TestWriteColorOverrideBoth16(t *testing.T) {
} }
func TestWriteColorOverrideDouble(t *testing.T) { func TestWriteColorOverrideDouble(t *testing.T) {
renderer := &Renderer{ renderer := &AnsiColor{
Buffer: new(bytes.Buffer), buffer: new(bytes.Buffer),
} }
text := "<#ffffff>jan</>@<#ffffff>Jans-MBP</>" text := "<#ffffff>jan</>@<#ffffff>Jans-MBP</>"
renderer.init("pwsh") renderer.init("pwsh")
@ -97,8 +97,8 @@ func TestWriteColorOverrideDouble(t *testing.T) {
} }
func TestWriteColorTransparent(t *testing.T) { func TestWriteColorTransparent(t *testing.T) {
renderer := &Renderer{ renderer := &AnsiColor{
Buffer: new(bytes.Buffer), buffer: new(bytes.Buffer),
} }
renderer.init("pwsh") renderer.init("pwsh")
text := "This is white" text := "This is white"
@ -108,8 +108,8 @@ func TestWriteColorTransparent(t *testing.T) {
func TestWriteColorName(t *testing.T) { func TestWriteColorName(t *testing.T) {
// given // given
renderer := &Renderer{ renderer := &AnsiColor{
Buffer: new(bytes.Buffer), buffer: new(bytes.Buffer),
} }
renderer.init("pwsh") renderer.init("pwsh")
text := "This is white, <red>this is red</>, white again" text := "This is white, <red>this is red</>, white again"
@ -123,8 +123,8 @@ func TestWriteColorName(t *testing.T) {
func TestWriteColorInvalid(t *testing.T) { func TestWriteColorInvalid(t *testing.T) {
// given // given
renderer := &Renderer{ renderer := &AnsiColor{
Buffer: new(bytes.Buffer), buffer: new(bytes.Buffer),
} }
renderer.init("pwsh") renderer.init("pwsh")
text := "This is white, <invalid>this is orange</>, white again" text := "This is white, <invalid>this is orange</>, white again"
@ -138,28 +138,28 @@ func TestWriteColorInvalid(t *testing.T) {
func TestLenWithoutANSI(t *testing.T) { func TestLenWithoutANSI(t *testing.T) {
text := "\x1b[44mhello\x1b[0m" text := "\x1b[44mhello\x1b[0m"
renderer := &Renderer{ renderer := &AnsiColor{
Buffer: new(bytes.Buffer), buffer: new(bytes.Buffer),
} }
renderer.init("pwsh") renderer.init("pwsh")
strippedLength := renderer.lenWithoutANSI(text) strippedLength := lenWithoutANSI(text, "zsh")
assert.Equal(t, 5, strippedLength) assert.Equal(t, 5, strippedLength)
} }
func TestLenWithoutANSIZsh(t *testing.T) { func TestLenWithoutANSIZsh(t *testing.T) {
text := "%{\x1b[44m%}hello%{\x1b[0m%}" text := "%{\x1b[44m%}hello%{\x1b[0m%}"
renderer := &Renderer{ renderer := &AnsiColor{
Buffer: new(bytes.Buffer), buffer: new(bytes.Buffer),
} }
renderer.init("zsh") renderer.init("zsh")
strippedLength := renderer.lenWithoutANSI(text) strippedLength := lenWithoutANSI(text, "zsh")
assert.Equal(t, 5, strippedLength) assert.Equal(t, 5, strippedLength)
} }
func TestGetAnsiFromColorStringBg(t *testing.T) { func TestGetAnsiFromColorStringBg(t *testing.T) {
// given // given
renderer := &Renderer{ renderer := &AnsiColor{
Buffer: new(bytes.Buffer), buffer: new(bytes.Buffer),
} }
// when // when
@ -171,8 +171,8 @@ func TestGetAnsiFromColorStringBg(t *testing.T) {
func TestGetAnsiFromColorStringFg(t *testing.T) { func TestGetAnsiFromColorStringFg(t *testing.T) {
// given // given
renderer := &Renderer{ renderer := &AnsiColor{
Buffer: new(bytes.Buffer), buffer: new(bytes.Buffer),
} }
// when // when
@ -184,8 +184,8 @@ func TestGetAnsiFromColorStringFg(t *testing.T) {
func TestGetAnsiFromColorStringHex(t *testing.T) { func TestGetAnsiFromColorStringHex(t *testing.T) {
// given // given
renderer := &Renderer{ renderer := &AnsiColor{
Buffer: new(bytes.Buffer), buffer: new(bytes.Buffer),
} }
// when // when
@ -197,8 +197,8 @@ func TestGetAnsiFromColorStringHex(t *testing.T) {
func TestGetAnsiFromColorStringInvalidFg(t *testing.T) { func TestGetAnsiFromColorStringInvalidFg(t *testing.T) {
// given // given
renderer := &Renderer{ renderer := &AnsiColor{
Buffer: new(bytes.Buffer), buffer: new(bytes.Buffer),
} }
// when // when
@ -210,8 +210,8 @@ func TestGetAnsiFromColorStringInvalidFg(t *testing.T) {
func TestGetAnsiFromColorStringInvalidBg(t *testing.T) { func TestGetAnsiFromColorStringInvalidBg(t *testing.T) {
// given // given
renderer := &Renderer{ renderer := &AnsiColor{
Buffer: new(bytes.Buffer), buffer: new(bytes.Buffer),
} }
// when // when

118
ansi_renderer.go Normal file
View file

@ -0,0 +1,118 @@
package main
import (
"bytes"
"fmt"
"strings"
"golang.org/x/text/unicode/norm"
)
func lenWithoutANSI(text, shell string) int {
rANSI := "[\u001B\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[a-zA-Z\\d]*)*)?\u0007)|(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PRZcf-ntqry=><~]))"
stripped := replaceAllString(rANSI, text, "")
switch shell {
case zsh:
stripped = strings.ReplaceAll(stripped, "%{", "")
stripped = strings.ReplaceAll(stripped, "%}", "")
case bash:
stripped = strings.ReplaceAll(stripped, "\\[", "")
stripped = strings.ReplaceAll(stripped, "\\]", "")
}
var i norm.Iter
i.InitString(norm.NFD, stripped)
var count int
for !i.Done() {
i.Next()
count++
}
return count
}
type formats struct {
linechange string
left string
right string
title string
creset string
clearOEL string
}
// AnsiRenderer exposes functionality using ANSI
type AnsiRenderer struct {
buffer *bytes.Buffer
formats *formats
shell string
}
const (
zsh = "zsh"
bash = "bash"
)
func (r *AnsiRenderer) init(shell string) {
r.shell = shell
r.formats = &formats{}
switch shell {
case zsh:
r.formats.linechange = "%%{\x1b[%d%s%%}"
r.formats.left = "%%{\x1b[%dC%%}"
r.formats.right = "%%{\x1b[%dD%%}"
r.formats.title = "%%{\033]0;%s\007%%}"
r.formats.creset = "%{\x1b[0m%}"
r.formats.clearOEL = "%{\x1b[K%}"
case bash:
r.formats.linechange = "\\[\x1b[%d%s\\]"
r.formats.left = "\\[\x1b[%dC\\]"
r.formats.right = "\\[\x1b[%dD\\]"
r.formats.title = "\\[\033]0;%s\007\\]"
r.formats.creset = "\\[\x1b[0m\\]"
r.formats.clearOEL = "\\[\x1b[K\\]"
default:
r.formats.linechange = "\x1b[%d%s"
r.formats.left = "\x1b[%dC"
r.formats.right = "\x1b[%dD"
r.formats.title = "\033]0;%s\007"
r.formats.creset = "\x1b[0m"
r.formats.clearOEL = "\x1b[K"
}
}
func (r *AnsiRenderer) carriageForward() {
r.buffer.WriteString(fmt.Sprintf(r.formats.left, 1000))
}
func (r *AnsiRenderer) setCursorForRightWrite(text string, offset int) {
strippedLen := lenWithoutANSI(text, r.shell) + -offset
r.buffer.WriteString(fmt.Sprintf(r.formats.right, strippedLen))
}
func (r *AnsiRenderer) changeLine(numberOfLines int) {
position := "B"
if numberOfLines < 0 {
position = "F"
numberOfLines = -numberOfLines
}
r.buffer.WriteString(fmt.Sprintf(r.formats.linechange, numberOfLines, position))
}
func (r *AnsiRenderer) setConsoleTitle(title string) {
r.buffer.WriteString(fmt.Sprintf(r.formats.title, title))
}
func (r *AnsiRenderer) creset() {
r.buffer.WriteString(r.formats.creset)
}
func (r *AnsiRenderer) print(text string) {
r.buffer.WriteString(text)
r.clearEOL()
}
func (r *AnsiRenderer) clearEOL() {
r.buffer.WriteString(r.formats.clearOEL)
}
func (r *AnsiRenderer) string() string {
return r.buffer.String()
}

View file

@ -67,7 +67,7 @@ boxes with question marks, [set up your terminal][setupterm] to use a supported
Let's take a closer look at what defines a block. Let's take a closer look at what defines a block.
- type: `prompt` | `newline` - type: `prompt` | `newline` | `rprompt`
- alignment: `left` | `right` - alignment: `left` | `right`
- vertical_offset: `int` - vertical_offset: `int`
- horizontal_offset: `int` - horizontal_offset: `int`
@ -75,9 +75,9 @@ Let's take a closer look at what defines a block.
### Type ### Type
Tells the engine what to do with the block. There are two options, either it renders one or more segments, Tells the engine what to do with the block. There are three options, either it renders one or more segments,
or it inserts a newline to start the next block on a new line. New line blocks require no additional inserts a newline to start the next block on a new line or sets a block as the `RPROMPT` when on [ZSH][rprompt].
configuration other than the `type`. New line blocks require no additional configuration other than the `type`.
### Alignment ### Alignment
@ -350,3 +350,4 @@ Oh my Posh mainly supports three different color types being
[fg]: /docs/configure#foreground [fg]: /docs/configure#foreground
[regex]: https://www.regular-expressions.info/tutorial.html [regex]: https://www.regular-expressions.info/tutorial.html
[regex-nl]: https://www.regular-expressions.info/lookaround.html [regex-nl]: https://www.regular-expressions.info/lookaround.html
[rprompt]: https://scriptingosx.com/2019/07/moving-to-zsh-06-customizing-the-zsh-prompt/

View file

@ -286,7 +286,7 @@ function omp_precmd() {
omp_now=$(oh-my-posh --millis) omp_now=$(oh-my-posh --millis)
omp_elapsed=$(($omp_now-$omp_start_time)) omp_elapsed=$(($omp_now-$omp_start_time))
fi fi
PS1="$(oh-my-posh -config ~/.poshthemes/jandedobbeleer.omp.json --error $? --execution-time $omp_elapsed)" eval "$(oh-my-posh --config ~/.poshthemes/jandedobbeleer.omp.json --error $? --execution-time $omp_elapsed --eval)"
unset omp_start_time unset omp_start_time
unset omp_now unset omp_now
unset omp_elapsed unset omp_elapsed
@ -326,7 +326,7 @@ Add the following to `~/.bashrc` (or `~/.profile` on MacOS):
```bash ```bash
function _update_ps1() { function _update_ps1() {
PS1="$(oh-my-posh -config ~/.poshthemes/jandedobbeleer.omp.json -error $?)" eval "$(oh-my-posh --config ~/.poshthemes/jandedobbeleer.omp.json --error $? --eval)"
} }
if [ "$TERM" != "linux" ] && [ -x "$(command -v oh-my-posh)" ]; then if [ "$TERM" != "linux" ] && [ -x "$(command -v oh-my-posh)" ]; then
@ -346,30 +346,6 @@ Or, when using `~/.profile`.
. ~/.profile . ~/.profile
``` ```
</TabItem>
<TabItem value="nix">
When using `nix-shell --pure`, `oh-my-posh` will not be accessible, and
your prompt will not appear.
As a workaround you can add this snippet to `~/.bashrc`,
which should re-enable the prompt in most cases:
```bash
# Workaround for nix-shell --pure
if [ "$IN_NIX_SHELL" == "pure" ]; then
if [ -x oh-my-posh ]; then
alias powerline-go="oh-my-posh -config ~/.poshthemes/jandedobbeleer.omp.json"
fi
fi
```
Once added, reload your profile for the changes to take effect.
```bash
. ~/.bashrc
```
</TabItem> </TabItem>
<TabItem value="fish"> <TabItem value="fish">
@ -377,7 +353,7 @@ Redefine `fish_prompt` in `~/.config/fish/config.fish`:
```bash ```bash
function fish_prompt function fish_prompt
eval oh-my-posh -config ~/.poshthemes/jandedobbeleer.omp.json -error $status eval oh-my-posh --config ~/.poshthemes/jandedobbeleer.omp.json --error $status --eval
end end
``` ```
@ -430,7 +406,7 @@ This will write the current configuration in your shell, allowing you to copy pa
and store it somehwere. Once adjusted to your liking, [change the prompt setting][prompt] to use the newly created file. and store it somehwere. Once adjusted to your liking, [change the prompt setting][prompt] to use the newly created file.
```bash ```bash
oh-my-posh -config ~/.mytheme.omp.json oh-my-posh --config ~/.mytheme.omp.json
``` ```
#### JSON Schema #### JSON Schema

View file

@ -8,10 +8,12 @@ import (
type engine struct { type engine struct {
settings *Settings settings *Settings
env environmentInfo env environmentInfo
renderer *Renderer color *AnsiColor
renderer *AnsiRenderer
activeBlock *Block activeBlock *Block
activeSegment *Segment activeSegment *Segment
previousActiveSegment *Segment previousActiveSegment *Segment
rprompt string
} }
func (e *engine) getPowerlineColor(foreground bool) string { func (e *engine) getPowerlineColor(foreground bool) string {
@ -33,10 +35,10 @@ func (e *engine) writePowerLineSeparator(background, foreground string, end bool
symbol = e.previousActiveSegment.PowerlineSymbol symbol = e.previousActiveSegment.PowerlineSymbol
} }
if e.activeSegment.InvertPowerline { if e.activeSegment.InvertPowerline {
e.renderer.write(foreground, background, symbol) e.color.write(foreground, background, symbol)
return return
} }
e.renderer.write(background, foreground, symbol) e.color.write(background, foreground, symbol)
} }
func (e *engine) endPowerline() { func (e *engine) endPowerline() {
@ -58,9 +60,9 @@ func (e *engine) renderPlainSegment(text string) {
} }
func (e *engine) renderDiamondSegment(text string) { func (e *engine) renderDiamondSegment(text string) {
e.renderer.write(Transparent, e.activeSegment.Background, e.activeSegment.LeadingDiamond) e.color.write(Transparent, e.activeSegment.Background, e.activeSegment.LeadingDiamond)
e.renderText(text) e.renderText(text)
e.renderer.write(Transparent, e.activeSegment.Background, e.activeSegment.TrailingDiamond) e.color.write(Transparent, e.activeSegment.Background, e.activeSegment.TrailingDiamond)
} }
func (e *engine) renderText(text string) { func (e *engine) renderText(text string) {
@ -70,9 +72,9 @@ func (e *engine) renderText(text string) {
} }
prefix := e.activeSegment.getValue(Prefix, defaultValue) prefix := e.activeSegment.getValue(Prefix, defaultValue)
postfix := e.activeSegment.getValue(Postfix, defaultValue) postfix := e.activeSegment.getValue(Postfix, defaultValue)
e.renderer.write(e.activeSegment.Background, e.activeSegment.Foreground, fmt.Sprintf("%s%s%s", prefix, text, postfix)) e.color.write(e.activeSegment.Background, e.activeSegment.Foreground, fmt.Sprintf("%s%s%s", prefix, text, postfix))
if *e.env.getArgs().Debug { if *e.env.getArgs().Debug {
e.renderer.write(e.activeSegment.Background, e.activeSegment.Foreground, fmt.Sprintf("(%s:%s)", e.activeSegment.Type, e.activeSegment.timing)) e.color.write(e.activeSegment.Background, e.activeSegment.Foreground, fmt.Sprintf("(%s:%s)", e.activeSegment.Type, e.activeSegment.timing))
} }
} }
@ -89,7 +91,7 @@ func (e *engine) renderSegmentText(text string) {
} }
func (e *engine) renderBlockSegments(block *Block) string { func (e *engine) renderBlockSegments(block *Block) string {
defer e.reset() defer e.resetBlock()
e.activeBlock = block e.activeBlock = block
e.setStringValues(block.Segments) e.setStringValues(block.Segments)
for _, segment := range block.Segments { for _, segment := range block.Segments {
@ -106,7 +108,7 @@ func (e *engine) renderBlockSegments(block *Block) string {
if e.previousActiveSegment != nil && e.previousActiveSegment.Style == Powerline { if e.previousActiveSegment != nil && e.previousActiveSegment.Style == Powerline {
e.writePowerLineSeparator(Transparent, e.previousActiveSegment.Background, true) e.writePowerLineSeparator(Transparent, e.previousActiveSegment.Background, true)
} }
return e.renderer.string() return e.color.string()
} }
func (e *engine) setStringValues(segments []*Segment) { func (e *engine) setStringValues(segments []*Segment) {
@ -126,21 +128,24 @@ func (e *engine) setStringValues(segments []*Segment) {
func (e *engine) render() { func (e *engine) render() {
for _, block := range e.settings.Blocks { for _, block := range e.settings.Blocks {
// if line break, append a line break // if line break, append a line break
if block.Type == LineBreak { switch block.Type {
case LineBreak:
e.renderer.print("\n") e.renderer.print("\n")
continue case Prompt:
} if block.VerticalOffset != 0 {
if block.VerticalOffset != 0 { e.renderer.changeLine(block.VerticalOffset)
e.renderer.changeLine(block.VerticalOffset) }
} switch block.Alignment {
switch block.Alignment { case Right:
case Right: e.renderer.carriageForward()
e.renderer.carriageForward() blockText := e.renderBlockSegments(block)
blockText := e.renderBlockSegments(block) e.renderer.setCursorForRightWrite(blockText, block.HorizontalOffset)
e.renderer.setCursorForRightWrite(blockText, block.HorizontalOffset) e.renderer.print(blockText)
e.renderer.print(blockText) case Left:
case Left: e.renderer.print(e.renderBlockSegments(block))
e.renderer.print(e.renderBlockSegments(block)) }
case RPrompt:
e.rprompt = e.renderBlockSegments(block)
} }
} }
if e.settings.ConsoleTitle { if e.settings.ConsoleTitle {
@ -157,10 +162,22 @@ func (e *engine) render() {
if e.settings.FinalSpace { if e.settings.FinalSpace {
e.renderer.print(" ") e.renderer.print(" ")
} }
e.write()
} }
func (e *engine) reset() { func (e *engine) write() {
e.renderer.reset() if *e.env.getArgs().Eval {
fmt.Printf("PS1=\"%s\"", e.renderer.string())
if e.rprompt != "" && e.env.getShellName() == zsh {
fmt.Printf("\nRPROMPT=\"%s\"", e.rprompt)
}
return
}
fmt.Print(e.renderer.string())
}
func (e *engine) resetBlock() {
e.color.reset()
e.previousActiveSegment = nil e.previousActiveSegment = nil
e.activeBlock = nil e.activeBlock = nil
} }

20
main.go
View file

@ -22,6 +22,7 @@ type args struct {
Debug *bool Debug *bool
ExecutionTime *float64 ExecutionTime *float64
Millis *bool Millis *bool
Eval *bool
} }
func main() { func main() {
@ -66,6 +67,10 @@ func main() {
"millis", "millis",
false, false,
"Get the current time in milliseconds"), "Get the current time in milliseconds"),
Eval: flag.Bool(
"eval",
false,
"Run in eval mode"),
} }
flag.Parse() flag.Parse()
env := &environment{ env := &environment{
@ -89,18 +94,23 @@ func main() {
fmt.Println(Version) fmt.Println(Version)
return return
} }
colorWriter := &Renderer{
Buffer: new(bytes.Buffer),
}
shell := env.getShellName() shell := env.getShellName()
if *args.Shell != "" { if *args.Shell != "" {
shell = *args.Shell shell = *args.Shell
} }
colorWriter.init(shell) renderer := &AnsiRenderer{
buffer: new(bytes.Buffer),
}
colorer := &AnsiColor{
buffer: new(bytes.Buffer),
}
renderer.init(shell)
colorer.init(shell)
engine := &engine{ engine := &engine{
settings: settings, settings: settings,
env: env, env: env,
renderer: colorWriter, color: colorer,
renderer: renderer,
} }
engine.render() engine.render()
} }

View file

@ -1,237 +0,0 @@
package main
import (
"bytes"
"errors"
"fmt"
"strings"
"github.com/gookit/color"
"golang.org/x/text/unicode/norm"
)
type formats struct {
single string
full string
transparent string
linechange string
left string
right string
rANSI string
title string
creset string
clearOEL string
}
var (
// Map for color names and their respective foreground [0] or background [1] color codes
colorMap map[string][2]string = map[string][2]string{
"black": {"30", "40"},
"red": {"31", "41"},
"green": {"32", "42"},
"yellow": {"33", "43"},
"blue": {"34", "44"},
"magenta": {"35", "45"},
"cyan": {"36", "46"},
"white": {"37", "47"},
"default": {"39", "49"},
"darkGray": {"90", "100"},
"lightRed": {"91", "101"},
"lightGreen": {"92", "102"},
"lightYellow": {"93", "103"},
"lightBlue": {"94", "104"},
"lightMagenta": {"95", "105"},
"lightCyan": {"96", "106"},
"lightWhite": {"97", "107"},
}
)
// Returns the color code for a given color name
func getColorFromName(colorName string, isBackground bool) (string, error) {
colorMapOffset := 0
if isBackground {
colorMapOffset = 1
}
if colorCodes, found := colorMap[colorName]; found {
return colorCodes[colorMapOffset], nil
}
return "", errors.New("color name does not exist")
}
// Renderer writes colorized strings
type Renderer struct {
Buffer *bytes.Buffer
formats *formats
shell string
}
const (
// Transparent implies a transparent color
Transparent = "transparent"
zsh = "zsh"
bash = "bash"
)
func (r *Renderer) init(shell string) {
r.shell = shell
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.linechange = "%%{\x1b[%d%s%%}"
r.formats.left = "%%{\x1b[%dC%%}"
r.formats.right = "%%{\x1b[%dD%%}"
r.formats.title = "%%{\033]0;%s\007%%}"
r.formats.creset = "%{\x1b[0m%}"
r.formats.clearOEL = "%{\x1b[K%}"
case bash:
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.linechange = "\\[\x1b[%d%s\\]"
r.formats.left = "\\[\x1b[%dC\\]"
r.formats.right = "\\[\x1b[%dD\\]"
r.formats.title = "\\[\033]0;%s\007\\]"
r.formats.creset = "\\[\x1b[0m\\]"
r.formats.clearOEL = "\\[\x1b[K\\]"
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.linechange = "\x1b[%d%s"
r.formats.left = "\x1b[%dC"
r.formats.right = "\x1b[%dD"
r.formats.title = "\033]0;%s\007"
r.formats.creset = "\x1b[0m"
r.formats.clearOEL = "\x1b[K"
}
}
// Gets the ANSI color code for a given color string.
// This can include a valid hex color in the format `#FFFFFF`,
// but also a name of one of the first 16 ANSI colors like `lightBlue`.
func (r *Renderer) getAnsiFromColorString(colorString string, isBackground bool) string {
colorFromName, err := getColorFromName(colorString, isBackground)
if err == nil {
return colorFromName
}
style := color.HEX(colorString, isBackground)
return style.Code()
}
func (r *Renderer) writeColoredText(background, foreground, text string) {
var coloredText string
if foreground == Transparent && background != "" {
ansiColor := r.getAnsiFromColorString(background, false)
coloredText = fmt.Sprintf(r.formats.transparent, ansiColor, text)
} else if background == "" || background == Transparent {
ansiColor := r.getAnsiFromColorString(foreground, false)
coloredText = fmt.Sprintf(r.formats.single, ansiColor, text)
} else if foreground != "" && background != "" {
bgAnsiColor := r.getAnsiFromColorString(background, true)
fgAnsiColor := r.getAnsiFromColorString(foreground, false)
coloredText = fmt.Sprintf(r.formats.full, bgAnsiColor, fgAnsiColor, text)
}
r.Buffer.WriteString(coloredText)
}
func (r *Renderer) writeAndRemoveText(background, foreground, text, textToRemove, parentText string) string {
r.writeColoredText(background, foreground, text)
return strings.Replace(parentText, textToRemove, "", 1)
}
func (r *Renderer) write(background, foreground, text string) {
// first we match for any potentially valid colors enclosed in <>
match := findAllNamedRegexMatch(`<(?P<foreground>[^,>]+)?,?(?P<background>[^>]+)?>(?P<content>[^<]*)<\/>`, text)
for i := range match {
extractedForegroundColor := match[i]["foreground"]
extractedBackgroundColor := match[i]["background"]
if col := r.getAnsiFromColorString(extractedForegroundColor, false); col == "" && extractedForegroundColor != Transparent && len(extractedBackgroundColor) == 0 {
continue // we skip invalid colors
}
if col := r.getAnsiFromColorString(extractedBackgroundColor, false); col == "" && extractedBackgroundColor != Transparent && len(extractedForegroundColor) == 0 {
continue // we skip invalid colors
}
// reuse function colors if only one was specified
if len(extractedBackgroundColor) == 0 {
extractedBackgroundColor = background
}
if len(extractedForegroundColor) == 0 {
extractedForegroundColor = foreground
}
escapedTextSegment := match[i]["text"]
innerText := match[i]["content"]
textBeforeColorOverride := strings.Split(text, escapedTextSegment)[0]
text = r.writeAndRemoveText(background, foreground, textBeforeColorOverride, textBeforeColorOverride, text)
text = r.writeAndRemoveText(extractedBackgroundColor, extractedForegroundColor, innerText, escapedTextSegment, text)
}
// color the remaining part of text with background and foreground
r.writeColoredText(background, foreground, text)
}
func (r *Renderer) lenWithoutANSI(str string) int {
stripped := replaceAllString(r.formats.rANSI, str, "")
switch r.shell {
case zsh:
stripped = strings.ReplaceAll(stripped, "%{", "")
stripped = strings.ReplaceAll(stripped, "%}", "")
case bash:
stripped = strings.ReplaceAll(stripped, "\\[", "")
stripped = strings.ReplaceAll(stripped, "\\]", "")
}
var i norm.Iter
i.InitString(norm.NFD, stripped)
var count int
for !i.Done() {
i.Next()
count++
}
return count
}
func (r *Renderer) carriageForward() {
fmt.Printf(r.formats.left, 1000)
}
func (r *Renderer) setCursorForRightWrite(text string, offset int) {
strippedLen := r.lenWithoutANSI(text) + -offset
fmt.Printf(r.formats.right, strippedLen)
}
func (r *Renderer) changeLine(numberOfLines int) {
position := "B"
if numberOfLines < 0 {
position = "F"
numberOfLines = -numberOfLines
}
fmt.Printf(r.formats.linechange, numberOfLines, position)
}
func (r *Renderer) setConsoleTitle(title string) {
fmt.Printf(r.formats.title, title)
}
func (r *Renderer) string() string {
return r.Buffer.String()
}
func (r *Renderer) reset() {
r.Buffer.Reset()
}
func (r *Renderer) creset() {
fmt.Print(r.formats.creset)
}
func (r *Renderer) print(text string) {
fmt.Print(text)
r.clearEOL()
}
func (r *Renderer) clearEOL() {
fmt.Print(r.formats.clearOEL)
}

View file

@ -28,6 +28,8 @@ const (
Prompt BlockType = "prompt" Prompt BlockType = "prompt"
// LineBreak creates a line break in the prompt // LineBreak creates a line break in the prompt
LineBreak BlockType = "newline" LineBreak BlockType = "newline"
// RPrompt a right aligned prompt in ZSH
RPrompt BlockType = "rprompt"
// Left aligns left // Left aligns left
Left BlockAlignment = "left" Left BlockAlignment = "left"
// Right aligns right // Right aligns right

View file

@ -7,7 +7,7 @@
"definitions": { "definitions": {
"color": { "color": {
"type": "string", "type": "string",
"pattern": "^(#([a-fA-F0-9]{6}|[a-fA-F0-9]{3})|black|red|green|yellow|blue|magenta|cyan|white|default|darkGray|lightRed|lightGreen|lightYellow|lightBlue|lightMagenta|lightCyan|lightWhite)$", "pattern": "^(#([a-fA-F0-9]{6}|[a-fA-F0-9]{3})|black|red|green|yellow|blue|magenta|cyan|white|default|darkGray|lightRed|lightGreen|lightYellow|lightBlue|lightMagenta|lightCyan|lightWhite|transparent)$",
"title": "Color string", "title": "Color string",
"description": "https://ohmyposh.dev/docs/configure#colors" "description": "https://ohmyposh.dev/docs/configure#colors"
}, },
@ -30,11 +30,29 @@
"then": { "then": {
"required": ["type"], "required": ["type"],
"title": "Newline, renders a line break" "title": "Newline, renders a line break"
}
},
{
"if": {
"properties": {
"type": { "const": "prompt" }
}
}, },
"else": { "then": {
"required": ["type", "alignment", "segments"], "required": ["type", "alignment", "segments"],
"title": "Prompt definition, contains 1 or more segments to render" "title": "Prompt definition, contains 1 or more segments to render"
} }
},
{
"if": {
"properties": {
"type": { "const": "rprompt" }
}
},
"then": {
"required": ["type", "segments"],
"title": "RPrompt definition, contains 1 or more segments to render in ZSH RPROMPT"
}
} }
], ],
"properties": { "properties": {
@ -42,7 +60,7 @@
"type": "string", "type": "string",
"title": "Block type", "title": "Block type",
"description": "https://ohmyposh.dev/docs/configure#type", "description": "https://ohmyposh.dev/docs/configure#type",
"enum": ["prompt", "newline"], "enum": ["prompt", "newline", "rprompt"],
"default": "prompt" "default": "prompt"
}, },
"alignment": { "alignment": {