Last active
November 3, 2024 22:29
-
-
Save Broderick-Westrope/b89b14770c09dda928c4a108f437b927 to your computer and use it in GitHub Desktop.
Golang Lipgloss Window Overlay
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
module test | |
go 1.23.0 | |
require ( | |
github.com/MakeNowJust/heredoc v1.0.0 | |
github.com/charmbracelet/lipgloss v1.0.0 | |
github.com/charmbracelet/x/ansi v0.4.2 | |
github.com/stretchr/testify v1.9.0 | |
) | |
require ( | |
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect | |
github.com/davecgh/go-spew v1.1.1 // indirect | |
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect | |
github.com/mattn/go-isatty v0.0.20 // indirect | |
github.com/mattn/go-runewidth v0.0.15 // indirect | |
github.com/muesli/termenv v0.15.2 // indirect | |
github.com/pmezard/go-difflib v1.0.0 // indirect | |
github.com/rivo/uniseg v0.4.7 // indirect | |
golang.org/x/sys v0.26.0 // indirect | |
gopkg.in/yaml.v3 v3.0.1 // indirect | |
) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= | |
github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= | |
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= | |
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= | |
github.com/charmbracelet/lipgloss v1.0.0 h1:O7VkGDvqEdGi93X+DeqsQ7PKHDgtQfF8j8/O2qFMQNg= | |
github.com/charmbracelet/lipgloss v1.0.0/go.mod h1:U5fy9Z+C38obMs+T+tJqst9VGzlOYGj4ri9reL3qUlo= | |
github.com/charmbracelet/x/ansi v0.4.2 h1:0JM6Aj/g/KC154/gOP4vfxun0ff6itogDYk41kof+qk= | |
github.com/charmbracelet/x/ansi v0.4.2/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw= | |
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= | |
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= | |
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= | |
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= | |
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= | |
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= | |
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= | |
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= | |
github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo= | |
github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= | |
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= | |
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= | |
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= | |
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= | |
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= | |
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= | |
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= | |
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | |
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= | |
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= | |
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= | |
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= | |
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= | |
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package main | |
import ( | |
"fmt" | |
"regexp" | |
"strings" | |
"unicode" | |
"github.com/charmbracelet/lipgloss" | |
"github.com/charmbracelet/x/ansi" | |
"github.com/mattn/go-runewidth" | |
) | |
// At its core this allows overlaying a string on top of another. | |
// My use case is for building TUI applications using "github.com/charmbracelet/bubbletea" wherein I like to have modal windows presented on top of the main window. | |
// This code was derived from the following source, but has the following changes: | |
// - Wrapping is not done for you. Instead, wrapping of the background and overlay strings must be done beforehand. | |
// - A helper function is included for overlaying at the center of the background string. | |
// - A boolean `ignoreMarginWhitespace`. When false, margin whitespace in the overlay string will overwrite the background string. | |
// When true, margin whitespace in the overlay string will be ignored such that these cells of the background string are preserved. See the tests for examples. | |
// | |
// CREDIT: https://gist.github.com/ras0q/9bf5d81544b22302393f61206892e2cd | |
// OverlayCenter writes the overlay string onto the background string such that the middle of the | |
// overlay string will be at the middle of the overlay will be at the middle of the background. | |
func OverlayCenter(bg string, overlay string, ignoreMarginWhitespace bool) (string, error) { | |
row := lipgloss.Height(bg) / 2 | |
row -= lipgloss.Height(overlay) / 2 | |
col := lipgloss.Width(bg) / 2 | |
col -= lipgloss.Width(overlay) / 2 | |
return Overlay(bg, overlay, row, col, ignoreMarginWhitespace) | |
} | |
// Overlay writes the overlay string onto the background string at the specified row and column. | |
// In this case, the row and column are zero indexed. | |
func Overlay(bg, overlay string, row, col int, ignoreMarginWhitespace bool) (string, error) { | |
bgLines := strings.Split(bg, "\n") | |
overlayLines := strings.Split(overlay, "\n") | |
for i, overlayLine := range overlayLines { | |
targetRow := i + row | |
// Ensure the target row exists in the background lines | |
for len(bgLines) <= targetRow { | |
bgLines = append(bgLines, "") | |
} | |
bgLine := bgLines[targetRow] | |
bgLineWidth := ansi.StringWidth(bgLine) | |
if bgLineWidth < col { | |
bgLine += strings.Repeat(" ", col-bgLineWidth) // Add padding | |
} | |
// Handle ignoreMarginWhitespace | |
if ignoreMarginWhitespace { | |
// Process the overlay line to preserve leading and trailing whitespace | |
overlayLine = removeMarginWhitespace(bgLine, overlayLine, col) | |
} | |
bgLeft := ansi.Truncate(bgLine, col, "") | |
bgRight, err := truncateLeft(bgLine, col+ansi.StringWidth(overlayLine)) | |
if err != nil { | |
return "", fmt.Errorf("failed to truncate line: %w", err) | |
} | |
bgLines[targetRow] = bgLeft + overlayLine + bgRight | |
} | |
return strings.Join(bgLines, "\n"), nil | |
} | |
// removeMarginWhitespace preserves the background where the overlay line has leading or trailing whitespace. | |
// This is done by detecting those empty cells in the overlay string and replacing them with the corresponding background cells. | |
func removeMarginWhitespace(bgLine, overlayLine string, col int) string { | |
var result strings.Builder | |
// Variables to track ANSI escape sequences | |
inAnsi := false | |
ansiSeq := strings.Builder{} | |
// Strip ANSI codes to analyze whitespace | |
overlayStripped := ansi.Strip(overlayLine) | |
overlayRunes := []rune(overlayStripped) | |
// Find first and last non-whitespace positions | |
firstNonWhitespacePos := -1 | |
lastNonWhitespacePos := -1 | |
visualPos := 0 | |
overlayVisualWidths := make([]int, len(overlayRunes)) | |
for i, r := range overlayRunes { | |
runeWidth := runewidth.RuneWidth(r) | |
overlayVisualWidths[i] = runeWidth | |
if !unicode.IsSpace(r) { | |
if firstNonWhitespacePos == -1 { | |
firstNonWhitespacePos = visualPos | |
} | |
lastNonWhitespacePos = visualPos + runeWidth - 1 // inclusive | |
} | |
visualPos += runeWidth | |
} | |
// If all characters are whitespace | |
if firstNonWhitespacePos == -1 { | |
firstNonWhitespacePos = 0 | |
lastNonWhitespacePos = -1 | |
} | |
// Now, process the overlayLine, keeping track of visual positions | |
visualPos = 0 | |
runeReader := strings.NewReader(overlayLine) | |
for { | |
r, _, err := runeReader.ReadRune() | |
if err != nil { | |
break | |
} | |
if r == '\x1b' { | |
// Start of ANSI escape sequence | |
inAnsi = true | |
ansiSeq.WriteRune(r) | |
continue | |
} | |
if inAnsi { | |
ansiSeq.WriteRune(r) | |
if (r >= 'A' && r <= 'Z') || (r >= 'a' && r <= 'z') { | |
// End of ANSI escape sequence | |
inAnsi = false | |
result.WriteString(ansiSeq.String()) | |
ansiSeq.Reset() | |
} | |
continue | |
} | |
runeWidth := runewidth.RuneWidth(r) | |
// Determine if current position is leading whitespace or trailing whitespace | |
var isLeadingWhitespace, isTrailingWhitespace bool | |
if visualPos < firstNonWhitespacePos { | |
isLeadingWhitespace = true | |
} else if visualPos > lastNonWhitespacePos { | |
isTrailingWhitespace = true | |
} | |
if unicode.IsSpace(r) && (isLeadingWhitespace || isTrailingWhitespace) { | |
// Preserve background character | |
for k := 0; k < runeWidth; k++ { | |
bgChar := getBgCharAt(bgLine, col+visualPos+k) | |
result.WriteString(bgChar) | |
} | |
} else { | |
// Include character from overlay (could be a non-whitespace or whitespace character in between) | |
result.WriteRune(r) | |
} | |
visualPos += runeWidth | |
} | |
return result.String() | |
} | |
// getBgCharAt returns the character from the background line at the specified visual index. | |
func getBgCharAt(bgLine string, visualIndex int) string { | |
var result strings.Builder | |
displayWidth := 0 | |
inAnsi := false | |
ansiSeq := strings.Builder{} | |
runeReader := strings.NewReader(bgLine) | |
for { | |
r, _, err := runeReader.ReadRune() | |
if err != nil { | |
break | |
} | |
if r == '\x1b' { | |
// Start of ANSI escape sequence | |
inAnsi = true | |
ansiSeq.WriteRune(r) | |
continue | |
} | |
if inAnsi { | |
ansiSeq.WriteRune(r) | |
if (r >= 'A' && r <= 'Z') || (r >= 'a' && r <= 'z') { | |
// End of ANSI escape sequence | |
inAnsi = false | |
result.WriteString(ansiSeq.String()) | |
ansiSeq.Reset() | |
} | |
continue | |
} | |
charWidth := runewidth.RuneWidth(r) | |
if displayWidth+charWidth > visualIndex { | |
// We have reached the desired index | |
result.WriteRune(r) | |
break | |
} | |
displayWidth += charWidth | |
} | |
// If no character found at the position, return a space | |
if result.Len() == 0 { | |
return " " | |
} | |
return result.String() | |
} | |
// truncateLeft removes characters from the beginning of a line, considering ANSI escape codes. | |
func truncateLeft(line string, padding int) (string, error) { | |
if strings.Contains(line, "\n") { | |
return "", fmt.Errorf("line must not contain newline") | |
} | |
wrapped := strings.Split(ansi.Hardwrap(line, padding, true), "\n") | |
if len(wrapped) == 1 { | |
return "", nil | |
} | |
var ansiStyle string | |
// Regular expression to match ANSI escape codes. | |
ansiStyles := regexp.MustCompile(`\x1b[[\d;]*m`).FindAllString(wrapped[0], -1) | |
if len(ansiStyles) > 0 { | |
ansiStyle = ansiStyles[len(ansiStyles)-1] | |
} | |
return ansiStyle + strings.Join(wrapped[1:], ""), nil | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package main | |
import ( | |
"testing" | |
"github.com/MakeNowJust/heredoc" | |
"github.com/charmbracelet/lipgloss" | |
"github.com/stretchr/testify/assert" | |
"github.com/stretchr/testify/require" | |
) | |
func TestOverlayCenter(t *testing.T) { | |
tt := map[string]struct { | |
bg string | |
overlay string | |
ignoreMarginWhitespace bool | |
want string | |
}{ | |
"simple": { | |
bg: heredoc.Doc(` | |
Facere enim neque consectetur soluta tenetur ducimus omnis. Voluptatibus accusantium maiores quia eaque velit nesciunt hic saepe tenetur. | |
Amet quidem reprehenderit ex. Error illum sit est expedita sapiente neque. Laborum vero necessitatibus similique suscipit nam. | |
Tempore occaecati eligendi accusamus eos similique harum impedit. Quas nam molestiae architecto quam. | |
Accusamus pariatur facilis ea nostrum exercitationem quam. Sit ipsam aperiam aspernatur hic fugit officia inventore. | |
Reiciendis doloribus ut eius id. Repellendus eum enim. Reprehenderit veritatis nulla molestiae nulla veniam. | |
Nemo animi nisi blanditiis. Eligendi tempora laudantium assumenda nam. | |
`), | |
overlay: "*********\n*****", | |
ignoreMarginWhitespace: false, | |
want: heredoc.Doc(` | |
Facere enim neque consectetur soluta tenetur ducimus omnis. Voluptatibus accusantium maiores quia eaque velit nesciunt hic saepe tenetur. | |
Amet quidem reprehenderit ex. Error illum sit est expedita sapiente neque. Laborum vero necessitatibus similique suscipit nam. | |
Tempore occaecati eligendi accusamus eos similique harum impedit*********m molestiae architecto quam. | |
Accusamus pariatur facilis ea nostrum exercitationem quam. Sit i*****aperiam aspernatur hic fugit officia inventore. | |
Reiciendis doloribus ut eius id. Repellendus eum enim. Reprehenderit veritatis nulla molestiae nulla veniam. | |
Nemo animi nisi blanditiis. Eligendi tempora laudantium assumenda nam. | |
`), | |
}, | |
"padded; enforce margins": { | |
bg: heredoc.Doc(` | |
Facere enim neque consectetur soluta tenetur ducimus omnis. Voluptatibus accusantium maiores quia eaque velit nesciunt hic saepe tenetur. | |
Amet quidem reprehenderit ex. Error illum sit est expedita sapiente neque. Laborum vero necessitatibus similique suscipit nam. | |
Tempore occaecati eligendi accusamus eos similique harum impedit. Quas nam molestiae architecto quam. | |
Accusamus pariatur facilis ea nostrum exercitationem quam. Sit ipsam aperiam aspernatur hic fugit officia inventore. | |
Reiciendis doloribus ut eius id. Repellendus eum enim. Reprehenderit veritatis nulla molestiae nulla veniam. | |
Nemo animi nisi blanditiis. Eligendi tempora laudantium assumenda nam. | |
`), | |
overlay: lipgloss.NewStyle().Padding(1, 3).Render("*********\n*****"), | |
ignoreMarginWhitespace: false, | |
want: heredoc.Doc(` | |
Facere enim neque consectetur soluta tenetur ducimus omnis. Voluptatibus accusantium maiores quia eaque velit nesciunt hic saepe tenetur. | |
Amet quidem reprehenderit ex. Error illum sit est expedita sa aborum vero necessitatibus similique suscipit nam. | |
Tempore occaecati eligendi accusamus eos similique harum impe ********* olestiae architecto quam. | |
Accusamus pariatur facilis ea nostrum exercitationem quam. Si ***** aspernatur hic fugit officia inventore. | |
Reiciendis doloribus ut eius id. Repellendus eum enim. Repreh is nulla molestiae nulla veniam. | |
Nemo animi nisi blanditiis. Eligendi tempora laudantium assumenda nam. | |
`), | |
}, | |
"padded; ignore margins": { | |
bg: heredoc.Doc(` | |
Facere enim neque consectetur soluta tenetur ducimus omnis. Voluptatibus accusantium maiores quia eaque velit nesciunt hic saepe tenetur. | |
Amet quidem reprehenderit ex. Error illum sit est expedita sapiente neque. Laborum vero necessitatibus similique suscipit nam. | |
Tempore occaecati eligendi accusamus eos similique harum impedit. Quas nam molestiae architecto quam. | |
Accusamus pariatur facilis ea nostrum exercitationem quam. Sit ipsam aperiam aspernatur hic fugit officia inventore. | |
Reiciendis doloribus ut eius id. Repellendus eum enim. Reprehenderit veritatis nulla molestiae nulla veniam. | |
Nemo animi nisi blanditiis. Eligendi tempora laudantium assumenda nam. | |
`), | |
overlay: lipgloss.NewStyle().Padding(1, 3).Render("*********\n*****"), | |
ignoreMarginWhitespace: true, | |
want: heredoc.Doc(` | |
Facere enim neque consectetur soluta tenetur ducimus omnis. Voluptatibus accusantium maiores quia eaque velit nesciunt hic saepe tenetur. | |
Amet quidem reprehenderit ex. Error illum sit est expedita sapiente neque. Laborum vero necessitatibus similique suscipit nam. | |
Tempore occaecati eligendi accusamus eos similique harum impedit*********m molestiae architecto quam. | |
Accusamus pariatur facilis ea nostrum exercitationem quam. Sit i*****aperiam aspernatur hic fugit officia inventore. | |
Reiciendis doloribus ut eius id. Repellendus eum enim. Reprehenderit veritatis nulla molestiae nulla veniam. | |
Nemo animi nisi blanditiis. Eligendi tempora laudantium assumenda nam. | |
`), | |
}, | |
} | |
for name, tc := range tt { | |
t.Run(name, func(t *testing.T) { | |
result, err := OverlayCenter(tc.bg, tc.overlay, tc.ignoreMarginWhitespace) | |
require.NoError(t, err) | |
assert.Equal(t, tc.want, result) | |
}) | |
} | |
} | |
func TestOverlay(t *testing.T) { | |
tt := map[string]struct { | |
bg string | |
overlay string | |
row int | |
col int | |
ignoreMarginWhitespace bool | |
want string | |
}{ | |
"single line; start": { | |
bg: "Nostrum libero modi velit neque dolores.", | |
overlay: "*********", | |
row: 0, | |
col: 0, | |
ignoreMarginWhitespace: false, | |
want: "*********ibero modi velit neque dolores.", | |
}, | |
"single line; middle": { | |
bg: "Nostrum libero modi velit neque dolores.", | |
overlay: "*********", | |
row: 0, | |
col: 10, | |
ignoreMarginWhitespace: false, | |
want: "Nostrum li********* velit neque dolores.", | |
}, | |
"single line; beyond final column": { | |
bg: "Nostrum libero modi velit neque dolores.", | |
overlay: "*********", | |
row: 0, | |
col: 35, | |
ignoreMarginWhitespace: false, | |
want: "Nostrum libero modi velit neque dol*********", | |
}, | |
"single line; beyond final row": { | |
bg: "Nostrum libero modi velit neque dolores.", | |
overlay: "*********", | |
row: 3, | |
col: 0, | |
ignoreMarginWhitespace: false, | |
want: "Nostrum libero modi velit neque dolores.\n\n\n*********", | |
}, | |
"single line; lipgloss styled": { | |
bg: "Nostrum libero modi velit neque dolores.", | |
overlay: lipgloss.NewStyle().Underline(true).Foreground(lipgloss.Color("1")).Render("*****"), | |
row: 0, | |
col: 5, | |
ignoreMarginWhitespace: false, | |
want: "Nostr*****bero modi velit neque dolores.", | |
}, | |
"single line; manual escape code": { | |
bg: "Nostrum libero modi velit neque dolores.", | |
overlay: "\x1b[31m*****\x1b[0m", | |
row: 0, | |
col: 5, | |
ignoreMarginWhitespace: false, | |
want: "Nostr\u001B[31m*****\u001B[0mbero modi velit neque dolores.", | |
}, | |
"multi-line background; overlay middle line": { | |
bg: "Line 1\nLine 2\nLine 3\nLine 4\nLine 5", | |
overlay: "*****", | |
row: 2, | |
col: 0, | |
ignoreMarginWhitespace: false, | |
want: "Line 1\nLine 2\n*****3\nLine 4\nLine 5", | |
}, | |
"multi-line overlay; beyond background": { | |
bg: "Line 1\nLine 2\nLine 3\nLine 4\nLine 5", | |
overlay: "*******\n*******", | |
row: 1, | |
col: 5, | |
ignoreMarginWhitespace: false, | |
want: "Line 1\nLine *******\nLine *******\nLine 4\nLine 5", | |
}, | |
"multi-line overlay; enforce margins": { | |
bg: "Line 1\nLine 2\nLine 3\nLine 4\nLine 5", | |
overlay: lipgloss.NewStyle().PaddingLeft(2).PaddingTop(1).Render("***\n***"), | |
row: 0, | |
col: 0, | |
ignoreMarginWhitespace: false, | |
want: " 1\n ***2\n ***3\nLine 4\nLine 5", | |
}, | |
"multi-line overlay; ignore margins": { | |
bg: "Line 1\nLine 2\nLine 3\nLine 4\nLine 5", | |
overlay: lipgloss.NewStyle().PaddingLeft(2).PaddingTop(1).Render("***\n***"), | |
row: 0, | |
col: 0, | |
ignoreMarginWhitespace: true, | |
want: "Line 1\nLi***2\nLi***3\nLine 4\nLine 5", | |
}, | |
} | |
for name, tc := range tt { | |
t.Run(name, func(t *testing.T) { | |
result, err := Overlay(tc.bg, tc.overlay, tc.row, tc.col, tc.ignoreMarginWhitespace) | |
require.NoError(t, err) | |
assert.Equal(t, tc.want, result) | |
}) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment