mirror of
https://github.com/JanDeDobbeleer/oh-my-posh.git
synced 2025-03-05 20:49:04 -08:00
refactor: count length using written text
This commit is contained in:
parent
06e08074fe
commit
04e6579a8e
|
@ -7,14 +7,9 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
ansiRegex = "[\u001B\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[a-zA-Z\\d]*)*)?\u0007)|(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PRZcf-ntqry=><~]))"
|
|
||||||
|
|
||||||
zsh = "zsh"
|
zsh = "zsh"
|
||||||
bash = "bash"
|
bash = "bash"
|
||||||
pwsh = "pwsh"
|
pwsh = "pwsh"
|
||||||
|
|
||||||
str = "STR"
|
|
||||||
url = "URL"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Ansi struct {
|
type Ansi struct {
|
||||||
|
@ -127,27 +122,6 @@ func (a *Ansi) Init(shell string) {
|
||||||
a.shellReservedKeywords = append(a.shellReservedKeywords, shellKeyWordReplacement{"`", "'"})
|
a.shellReservedKeywords = append(a.shellReservedKeywords, shellKeyWordReplacement{"`", "'"})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Ansi) LenWithoutANSI(text string) int {
|
|
||||||
if len(text) == 0 {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
// replace hyperlinks(file/http/https)
|
|
||||||
matches := regex.FindAllNamedRegexMatch(`(?P<STR>\x1b]8;;(file|http|https):\/\/(.+?)\x1b\\(?P<URL>.+?)\x1b]8;;\x1b\\)`, text)
|
|
||||||
for _, match := range matches {
|
|
||||||
text = strings.ReplaceAll(text, match[str], match[url])
|
|
||||||
}
|
|
||||||
// replace console title
|
|
||||||
matches = regex.FindAllNamedRegexMatch(`(?P<STR>\x1b\]0;(.+)\007)`, text)
|
|
||||||
for _, match := range matches {
|
|
||||||
text = strings.ReplaceAll(text, match[str], "")
|
|
||||||
}
|
|
||||||
stripped := regex.ReplaceAllString(ansiRegex, text, "")
|
|
||||||
stripped = strings.ReplaceAll(stripped, a.escapeLeft, "")
|
|
||||||
stripped = strings.ReplaceAll(stripped, a.escapeRight, "")
|
|
||||||
runeText := []rune(stripped)
|
|
||||||
return len(runeText)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *Ansi) generateHyperlink(text string) string {
|
func (a *Ansi) generateHyperlink(text string) string {
|
||||||
// hyperlink matching
|
// hyperlink matching
|
||||||
results := regex.FindNamedRegexMatch("(?P<all>(?:\\[(?P<name>.+)\\])(?:\\((?P<url>.*)\\)))", text)
|
results := regex.FindNamedRegexMatch("(?P<all>(?:\\[(?P<name>.+)\\])(?:\\((?P<url>.*)\\)))", text)
|
||||||
|
@ -183,8 +157,8 @@ func (a *Ansi) CarriageForward() string {
|
||||||
return fmt.Sprintf(a.right, 1000)
|
return fmt.Sprintf(a.right, 1000)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Ansi) GetCursorForRightWrite(text string, offset int) string {
|
func (a *Ansi) GetCursorForRightWrite(length, offset int) string {
|
||||||
strippedLen := a.LenWithoutANSI(text) + -offset
|
strippedLen := length + (-offset)
|
||||||
return fmt.Sprintf(a.left, strippedLen)
|
return fmt.Sprintf(a.left, strippedLen)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,24 +6,6 @@ import (
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestLenWithoutAnsi(t *testing.T) {
|
|
||||||
cases := []struct {
|
|
||||||
Text string
|
|
||||||
ShellName string
|
|
||||||
Expected int
|
|
||||||
}{
|
|
||||||
{Text: "%{\x1b[44m%}hello%{\x1b[0m%}", ShellName: zsh, Expected: 5},
|
|
||||||
{Text: "\x1b[44mhello\x1b[0m", ShellName: pwsh, Expected: 5},
|
|
||||||
{Text: "\\[\x1b[44m\\]hello\\[\x1b[0m\\]", ShellName: bash, Expected: 5},
|
|
||||||
}
|
|
||||||
for _, tc := range cases {
|
|
||||||
a := Ansi{}
|
|
||||||
a.Init(tc.ShellName)
|
|
||||||
strippedLength := a.LenWithoutANSI(tc.Text)
|
|
||||||
assert.Equal(t, 5, strippedLength)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGenerateHyperlinkNoUrl(t *testing.T) {
|
func TestGenerateHyperlinkNoUrl(t *testing.T) {
|
||||||
cases := []struct {
|
cases := []struct {
|
||||||
Text string
|
Text string
|
||||||
|
|
|
@ -8,6 +8,7 @@ import (
|
||||||
// PlainWriter writes a plain string
|
// PlainWriter writes a plain string
|
||||||
type PlainWriter struct {
|
type PlainWriter struct {
|
||||||
builder strings.Builder
|
builder strings.Builder
|
||||||
|
length int
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *PlainWriter) SetColors(background, foreground string) {}
|
func (a *PlainWriter) SetColors(background, foreground string) {}
|
||||||
|
@ -19,6 +20,7 @@ func (a *PlainWriter) Write(background, foreground, text string) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
writeAndRemoveText := func(text, textToRemove, parentText string) string {
|
writeAndRemoveText := func(text, textToRemove, parentText string) string {
|
||||||
|
a.length += measureText(text)
|
||||||
a.builder.WriteString(text)
|
a.builder.WriteString(text)
|
||||||
return strings.Replace(parentText, textToRemove, "", 1)
|
return strings.Replace(parentText, textToRemove, "", 1)
|
||||||
}
|
}
|
||||||
|
@ -30,11 +32,12 @@ func (a *PlainWriter) Write(background, foreground, text string) {
|
||||||
text = writeAndRemoveText(textBeforeColorOverride, textBeforeColorOverride, text)
|
text = writeAndRemoveText(textBeforeColorOverride, textBeforeColorOverride, text)
|
||||||
text = writeAndRemoveText(innerText, escapedTextSegment, text)
|
text = writeAndRemoveText(innerText, escapedTextSegment, text)
|
||||||
}
|
}
|
||||||
|
a.length += measureText(text)
|
||||||
a.builder.WriteString(text)
|
a.builder.WriteString(text)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *PlainWriter) String() string {
|
func (a *PlainWriter) String() (string, int) {
|
||||||
return a.builder.String()
|
return a.builder.String(), a.length
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *PlainWriter) Reset() {
|
func (a *PlainWriter) Reset() {
|
||||||
|
|
8
src/color/text.go
Normal file
8
src/color/text.go
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
package color
|
||||||
|
|
||||||
|
import "unicode/utf8"
|
||||||
|
|
||||||
|
func measureText(text string) int {
|
||||||
|
length := utf8.RuneCountInString(text)
|
||||||
|
return length
|
||||||
|
}
|
|
@ -12,7 +12,7 @@ const (
|
||||||
|
|
||||||
type Writer interface {
|
type Writer interface {
|
||||||
Write(background, foreground, text string)
|
Write(background, foreground, text string)
|
||||||
String() string
|
String() (string, int)
|
||||||
Reset()
|
Reset()
|
||||||
SetColors(background, foreground string)
|
SetColors(background, foreground string)
|
||||||
SetParentColors(background, foreground string)
|
SetParentColors(background, foreground string)
|
||||||
|
@ -28,6 +28,7 @@ type AnsiWriter struct {
|
||||||
AnsiColors AnsiColors
|
AnsiColors AnsiColors
|
||||||
|
|
||||||
builder strings.Builder
|
builder strings.Builder
|
||||||
|
length int
|
||||||
}
|
}
|
||||||
|
|
||||||
type Color struct {
|
type Color struct {
|
||||||
|
@ -104,6 +105,7 @@ func (a *AnsiWriter) writeColoredText(background, foreground AnsiColor, text str
|
||||||
if text == "" || (foreground.IsTransparent() && background.IsTransparent()) {
|
if text == "" || (foreground.IsTransparent() && background.IsTransparent()) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
a.length += measureText(text)
|
||||||
// default to white fg if empty, empty backgrond is supported
|
// default to white fg if empty, empty backgrond is supported
|
||||||
if foreground.IsEmpty() {
|
if foreground.IsEmpty() {
|
||||||
foreground = a.getAnsiFromColorString("white", false)
|
foreground = a.getAnsiFromColorString("white", false)
|
||||||
|
@ -226,10 +228,11 @@ func (a *AnsiWriter) expandKeyword(keyword string) string {
|
||||||
return keyword
|
return keyword
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *AnsiWriter) String() string {
|
func (a *AnsiWriter) String() (string, int) {
|
||||||
return a.builder.String()
|
return a.builder.String(), a.length
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *AnsiWriter) Reset() {
|
func (a *AnsiWriter) Reset() {
|
||||||
|
a.length = 0
|
||||||
a.builder.Reset()
|
a.builder.Reset()
|
||||||
}
|
}
|
||||||
|
|
|
@ -181,7 +181,7 @@ func TestWriteANSIColors(t *testing.T) {
|
||||||
AnsiColors: &DefaultColors{},
|
AnsiColors: &DefaultColors{},
|
||||||
}
|
}
|
||||||
renderer.Write(tc.Colors.Background, tc.Colors.Foreground, tc.Input)
|
renderer.Write(tc.Colors.Background, tc.Colors.Foreground, tc.Input)
|
||||||
got := renderer.String()
|
got, _ := renderer.String()
|
||||||
assert.Equal(t, tc.Expected, got, tc.Case)
|
assert.Equal(t, tc.Expected, got, tc.Case)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -42,6 +42,7 @@ type Block struct {
|
||||||
previousActiveSegment *Segment
|
previousActiveSegment *Segment
|
||||||
activeBackground string
|
activeBackground string
|
||||||
activeForeground string
|
activeForeground string
|
||||||
|
length int
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Block) init(env environment.Environment, writer color.Writer, ansi *color.Ansi) {
|
func (b *Block) init(env environment.Environment, writer color.Writer, ansi *color.Ansi) {
|
||||||
|
@ -92,7 +93,7 @@ func (b *Block) renderSegmentsText() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Block) renderSegments() string {
|
func (b *Block) renderSegments() (string, int) {
|
||||||
defer b.writer.Reset()
|
defer b.writer.Reset()
|
||||||
for _, segment := range b.Segments {
|
for _, segment := range b.Segments {
|
||||||
if !segment.active {
|
if !segment.active {
|
||||||
|
@ -180,7 +181,7 @@ func (b *Block) debug() (int, []*SegmentTiming) {
|
||||||
segmentTiming.text = segment.text
|
segmentTiming.text = segment.text
|
||||||
if segmentTiming.active {
|
if segmentTiming.active {
|
||||||
b.renderSegment(segment)
|
b.renderSegment(segment)
|
||||||
segmentTiming.text = b.writer.String()
|
segmentTiming.text, b.length = b.writer.String()
|
||||||
b.writer.Reset()
|
b.writer.Reset()
|
||||||
}
|
}
|
||||||
segmentTiming.duration = time.Since(start)
|
segmentTiming.duration = time.Since(start)
|
||||||
|
|
|
@ -19,7 +19,9 @@ type Engine struct {
|
||||||
Plain bool
|
Plain bool
|
||||||
|
|
||||||
console strings.Builder
|
console strings.Builder
|
||||||
|
currentLineLength int
|
||||||
rprompt string
|
rprompt string
|
||||||
|
rpromptLength int
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *Engine) write(text string) {
|
func (e *Engine) write(text string) {
|
||||||
|
@ -38,12 +40,11 @@ func (e *Engine) string() string {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *Engine) canWriteRPrompt() bool {
|
func (e *Engine) canWriteRPrompt() bool {
|
||||||
prompt := e.string()
|
|
||||||
consoleWidth, err := e.Env.TerminalWidth()
|
consoleWidth, err := e.Env.TerminalWidth()
|
||||||
if err != nil || consoleWidth == 0 {
|
if err != nil || consoleWidth == 0 {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
promptWidth := e.Ansi.LenWithoutANSI(prompt)
|
promptWidth := e.currentLineLength
|
||||||
availableSpace := consoleWidth - promptWidth
|
availableSpace := consoleWidth - promptWidth
|
||||||
// spanning multiple lines
|
// spanning multiple lines
|
||||||
if availableSpace < 0 {
|
if availableSpace < 0 {
|
||||||
|
@ -51,7 +52,7 @@ func (e *Engine) canWriteRPrompt() bool {
|
||||||
availableSpace = consoleWidth - overflow
|
availableSpace = consoleWidth - overflow
|
||||||
}
|
}
|
||||||
promptBreathingRoom := 30
|
promptBreathingRoom := 30
|
||||||
canWrite := (availableSpace - e.Ansi.LenWithoutANSI(e.rprompt)) >= promptBreathingRoom
|
canWrite := (availableSpace - e.rpromptLength) >= promptBreathingRoom
|
||||||
return canWrite
|
return canWrite
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -66,7 +67,6 @@ func (e *Engine) Render() string {
|
||||||
if e.Config.FinalSpace {
|
if e.Config.FinalSpace {
|
||||||
e.write(" ")
|
e.write(" ")
|
||||||
}
|
}
|
||||||
|
|
||||||
if !e.Config.OSC99 {
|
if !e.Config.OSC99 {
|
||||||
return e.print()
|
return e.print()
|
||||||
}
|
}
|
||||||
|
@ -75,6 +75,11 @@ func (e *Engine) Render() string {
|
||||||
return e.print()
|
return e.print()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (e *Engine) newline() {
|
||||||
|
e.write("\n")
|
||||||
|
e.currentLineLength = 0
|
||||||
|
}
|
||||||
|
|
||||||
func (e *Engine) renderBlock(block *Block) {
|
func (e *Engine) renderBlock(block *Block) {
|
||||||
// when in bash, for rprompt blocks we need to write plain
|
// when in bash, for rprompt blocks we need to write plain
|
||||||
// and wrap in escaped mode or the prompt will not render correctly
|
// and wrap in escaped mode or the prompt will not render correctly
|
||||||
|
@ -88,14 +93,14 @@ func (e *Engine) renderBlock(block *Block) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if block.Newline {
|
if block.Newline {
|
||||||
e.write("\n")
|
e.newline()
|
||||||
}
|
}
|
||||||
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.write("\n")
|
e.newline()
|
||||||
case Prompt:
|
case Prompt:
|
||||||
if block.VerticalOffset != 0 {
|
if block.VerticalOffset != 0 {
|
||||||
e.writeANSI(e.Ansi.ChangeLine(block.VerticalOffset))
|
e.writeANSI(e.Ansi.ChangeLine(block.VerticalOffset))
|
||||||
|
@ -103,18 +108,22 @@ func (e *Engine) renderBlock(block *Block) {
|
||||||
switch block.Alignment {
|
switch block.Alignment {
|
||||||
case Right:
|
case Right:
|
||||||
e.writeANSI(e.Ansi.CarriageForward())
|
e.writeANSI(e.Ansi.CarriageForward())
|
||||||
blockText := block.renderSegments()
|
text, length := block.renderSegments()
|
||||||
e.writeANSI(e.Ansi.GetCursorForRightWrite(blockText, block.HorizontalOffset))
|
e.currentLineLength += length
|
||||||
e.write(blockText)
|
e.writeANSI(e.Ansi.GetCursorForRightWrite(length, block.HorizontalOffset))
|
||||||
|
e.write(text)
|
||||||
case Left:
|
case Left:
|
||||||
e.write(block.renderSegments())
|
text, length := block.renderSegments()
|
||||||
|
e.currentLineLength += length
|
||||||
|
e.write(text)
|
||||||
}
|
}
|
||||||
case RPrompt:
|
case RPrompt:
|
||||||
blockText := block.renderSegments()
|
text, length := block.renderSegments()
|
||||||
|
e.rpromptLength = length
|
||||||
if e.Env.Shell() == bash {
|
if e.Env.Shell() == bash {
|
||||||
blockText = e.Ansi.FormatText(blockText)
|
text = e.Ansi.FormatText(text)
|
||||||
}
|
}
|
||||||
e.rprompt = blockText
|
e.rprompt = text
|
||||||
}
|
}
|
||||||
// Due to a bug in Powershell, the end of the line needs to be cleared.
|
// 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
|
// If this doesn't happen, the portion after the prompt gets colored in the background
|
||||||
|
@ -183,7 +192,7 @@ func (e *Engine) print() string {
|
||||||
}
|
}
|
||||||
e.write(e.Ansi.SaveCursorPosition())
|
e.write(e.Ansi.SaveCursorPosition())
|
||||||
e.write(e.Ansi.CarriageForward())
|
e.write(e.Ansi.CarriageForward())
|
||||||
e.write(e.Ansi.GetCursorForRightWrite(e.rprompt, 0))
|
e.write(e.Ansi.GetCursorForRightWrite(e.rpromptLength, 0))
|
||||||
e.write(e.rprompt)
|
e.write(e.rprompt)
|
||||||
e.write(e.Ansi.RestoreCursorPosition())
|
e.write(e.Ansi.RestoreCursorPosition())
|
||||||
}
|
}
|
||||||
|
@ -217,14 +226,15 @@ func (e *Engine) RenderTooltip(tip string) string {
|
||||||
switch e.Env.Shell() {
|
switch e.Env.Shell() {
|
||||||
case zsh, winCMD:
|
case zsh, winCMD:
|
||||||
block.init(e.Env, e.Writer, e.Ansi)
|
block.init(e.Env, e.Writer, e.Ansi)
|
||||||
return block.renderSegments()
|
text, _ := block.renderSegments()
|
||||||
|
return text
|
||||||
case pwsh, powershell5:
|
case pwsh, powershell5:
|
||||||
block.initPlain(e.Env, e.Config)
|
block.initPlain(e.Env, e.Config)
|
||||||
tooltipText := block.renderSegments()
|
text, length := block.renderSegments()
|
||||||
e.write(e.Ansi.ClearAfter())
|
e.write(e.Ansi.ClearAfter())
|
||||||
e.write(e.Ansi.CarriageForward())
|
e.write(e.Ansi.CarriageForward())
|
||||||
e.write(e.Ansi.GetCursorForRightWrite(tooltipText, 0))
|
e.write(e.Ansi.GetCursorForRightWrite(length, 0))
|
||||||
e.write(tooltipText)
|
e.write(text)
|
||||||
return e.string()
|
return e.string()
|
||||||
}
|
}
|
||||||
return ""
|
return ""
|
||||||
|
@ -251,11 +261,13 @@ func (e *Engine) RenderTransientPrompt() string {
|
||||||
switch e.Env.Shell() {
|
switch e.Env.Shell() {
|
||||||
case zsh:
|
case zsh:
|
||||||
// escape double quotes contained in the prompt
|
// escape double quotes contained in the prompt
|
||||||
prompt := fmt.Sprintf("PS1=\"%s\"", strings.ReplaceAll(e.Writer.String(), "\"", "\"\""))
|
str, _ := e.Writer.String()
|
||||||
|
prompt := fmt.Sprintf("PS1=\"%s\"", strings.ReplaceAll(str, "\"", "\"\""))
|
||||||
prompt += "\nRPROMPT=\"\""
|
prompt += "\nRPROMPT=\"\""
|
||||||
return prompt
|
return prompt
|
||||||
case pwsh, powershell5, winCMD:
|
case pwsh, powershell5, winCMD:
|
||||||
return e.Writer.String()
|
str, _ := e.Writer.String()
|
||||||
|
return str
|
||||||
}
|
}
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
@ -278,5 +290,7 @@ func (e *Engine) RenderRPrompt() string {
|
||||||
if !block.enabled() {
|
if !block.enabled() {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
return block.renderSegments()
|
text, length := block.renderSegments()
|
||||||
|
e.rpromptLength = length
|
||||||
|
return text
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,6 @@ import (
|
||||||
"oh-my-posh/mock"
|
"oh-my-posh/mock"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
@ -26,8 +25,8 @@ func TestCanWriteRPrompt(t *testing.T) {
|
||||||
{Case: "Width Error", Expected: true, TerminalWidthError: errors.New("burp")},
|
{Case: "Width Error", Expected: true, TerminalWidthError: errors.New("burp")},
|
||||||
{Case: "Terminal > Prompt enabled", Expected: true, TerminalWidth: 200, PromptLength: 100, RPromptLength: 10},
|
{Case: "Terminal > Prompt enabled", Expected: true, TerminalWidth: 200, PromptLength: 100, RPromptLength: 10},
|
||||||
{Case: "Terminal > Prompt enabled edge", Expected: true, TerminalWidth: 200, PromptLength: 100, RPromptLength: 70},
|
{Case: "Terminal > Prompt enabled edge", Expected: true, TerminalWidth: 200, PromptLength: 100, RPromptLength: 70},
|
||||||
{Case: "Terminal > Prompt disabled no breathing", Expected: false, TerminalWidth: 200, PromptLength: 100, RPromptLength: 71},
|
|
||||||
{Case: "Prompt > Terminal enabled", Expected: true, TerminalWidth: 200, PromptLength: 300, RPromptLength: 70},
|
{Case: "Prompt > Terminal enabled", Expected: true, TerminalWidth: 200, PromptLength: 300, RPromptLength: 70},
|
||||||
|
{Case: "Terminal > Prompt disabled no breathing", Expected: false, TerminalWidth: 200, PromptLength: 100, RPromptLength: 71},
|
||||||
{Case: "Prompt > Terminal disabled no breathing", Expected: false, TerminalWidth: 200, PromptLength: 300, RPromptLength: 80},
|
{Case: "Prompt > Terminal disabled no breathing", Expected: false, TerminalWidth: 200, PromptLength: 300, RPromptLength: 80},
|
||||||
{Case: "Prompt > Terminal disabled no room", Expected: true, TerminalWidth: 200, PromptLength: 400, RPromptLength: 80},
|
{Case: "Prompt > Terminal disabled no room", Expected: true, TerminalWidth: 200, PromptLength: 400, RPromptLength: 80},
|
||||||
}
|
}
|
||||||
|
@ -35,14 +34,11 @@ func TestCanWriteRPrompt(t *testing.T) {
|
||||||
for _, tc := range cases {
|
for _, tc := range cases {
|
||||||
env := new(mock.MockedEnvironment)
|
env := new(mock.MockedEnvironment)
|
||||||
env.On("TerminalWidth").Return(tc.TerminalWidth, tc.TerminalWidthError)
|
env.On("TerminalWidth").Return(tc.TerminalWidth, tc.TerminalWidthError)
|
||||||
ansi := &color.Ansi{}
|
|
||||||
ansi.Init(plain)
|
|
||||||
engine := &Engine{
|
engine := &Engine{
|
||||||
Env: env,
|
Env: env,
|
||||||
Ansi: ansi,
|
|
||||||
}
|
}
|
||||||
engine.rprompt = strings.Repeat("x", tc.RPromptLength)
|
engine.rpromptLength = tc.RPromptLength
|
||||||
engine.console.WriteString(strings.Repeat("x", tc.PromptLength))
|
engine.currentLineLength = tc.PromptLength
|
||||||
got := engine.canWriteRPrompt()
|
got := engine.canWriteRPrompt()
|
||||||
assert.Equal(t, tc.Expected, got, tc.Case)
|
assert.Equal(t, tc.Expected, got, tc.Case)
|
||||||
}
|
}
|
||||||
|
|
|
@ -67,6 +67,8 @@ const (
|
||||||
lineChange = "linechange"
|
lineChange = "linechange"
|
||||||
consoleTitle = "title"
|
consoleTitle = "title"
|
||||||
link = "link"
|
link = "link"
|
||||||
|
|
||||||
|
ansiRegex = "[\u001B\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[a-zA-Z\\d]*)*)?\u0007)|(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PRZcf-ntqry=><~]))"
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:embed font/Hack-Nerd-Bold.ttf
|
//go:embed font/Hack-Nerd-Bold.ttf
|
||||||
|
@ -187,7 +189,7 @@ func (ir *ImageRenderer) Init(config string) {
|
||||||
osc99: `^(?P<STR>\x1b\]9;9;(.+)\x1b\\)`,
|
osc99: `^(?P<STR>\x1b\]9;9;(.+)\x1b\\)`,
|
||||||
lineChange: `^(?P<STR>\x1b\[(\d)[FB])`,
|
lineChange: `^(?P<STR>\x1b\[(\d)[FB])`,
|
||||||
consoleTitle: `^(?P<STR>\x1b\]0;(.+)\007)`,
|
consoleTitle: `^(?P<STR>\x1b\]0;(.+)\007)`,
|
||||||
link: `^(?P<STR>\x1b]8;;file:\/\/(.+)\x1b\\(?P<URL>.+)\x1b]8;;\x1b\\)`,
|
link: `^(?P<STR>\x1b]8;;(file|https)(.+)\x1b\\(?P<URL>.+)\x1b]8;;\x1b\\)`,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -247,13 +249,35 @@ func (ir *ImageRenderer) runeAdditionalWidth(r rune) int {
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ir *ImageRenderer) lenWithoutANSI(text string) int {
|
||||||
|
if len(text) == 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
// replace hyperlinks(file/http/https)
|
||||||
|
regexStr := ir.ansiSequenceRegexMap[link]
|
||||||
|
matches := regex.FindAllNamedRegexMatch(regexStr, text)
|
||||||
|
for _, match := range matches {
|
||||||
|
text = strings.ReplaceAll(text, match[str], match[url])
|
||||||
|
}
|
||||||
|
// replace console title
|
||||||
|
regexStr = ir.ansiSequenceRegexMap[consoleTitle]
|
||||||
|
matches = regex.FindAllNamedRegexMatch(regexStr, text)
|
||||||
|
for _, match := range matches {
|
||||||
|
text = strings.ReplaceAll(text, match[str], "")
|
||||||
|
}
|
||||||
|
stripped := regex.ReplaceAllString(ansiRegex, text, "")
|
||||||
|
runeText := []rune(stripped)
|
||||||
|
length := len(runeText)
|
||||||
|
for _, rune := range runeText {
|
||||||
|
length += ir.runeAdditionalWidth(rune)
|
||||||
|
}
|
||||||
|
return length
|
||||||
|
}
|
||||||
|
|
||||||
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.Ansi.LenWithoutANSI(line)
|
length := ir.lenWithoutANSI(line)
|
||||||
for _, char := range line {
|
|
||||||
length += ir.runeAdditionalWidth(char)
|
|
||||||
}
|
|
||||||
if length > longest {
|
if length > longest {
|
||||||
longest = length
|
longest = length
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,9 +33,7 @@ func TestStringImageFileWithText(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestStringImageFileWithANSI(t *testing.T) {
|
func TestStringImageFileWithANSI(t *testing.T) {
|
||||||
prompt := `[38;2;0;55;218;49m[7m\uE0B0[m[0m[48;2;0;55;218m[38;2;255;255;255m oh-my-posh
|
prompt := `[38;2;40;105;131m[0m[48;2;40;105;131m[38;2;224;222;244m jan [0m[38;2;40;105;131m[0m[38;2;224;222;244m [0m`
|
||||||
[0m[48;2;193;156;0m[38;2;0;55;218m\uE0B0[0m[48;2;193;156;0m[38;2;17;17;17m main ≡ ~4 -8 ?7 [0m[38;2;193;156;0m\uE0B0[0m
|
|
||||||
[37m [0m[0m`
|
|
||||||
err := runImageTest(prompt)
|
err := runImageTest(prompt)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue