Skip to content

Commit

Permalink
[color] Use graphemes to measure strings.
Browse files Browse the repository at this point in the history
The number of Unicode code points in a string is not the same as the
number of user-visible characters (graphemes). When measuring colorized
strings, we want the latter rather than the former. Notably, these
changes fix some issues where the interactive display cut off before the
right edge of the terminal.
  • Loading branch information
pgavlin committed Nov 2, 2022
1 parent 008e45c commit 221795e
Show file tree
Hide file tree
Showing 15 changed files with 4,560 additions and 4,520 deletions.
4 changes: 1 addition & 3 deletions pkg/backend/display/jsonmessage.go
Expand Up @@ -188,9 +188,7 @@ func (r *messageRenderer) tick(display *ProgressDisplay) {
func (r *messageRenderer) renderRow(display *ProgressDisplay,
id string, colorizedColumns []string, maxColumnLengths []int) {

uncolorizedColumns := display.uncolorizeColumns(colorizedColumns)

row := renderRow(colorizedColumns, uncolorizedColumns, maxColumnLengths)
row := renderRow(colorizedColumns, maxColumnLengths)
if r.isInteractive {
// Ensure we don't go past the end of the terminal. Note: this is made complex due to
// msgWithColors having the color code information embedded with it. So we need to get
Expand Down
57 changes: 15 additions & 42 deletions pkg/backend/display/progress.go
Expand Up @@ -24,7 +24,6 @@ import (
"strings"
"time"
"unicode"
"unicode/utf8"

"github.com/pulumi/pulumi/pkg/v3/backend/display/internal/terminal"
"github.com/pulumi/pulumi/pkg/v3/engine"
Expand Down Expand Up @@ -137,10 +136,6 @@ type ProgressDisplay struct {
// Maps used so we can generate short IDs for resource urns.
urnToID map[resource.URN]string

// Cache of colorized to uncolorized text. We go between the two a lot, so caching helps
// prevent lots of recomputation
colorizedToUncolorized map[string]string

// Structure that tracks the time taken to perform an action on a resource.
opStopwatch opStopwatch
}
Expand Down Expand Up @@ -254,20 +249,19 @@ func ShowProgressEvents(op string, action apitype.UpdateKind, stack tokens.Name,
}

display := &ProgressDisplay{
action: action,
isPreview: isPreview,
isTerminal: isInteractive,
opts: opts,
renderer: renderer,
stack: stack,
proj: proj,
eventUrnToResourceRow: make(map[resource.URN]ResourceRow),
suffixColumn: int(statusColumn),
suffixesArray: []string{"", ".", "..", "..."},
urnToID: make(map[resource.URN]string),
colorizedToUncolorized: make(map[string]string),
displayOrderCounter: 1,
opStopwatch: newOpStopwatch(),
action: action,
isPreview: isPreview,
isTerminal: isInteractive,
opts: opts,
renderer: renderer,
stack: stack,
proj: proj,
eventUrnToResourceRow: make(map[resource.URN]ResourceRow),
suffixColumn: int(statusColumn),
suffixesArray: []string{"", ".", "..", "..."},
urnToID: make(map[resource.URN]string),
displayOrderCounter: 1,
opStopwatch: newOpStopwatch(),
}

ticker := time.NewTicker(1 * time.Second)
Expand All @@ -283,27 +277,7 @@ func ShowProgressEvents(op string, action apitype.UpdateKind, stack tokens.Name,
}

func (display *ProgressDisplay) println(line string) {
display.renderer.println(display, display.opts.Color.Colorize(line))
}

func (display *ProgressDisplay) uncolorizeString(v string) string {
uncolorized, has := display.colorizedToUncolorized[v]
if !has {
uncolorized = colors.Never.Colorize(v)
display.colorizedToUncolorized[v] = uncolorized
}

return uncolorized
}

func (display *ProgressDisplay) uncolorizeColumns(columns []string) []string {
uncolorizedColumns := make([]string, len(columns))

for i, v := range columns {
uncolorizedColumns[i] = display.uncolorizeString(v)
}

return uncolorizedColumns
display.renderer.println(display, line)
}

type treeNode struct {
Expand Down Expand Up @@ -410,10 +384,9 @@ func (display *ProgressDisplay) convertNodesToRows(
}

colorizedColumns := make([]string, len(node.colorizedColumns))
uncolorisedColumns := display.uncolorizeColumns(node.colorizedColumns)

for i, colorizedColumn := range node.colorizedColumns {
columnWidth := utf8.RuneCountInString(uncolorisedColumns[i])
columnWidth := colors.MeasureColorizedString(colorizedColumn)

if i == display.suffixColumn {
columnWidth += maxSuffixLength
Expand Down
25 changes: 12 additions & 13 deletions pkg/backend/display/tableutil.go
Expand Up @@ -16,7 +16,6 @@ package display

import (
"strings"
"unicode/utf8"

"github.com/pulumi/pulumi/sdk/v3/go/common/diag/colors"
"github.com/pulumi/pulumi/sdk/v3/go/common/util/contract"
Expand All @@ -26,9 +25,9 @@ func columnHeader(msg string) string {
return colors.Underline + colors.BrightBlue + msg + colors.Reset
}

func messagePadding(message string, maxLength, extraPadding int) string {
extraWhitespace := maxLength - utf8.RuneCountInString(message)
contract.Assertf(extraWhitespace >= 0, "Neg whitespace. %v %s", maxLength, message)
func messagePadding(message string, maxWidth, extraPadding int) string {
extraWhitespace := maxWidth - colors.MeasureColorizedString(message)
contract.Assertf(extraWhitespace >= 0, "Neg whitespace. %v %s", maxWidth, message)

// Place two spaces between all columns (except after the first column). The first
// column already has a ": " so it doesn't need the extra space.
Expand All @@ -38,12 +37,12 @@ func messagePadding(message string, maxLength, extraPadding int) string {
}

// Gets the padding necessary to prepend to a column in order to keep it aligned in the terminal.
func columnPadding(uncolorizedColumns []string, columnIndex int, maxColumnLengths []int) string {
func columnPadding(columns []string, columnIndex int, maxColumnWidths []int) string {
extraWhitespace := " "
if columnIndex >= 0 && len(maxColumnLengths) > 0 {
column := uncolorizedColumns[columnIndex]
maxLength := maxColumnLengths[columnIndex]
extraWhitespace = messagePadding(column, maxLength, 2)
if columnIndex >= 0 && len(maxColumnWidths) > 0 {
column := columns[columnIndex]
maxWidth := maxColumnWidths[columnIndex]
extraWhitespace = messagePadding(column, maxWidth, 2)
}
return extraWhitespace
}
Expand All @@ -52,11 +51,11 @@ func columnPadding(uncolorizedColumns []string, columnIndex int, maxColumnLength
// status, then some amount of optional padding, then some amount of msgWithColors, then the
// suffix. Importantly, if there isn't enough room to display all of that on the terminal, then
// the msg will be truncated to try to make it fit.
func renderRow(colorizedColumns, uncolorizedColumns []string, maxColumnLengths []int) string {
func renderRow(columns []string, maxColumnWidths []int) string {
var row strings.Builder
for i := 0; i < len(colorizedColumns); i++ {
row.WriteString(columnPadding(uncolorizedColumns, i-1, maxColumnLengths))
row.WriteString(colorizedColumns[i])
for i := 0; i < len(columns); i++ {
row.WriteString(columnPadding(columns, i-1, maxColumnWidths))
row.WriteString(columns[i])
}
return row.String()
}

0 comments on commit 221795e

Please sign in to comment.