|  | /* | 
|  | * Licensed to the Apache Software Foundation (ASF) under one | 
|  | * or more contributor license agreements.  See the NOTICE file | 
|  | * distributed with this work for additional information | 
|  | * regarding copyright ownership.  The ASF licenses this file | 
|  | * to you under the Apache License, Version 2.0 (the | 
|  | * "License"); you may not use this file except in compliance | 
|  | * with the License.  You may obtain a copy of the License at | 
|  | * | 
|  | *   https://www.apache.org/licenses/LICENSE-2.0 | 
|  | * | 
|  | * Unless required by applicable law or agreed to in writing, | 
|  | * software distributed under the License is distributed on an | 
|  | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | 
|  | * KIND, either express or implied.  See the License for the | 
|  | * specific language governing permissions and limitations | 
|  | * under the License. | 
|  | */ | 
|  |  | 
|  | package utils | 
|  |  | 
|  | import ( | 
|  | "fmt" | 
|  | "math" | 
|  | "regexp" | 
|  | "strings" | 
|  |  | 
|  | "github.com/fatih/color" | 
|  | "github.com/rs/zerolog" | 
|  | ) | 
|  |  | 
|  | var AsciiBoxLogger zerolog.Logger | 
|  |  | 
|  | // AsciiBox is a string surrounded by an ascii border (and an optional name) | 
|  | type AsciiBox struct { | 
|  | data             string | 
|  | asciiBoxWriter   *asciiBoxWriter | 
|  | compressedBoxSet string | 
|  | } | 
|  |  | 
|  | func WithAsciiBoxName(name string) func(*BoxOptions) { | 
|  | return func(opts *BoxOptions) { | 
|  | opts.Name = name | 
|  | } | 
|  | } | 
|  |  | 
|  | func WithAsciiBoxHeader(header string) func(*BoxOptions) { | 
|  | return func(box *BoxOptions) { | 
|  | box.Header = header | 
|  | } | 
|  | } | 
|  |  | 
|  | func WithAsciiBoxFooter(footer string) func(*BoxOptions) { | 
|  | return func(box *BoxOptions) { | 
|  | box.Footer = footer | 
|  | } | 
|  | } | 
|  |  | 
|  | func WithAsciiBoxCharWidth(charWidth int) func(*BoxOptions) { | 
|  | return func(opts *BoxOptions) { | 
|  | opts.CharWidth = charWidth | 
|  | } | 
|  | } | 
|  |  | 
|  | func WithAsciiBoxOptions(boxOptions BoxOptions) func(*BoxOptions) { | 
|  | return func(opts *BoxOptions) { | 
|  | *opts = boxOptions | 
|  | } | 
|  | } | 
|  |  | 
|  | func WithAsciiBoxBoxSet(boxSet BoxSet) func(*BoxOptions) { | 
|  | return func(opts *BoxOptions) { | 
|  | opts.BoxSet = boxSet | 
|  | } | 
|  | } | 
|  |  | 
|  | type BoxOptions struct { | 
|  | // The name of the box | 
|  | Name string | 
|  | // The additional header of the box appearing on the right upper side | 
|  | Header string | 
|  | // The additional footer of the box appearing on the right lower side | 
|  | Footer string | 
|  | // The desired CharWidth | 
|  | CharWidth int | 
|  | // The BoxSet used to print this box | 
|  | BoxSet BoxSet | 
|  | } | 
|  |  | 
|  | type BoxSet struct { | 
|  | UpperLeftCorner  string | 
|  | UpperRightCorner string | 
|  | HorizontalLine   string | 
|  | VerticalLine     string | 
|  | LowerLeftCorner  string | 
|  | LowerRightCorner string | 
|  | } | 
|  |  | 
|  | func DefaultBoxSet() BoxSet { | 
|  | return BoxSet{ | 
|  | "╔", | 
|  | "╗", | 
|  | "═", | 
|  | "║", | 
|  | "╚", | 
|  | "╝", | 
|  | } | 
|  | } | 
|  |  | 
|  | func DefaultLightBoxSet() BoxSet { | 
|  | return BoxSet{ | 
|  | "╭", | 
|  | "╮", | 
|  | "┄", | 
|  | "┆", | 
|  | "╰", | 
|  | "╯", | 
|  | } | 
|  | } | 
|  |  | 
|  | // DebugAsciiBox set to true to get debug messages | 
|  | var DebugAsciiBox bool | 
|  |  | 
|  | // ANSI_PATTERN source: https://github.com/chalk/ansi-regex/blob/main/index.js#L3 | 
|  | var ANSI_PATTERN = regexp.MustCompile("[\u001b\u009b][\\[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]") | 
|  |  | 
|  | // AsciiBoxer is used to render something in a box | 
|  | type AsciiBoxer interface { | 
|  | // Box with options | 
|  | Box(...func(*BoxOptions)) AsciiBox | 
|  | } | 
|  |  | 
|  | var AsciiBoxWriterDefault = NewAsciiBoxWriter() | 
|  |  | 
|  | var AsciiBoxWriterLight = NewAsciiBoxWriter(WithAsciiBoxWriterDefaultBoxSet(DefaultLightBoxSet())) | 
|  |  | 
|  | type AsciiBoxWriter interface { | 
|  | BoxBox(box AsciiBox, options ...func(*BoxOptions)) AsciiBox | 
|  | BoxString(data string, options ...func(*BoxOptions)) AsciiBox | 
|  | AlignBoxes(asciiBoxes []AsciiBox, desiredWith int, options ...func(*BoxOptions)) AsciiBox | 
|  | BoxSideBySide(box1 AsciiBox, box2 AsciiBox, options ...func(*BoxOptions)) AsciiBox | 
|  | BoxBelowBox(box1 AsciiBox, box2 AsciiBox, options ...func(*BoxOptions)) AsciiBox | 
|  | } | 
|  |  | 
|  | func NewAsciiBoxWriter(opts ...func(writer *asciiBoxWriter)) AsciiBoxWriter { | 
|  | return newAsciiBoxWriter(opts...) | 
|  | } | 
|  |  | 
|  | func WithAsciiBoxWriterDefaultBoxSet(boxSet BoxSet) func(*asciiBoxWriter) { | 
|  | return func(a *asciiBoxWriter) { | 
|  | a.defaultBoxSet = boxSet | 
|  | } | 
|  | } | 
|  |  | 
|  | func WithAsciiBoxWriterDefaultColoredBoxes(nameColor, headerColor, footerColor *color.Color) func(*asciiBoxWriter) { | 
|  | return func(a *asciiBoxWriter) { | 
|  | if nameColor != nil { | 
|  | a.namePrinter = nameColor.Sprint | 
|  | } else { | 
|  | a.namePrinter = fmt.Sprint | 
|  | } | 
|  | if headerColor != nil { | 
|  | a.headerPrinter = headerColor.Sprint | 
|  | } else { | 
|  | a.headerPrinter = fmt.Sprint | 
|  | } | 
|  | if footerColor != nil { | 
|  | a.footerPrinter = footerColor.Sprint | 
|  | } else { | 
|  | a.footerPrinter = fmt.Sprint | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | func WithAsciiBoxWriterDisableColoredBoxes() func(*asciiBoxWriter) { | 
|  | return func(a *asciiBoxWriter) { | 
|  | a.namePrinter = fmt.Sprint | 
|  | a.headerPrinter = fmt.Sprint | 
|  | a.footerPrinter = fmt.Sprint | 
|  | } | 
|  | } | 
|  |  | 
|  | /////////////////////////////////////// | 
|  | /////////////////////////////////////// | 
|  | // | 
|  | // Internal section | 
|  | // | 
|  |  | 
|  | func (b BoxSet) compressBoxSet() string { | 
|  | return b.UpperLeftCorner + b.UpperRightCorner + b.HorizontalLine + b.VerticalLine + b.LowerLeftCorner + b.LowerRightCorner | 
|  | } | 
|  |  | 
|  | func (b BoxSet) contributeToCompressedBoxSet(box AsciiBox) string { | 
|  | actualSet := b.compressBoxSet() | 
|  | if box.compressedBoxSet == "" { | 
|  | // they have nothing to contribute | 
|  | return actualSet | 
|  | } | 
|  | if actualSet == "" { | 
|  | // I have nothing to contribute | 
|  | return box.compressedBoxSet | 
|  | } | 
|  | if strings.Contains(box.compressedBoxSet, actualSet) { | 
|  | // we have nothing to add | 
|  | return box.compressedBoxSet | 
|  | } | 
|  | return box.compressedBoxSet + "," + actualSet | 
|  | } | 
|  |  | 
|  | func combineCompressedBoxSets(box1, box2 AsciiBox) string { | 
|  | allSets := make(map[string]any) | 
|  | for _, s := range strings.Split(box1.compressedBoxSet, ",") { | 
|  | allSets[s] = true | 
|  | } | 
|  | for _, s := range strings.Split(box2.compressedBoxSet, ",") { | 
|  | allSets[s] = true | 
|  | } | 
|  | var foundSets []string | 
|  | for set := range allSets { | 
|  | foundSets = append(foundSets, set) | 
|  | } | 
|  | return strings.Join(foundSets, ",") | 
|  | } | 
|  |  | 
|  | type asciiBoxWriter struct { | 
|  | newLine      rune | 
|  | emptyPadding string | 
|  | // the name gets prefixed with an extra symbol for indent | 
|  | extraNameCharIndent int | 
|  | borderWidth         int | 
|  | newLineCharWidth    int | 
|  | defaultBoxSet       BoxSet | 
|  | boxHeaderRegex      *regexp.Regexp | 
|  | boxFooterRegex      *regexp.Regexp | 
|  | namePrinter         func(a ...any) string | 
|  | headerPrinter       func(a ...any) string | 
|  | footerPrinter       func(a ...any) string | 
|  | } | 
|  |  | 
|  | var _ AsciiBoxWriter = (*asciiBoxWriter)(nil) | 
|  |  | 
|  | func newAsciiBoxWriter(opts ...func(writer *asciiBoxWriter)) *asciiBoxWriter { | 
|  | a := &asciiBoxWriter{ | 
|  | newLine:      '\n', | 
|  | emptyPadding: " ", | 
|  | // the name gets prefixed with an extra symbol for indent | 
|  | extraNameCharIndent: 1, | 
|  | borderWidth:         1, | 
|  | newLineCharWidth:    1, | 
|  | defaultBoxSet:       DefaultBoxSet(), | 
|  | namePrinter:         color.New(color.FgGreen, color.Bold).Sprint, | 
|  | headerPrinter:       color.New(color.FgBlue).Sprint, | 
|  | footerPrinter:       color.New(color.FgRed, color.Italic).Sprint, | 
|  | } | 
|  | for _, opt := range opts { | 
|  | opt(a) | 
|  | } | 
|  | hl := a.defaultBoxSet.HorizontalLine | 
|  | a.boxHeaderRegex = regexp.MustCompile(`^` + a.defaultBoxSet.UpperLeftCorner + hl + `(?P<name>[^` + hl + `]+)` + hl + `*` + `(?P<header>[^` + hl + `]+)?` + hl + `*` + a.defaultBoxSet.UpperRightCorner) | 
|  | a.boxFooterRegex = regexp.MustCompile(`(?m)^` + a.defaultBoxSet.LowerLeftCorner + hl + `*` + `(?P<footer>[^` + hl + `]+)` + hl + `*` + a.defaultBoxSet.LowerRightCorner) | 
|  | return a | 
|  | } | 
|  |  | 
|  | func (a *asciiBoxWriter) boxString(data string, options ...func(*BoxOptions)) AsciiBox { | 
|  | var opts BoxOptions | 
|  | opts.BoxSet = a.defaultBoxSet | 
|  | for _, opt := range options { | 
|  | opt(&opts) | 
|  | } | 
|  | name := opts.Name | 
|  | nameLength := countChars(name) | 
|  | if name != "" { | 
|  | name = a.namePrinter(name) | 
|  | } | 
|  |  | 
|  | header := opts.Header | 
|  | if name != "" && header != "" { | 
|  | header = opts.BoxSet.HorizontalLine + opts.BoxSet.HorizontalLine + a.headerPrinter(header) + opts.BoxSet.HorizontalLine // Lazy manipulation to trick calculation below (adds a spacing between name and header) | 
|  | } | 
|  | headerLength := countChars(header) | 
|  |  | 
|  | footer := opts.Footer | 
|  | if footer != "" { | 
|  | footer = a.footerPrinter(footer) + opts.BoxSet.HorizontalLine | 
|  | } | 
|  | footerLength := countChars(footer) | 
|  |  | 
|  | charWidth := opts.CharWidth | 
|  |  | 
|  | data = strings.ReplaceAll(data, "\r\n", "\n") // carriage return just messes with boxes | 
|  | data = strings.ReplaceAll(data, "\t", "  ")   // Tabs just don't work well as they distort the boxes so we convert them to a double space | 
|  |  | 
|  | rawBox := AsciiBox{data, a, opts.BoxSet.compressBoxSet()} | 
|  | longestLine := rawBox.Width() | 
|  | footerAddOn := 0 | 
|  | if footer != "" { | 
|  | footerAddOn = footerLength + 2 | 
|  | } | 
|  | longestLine = max(longestLine, footerAddOn) | 
|  | if charWidth < longestLine { | 
|  | if DebugAsciiBox { | 
|  | AsciiBoxLogger.Debug().Int("nChars", longestLine-charWidth).Msg("Overflow by nChars chars") | 
|  | } | 
|  | charWidth = longestLine + a.borderWidth + a.borderWidth | 
|  | } | 
|  | var boxedString strings.Builder | 
|  | boxedString.Grow((a.borderWidth + longestLine + a.borderWidth + a.newLineCharWidth) * rawBox.Height()) | 
|  | namePadding := int(math.Max(float64(charWidth-nameLength-a.borderWidth-a.extraNameCharIndent-a.borderWidth-headerLength), 0)) | 
|  | boxedString.WriteString(opts.BoxSet.UpperLeftCorner + opts.BoxSet.HorizontalLine + name + strings.Repeat(opts.BoxSet.HorizontalLine, namePadding) + header + opts.BoxSet.UpperRightCorner) | 
|  | boxedString.WriteRune(a.newLine) | 
|  | // Name of the header stretches the box so we align to that | 
|  | charWidth = a.borderWidth + a.extraNameCharIndent + nameLength + namePadding + headerLength + a.borderWidth | 
|  | for _, line := range rawBox.Lines() { | 
|  | linePadding := float64(charWidth - boxLineOverheat - countChars(line)) | 
|  | if linePadding < 0 { | 
|  | linePadding = 0 | 
|  | } | 
|  | // TODO: this distorts boxes... | 
|  | frontPadding := math.Floor(linePadding / 2.0) | 
|  | backPadding := math.Ceil(linePadding / 2.0) | 
|  | boxedString.WriteString(opts.BoxSet.VerticalLine + strings.Repeat(a.emptyPadding, int(frontPadding)) + line + strings.Repeat(a.emptyPadding, int(backPadding)) + opts.BoxSet.VerticalLine) | 
|  | boxedString.WriteRune(a.newLine) | 
|  | } | 
|  | bottomPadding := namePadding + nameLength + a.extraNameCharIndent + headerLength - footerLength | 
|  | boxedString.WriteString(opts.BoxSet.LowerLeftCorner + strings.Repeat(opts.BoxSet.HorizontalLine, bottomPadding) + footer + opts.BoxSet.LowerRightCorner) | 
|  | return AsciiBox{boxedString.String(), a, opts.BoxSet.compressBoxSet()} | 
|  | } | 
|  |  | 
|  | func (a *asciiBoxWriter) getBoxName(box AsciiBox) string { | 
|  | subMatch := a.boxHeaderRegex.FindStringSubmatch(box.String()) | 
|  | if subMatch == nil { | 
|  | return "" | 
|  | } | 
|  | index := a.boxHeaderRegex.SubexpIndex("name") | 
|  | if index < 0 { | 
|  | return "" | 
|  | } | 
|  | return cleanString(subMatch[index]) | 
|  | } | 
|  |  | 
|  | func (a *asciiBoxWriter) getBoxHeader(box AsciiBox) string { | 
|  | subMatch := a.boxHeaderRegex.FindStringSubmatch(box.String()) | 
|  | if subMatch == nil { | 
|  | return "" | 
|  | } | 
|  | index := a.boxHeaderRegex.SubexpIndex("header") | 
|  | if index < 0 { | 
|  | return "" | 
|  | } | 
|  | return cleanString(subMatch[index]) | 
|  | } | 
|  |  | 
|  | func (a *asciiBoxWriter) getBoxFooter(box AsciiBox) string { | 
|  | subMatch := a.boxFooterRegex.FindStringSubmatch(box.String()) | 
|  | if subMatch == nil { | 
|  | return "" | 
|  | } | 
|  | index := a.boxFooterRegex.SubexpIndex("footer") | 
|  | if index < 0 { | 
|  | return "" | 
|  | } | 
|  | return cleanString(subMatch[index]) | 
|  | } | 
|  |  | 
|  | func (a *asciiBoxWriter) changeBoxName(box AsciiBox, newName string) AsciiBox { | 
|  | return a.changeBoxAttributes(box, &newName, nil, nil) | 
|  | } | 
|  |  | 
|  | func (a *asciiBoxWriter) changeBoxHeader(box AsciiBox, newHeader string) AsciiBox { | 
|  | return a.changeBoxAttributes(box, nil, &newHeader, nil) | 
|  |  | 
|  | } | 
|  |  | 
|  | func (a *asciiBoxWriter) changeBoxFooter(box AsciiBox, newFooter string) AsciiBox { | 
|  | return a.changeBoxAttributes(box, nil, nil, &newFooter) | 
|  | } | 
|  |  | 
|  | func (a *asciiBoxWriter) changeBoxAttributes(box AsciiBox, newName, newHeader, newFooter *string) AsciiBox { | 
|  | // Current data | 
|  | name := box.asciiBoxWriter.getBoxName(box) | 
|  | header := box.asciiBoxWriter.getBoxHeader(box) | 
|  | footer := box.asciiBoxWriter.getBoxFooter(box) | 
|  | // set new metadata | 
|  | if newName != nil { | 
|  | name = *newName | 
|  | } | 
|  | if newHeader != nil { | 
|  | header = *newHeader | 
|  | } | 
|  | if newFooter != nil { | 
|  | footer = *newFooter | 
|  | } | 
|  | var newOptions = []func(options *BoxOptions){ | 
|  | WithAsciiBoxName(name), | 
|  | WithAsciiBoxHeader(header), | 
|  | WithAsciiBoxFooter(footer), | 
|  | } | 
|  |  | 
|  | if !a.hasBorders(box) { // this means that this is a naked box. | 
|  | return a.boxString(box.String(), newOptions...) | 
|  | } | 
|  | minimumWidth := countChars(a.defaultBoxSet.UpperLeftCorner + a.defaultBoxSet.HorizontalLine + name + a.defaultBoxSet.UpperRightCorner) | 
|  | if header != "" { // if we have a header we need to extend that minimum width to make space for the header | 
|  | minimumWidth += countChars(a.defaultBoxSet.HorizontalLine + header) | 
|  | } | 
|  | boxContent := a.unwrap(box)                            // get the content itself ... | 
|  | rawWidth := boxContent.Width()                         // ... and look at the width. | 
|  | minimumWidth = max(minimumWidth, rawWidth+2)           // check that we have enough space for the content. | 
|  | minimumWidth = max(minimumWidth, countChars(footer)+2) // check that we have enough space for the footer. | 
|  | newBox := a.BoxString( | 
|  | boxContent.String(), | 
|  | append(newOptions, WithAsciiBoxCharWidth(minimumWidth))..., | 
|  | ) | 
|  | newBox.compressedBoxSet = a.defaultBoxSet.contributeToCompressedBoxSet(box) | 
|  | return newBox | 
|  | } | 
|  |  | 
|  | func (a *asciiBoxWriter) mergeHorizontal(boxes []AsciiBox) AsciiBox { | 
|  | switch len(boxes) { | 
|  | case 0: | 
|  | return AsciiBox{"", a, a.defaultBoxSet.compressBoxSet()} | 
|  | case 1: | 
|  | return boxes[0] | 
|  | case 2: | 
|  | return a.BoxSideBySide(boxes[0], boxes[1]) | 
|  | default: | 
|  | return a.BoxSideBySide(boxes[0], a.mergeHorizontal(boxes[1:])) | 
|  | } | 
|  | } | 
|  |  | 
|  | func (a *asciiBoxWriter) expandBox(box AsciiBox, desiredWidth int) AsciiBox { | 
|  | if box.Width() >= desiredWidth { | 
|  | return box | 
|  | } | 
|  | boxLines := box.Lines() | 
|  | numberOfLine := len(boxLines) | 
|  | boxWidth := box.Width() | 
|  | padding := strings.Repeat(" ", desiredWidth-boxWidth) | 
|  | var newBox strings.Builder | 
|  | newBox.Grow((boxWidth + a.newLineCharWidth) * numberOfLine) | 
|  | for i, line := range boxLines { | 
|  | newBox.WriteString(line) | 
|  | newBox.WriteString(padding) | 
|  | if i < numberOfLine-1 { | 
|  | newBox.WriteRune(a.newLine) | 
|  | } | 
|  | } | 
|  | return AsciiBox{newBox.String(), a, a.defaultBoxSet.contributeToCompressedBoxSet(box)} | 
|  | } | 
|  |  | 
|  | func (a *asciiBoxWriter) unwrap(box AsciiBox) AsciiBox { | 
|  | if !a.hasBorders(box) { | 
|  | return box | 
|  | } | 
|  | originalLines := box.Lines() | 
|  | newLines := make([]string, len(originalLines)-2) | 
|  | completeBoxSet := a.defaultBoxSet.contributeToCompressedBoxSet(box) | 
|  | for i, line := range originalLines { | 
|  | if i == 0 { | 
|  | // we ignore the first line | 
|  | continue | 
|  | } | 
|  | if i == len(originalLines)-1 { | 
|  | // we ignore the last line | 
|  | break | 
|  | } | 
|  | runes := []rune(line) | 
|  | // Strip the vertical Lines and trim the padding | 
|  | unwrappedLine := string(runes[1 : len(runes)-1]) | 
|  | if !strings.ContainsAny(unwrappedLine, strings.ReplaceAll(completeBoxSet, ",", "")) { | 
|  | // only trim boxes witch don't contain other boxes | 
|  | unwrappedLine = strings.Trim(unwrappedLine, a.emptyPadding) | 
|  | } | 
|  | newLines[i-1] = unwrappedLine | 
|  | } | 
|  | return AsciiBox{strings.Join(newLines, string(a.newLine)), a, completeBoxSet} | 
|  | } | 
|  |  | 
|  | func (a *asciiBoxWriter) hasBorders(box AsciiBox) bool { | 
|  | if len(box.String()) == 0 { | 
|  | return false | 
|  | } | 
|  | // Check if the first char is the upper left corner | 
|  | return []rune(box.String())[0] == []rune(a.defaultBoxSet.UpperLeftCorner)[0] | 
|  | } | 
|  |  | 
|  | func countChars(s string) int { | 
|  | return len([]rune(ANSI_PATTERN.ReplaceAllString(s, ""))) | 
|  | } | 
|  |  | 
|  | // cleanString returns the strings minus the control sequences | 
|  | func cleanString(s string) string { | 
|  | regex, _ := regexp.Compile(`\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])`) | 
|  | return regex.ReplaceAllString(s, "") | 
|  | } | 
|  |  | 
|  | // | 
|  | // Internal section | 
|  | // | 
|  | /////////////////////////////////////// | 
|  | /////////////////////////////////////// | 
|  |  | 
|  | // Width returns the width of the box without the newlines | 
|  | func (m AsciiBox) Width() int { | 
|  | maxWidth := 0 | 
|  | for _, line := range m.Lines() { | 
|  | currentLength := countChars(line) | 
|  | if maxWidth < currentLength { | 
|  | maxWidth = currentLength | 
|  | } | 
|  | } | 
|  | return maxWidth | 
|  | } | 
|  |  | 
|  | // Height returns the height of the box without | 
|  | func (m AsciiBox) Height() int { | 
|  | return len(m.Lines()) | 
|  | } | 
|  |  | 
|  | // Lines returns the lines of the box | 
|  | func (m AsciiBox) Lines() []string { | 
|  | return strings.Split(m.data, "\n") | 
|  | } | 
|  |  | 
|  | func (m AsciiBox) GetBoxName() string { | 
|  | return m.asciiBoxWriter.getBoxName(m) | 
|  | } | 
|  |  | 
|  | func (m AsciiBox) ChangeBoxName(newName string) AsciiBox { | 
|  | return m.asciiBoxWriter.changeBoxName(m, newName) | 
|  | } | 
|  |  | 
|  | func (m AsciiBox) ChangeBoxHeader(newHeader string) AsciiBox { | 
|  | return m.asciiBoxWriter.changeBoxHeader(m, newHeader) | 
|  | } | 
|  |  | 
|  | func (m AsciiBox) ChangeBoxFooter(newFooter string) AsciiBox { | 
|  | return m.asciiBoxWriter.changeBoxFooter(m, newFooter) | 
|  | } | 
|  |  | 
|  | func (m AsciiBox) IsEmpty() bool { | 
|  | if m.asciiBoxWriter.hasBorders(m) { | 
|  | return m.asciiBoxWriter.unwrap(m).String() == "" | 
|  | } | 
|  | return m.String() == "" | 
|  | } | 
|  |  | 
|  | // String returns the string of the box | 
|  | func (m AsciiBox) String() string { | 
|  | return m.data | 
|  | } | 
|  |  | 
|  | // BoxBox boxes a box | 
|  | func (a *asciiBoxWriter) BoxBox(box AsciiBox, options ...func(*BoxOptions)) AsciiBox { | 
|  | // TODO: if there is a box bigger then others in that this will get distorted | 
|  | newBox := a.BoxString(box.data, options...) | 
|  | newBox.compressedBoxSet = a.defaultBoxSet.contributeToCompressedBoxSet(box) | 
|  | return newBox | 
|  | } | 
|  |  | 
|  | // BoxString boxes a newline separated string into a beautiful box | 
|  | func (a *asciiBoxWriter) BoxString(data string, options ...func(*BoxOptions)) AsciiBox { | 
|  | return a.boxString(data, options...) | 
|  | } | 
|  |  | 
|  | // AlignBoxes aligns all boxes to a desiredWidth and orders them from left to right and top to bottom (size will be at min the size of the biggest box) | 
|  | func (a *asciiBoxWriter) AlignBoxes(boxes []AsciiBox, desiredWidth int, options ...func(*BoxOptions)) AsciiBox { | 
|  | if len(boxes) == 0 { | 
|  | return AsciiBox{"", a, a.defaultBoxSet.compressBoxSet()} | 
|  | } | 
|  | actualWidth := desiredWidth | 
|  | for _, box := range boxes { | 
|  | boxWidth := box.Width() | 
|  | if boxWidth > actualWidth { | 
|  | if DebugAsciiBox { | 
|  | AsciiBoxLogger.Debug().Int("nChars", boxWidth-desiredWidth).Msg("Overflow by nChars chars") | 
|  | } | 
|  | actualWidth = boxWidth | 
|  | } | 
|  | } | 
|  | if DebugAsciiBox { | 
|  | AsciiBoxLogger.Debug().Int("actualWidth", actualWidth).Msg("Working with actualWidth chars") | 
|  | } | 
|  | bigBox := AsciiBox{"", a, a.defaultBoxSet.compressBoxSet()} | 
|  | currentBoxRow := make([]AsciiBox, 0) | 
|  | currentRowLength := 0 | 
|  | for _, box := range boxes { | 
|  | currentRowLength += box.Width() | 
|  | if currentRowLength > actualWidth { | 
|  | mergedBoxes := a.mergeHorizontal(currentBoxRow) | 
|  | if bigBox.IsEmpty() { | 
|  | bigBox = mergedBoxes | 
|  | } else { | 
|  | bigBox = a.BoxBelowBox(bigBox, mergedBoxes) | 
|  | } | 
|  | currentRowLength = box.Width() | 
|  | currentBoxRow = make([]AsciiBox, 0) | 
|  | } | 
|  | currentBoxRow = append(currentBoxRow, box) | 
|  | } | 
|  | if len(currentBoxRow) > 0 { | 
|  | // Special case where all boxes fit into one row | 
|  | mergedBoxes := a.mergeHorizontal(currentBoxRow) | 
|  | if bigBox.IsEmpty() { | 
|  | bigBox = mergedBoxes | 
|  | } else { | 
|  | bigBox = a.BoxBelowBox(bigBox, mergedBoxes) | 
|  | } | 
|  | } | 
|  | return bigBox | 
|  | } | 
|  |  | 
|  | // BoxSideBySide renders two boxes side by side | 
|  | func (a *asciiBoxWriter) BoxSideBySide(box1, box2 AsciiBox, options ...func(*BoxOptions)) AsciiBox { | 
|  | const newLineCharWidth = 1 | 
|  | var aggregateBox strings.Builder | 
|  | box1Width := box1.Width() | 
|  | box1Lines := box1.Lines() | 
|  | box2Width := box2.Width() | 
|  | box2Lines := box2.Lines() | 
|  | maxRows := int(math.Max(float64(len(box1Lines)), float64(len(box2Lines)))) | 
|  | aggregateBox.Grow((box1Width + box2Width + newLineCharWidth) * maxRows) | 
|  | for row := 0; row < maxRows; row++ { | 
|  | ranOutOfLines := false | 
|  | if row >= len(box1Lines) { | 
|  | ranOutOfLines = true | 
|  | aggregateBox.WriteString(strings.Repeat(" ", box1Width)) | 
|  | } else { | 
|  | split1Row := box1Lines[row] | 
|  | padding := box1Width - countChars(split1Row) | 
|  | aggregateBox.WriteString(split1Row + strings.Repeat(" ", padding)) | 
|  | } | 
|  | if row >= len(box2Lines) { | 
|  | if ranOutOfLines { | 
|  | break | 
|  | } | 
|  | aggregateBox.WriteString(strings.Repeat(" ", box2Width)) | 
|  | } else { | 
|  | split2Row := box2Lines[row] | 
|  | padding := box2Width - countChars(split2Row) | 
|  | aggregateBox.WriteString(split2Row + strings.Repeat(" ", padding)) | 
|  | } | 
|  | if row < maxRows-1 { | 
|  | // Only write newline if we are not the last line | 
|  | aggregateBox.WriteRune('\n') | 
|  | } | 
|  | } | 
|  | return AsciiBox{aggregateBox.String(), a, combineCompressedBoxSets(box1, box2)} | 
|  | } | 
|  |  | 
|  | // BoxBelowBox renders two boxes below | 
|  | func (a *asciiBoxWriter) BoxBelowBox(box1, box2 AsciiBox, options ...func(*BoxOptions)) AsciiBox { | 
|  | box1Width := box1.Width() | 
|  | box2Width := box2.Width() | 
|  | if box1Width < box2Width { | 
|  | box1 = a.expandBox(box1, box2Width) | 
|  | } else if box2Width < box1Width { | 
|  | box2 = a.expandBox(box2, box1Width) | 
|  | } | 
|  | return AsciiBox{box1.String() + "\n" + box2.String(), a, combineCompressedBoxSets(box1, box2)} | 
|  | } |