src.elv.sh@v0.21.0-dev.0.20240515223629-06979efb9a2a/pkg/ui/text.go (about)

     1  package ui
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"math/big"
     7  	"strconv"
     8  	"strings"
     9  
    10  	"src.elv.sh/pkg/eval/vals"
    11  	"src.elv.sh/pkg/wcwidth"
    12  )
    13  
    14  // Text contains of a list of styled Segments.
    15  //
    16  // When only functions in this package are used to manipulate Text instances,
    17  // they will always satisfy the following properties:
    18  //
    19  //   - If the Text is empty, it is nil (not a non-nil slice of size 0).
    20  //
    21  //   - No [Segment] in it has an empty Text field.
    22  //
    23  //   - No two adjacent [Segment] instances have the same [Style].
    24  type Text []*Segment
    25  
    26  // T constructs a new Text with the given content and the given Styling's
    27  // applied.
    28  func T(s string, ts ...Styling) Text {
    29  	if s == "" {
    30  		return nil
    31  	}
    32  	return StyleText(Text{&Segment{Text: s}}, ts...)
    33  }
    34  
    35  // Concat concatenates multiple Text's into one.
    36  func Concat(texts ...Text) Text {
    37  	var tb TextBuilder
    38  	for _, text := range texts {
    39  		tb.WriteText(text)
    40  	}
    41  	return tb.Text()
    42  }
    43  
    44  // Kind returns "styled-text".
    45  func (Text) Kind() string { return "ui:text" }
    46  
    47  // Repr returns the representation of the current Text. It is just a wrapper
    48  // around the containing Segments.
    49  func (t Text) Repr(indent int) string {
    50  	buf := new(bytes.Buffer)
    51  	for _, s := range t {
    52  		buf.WriteByte(' ')
    53  		buf.WriteString(s.Repr(indent + 1))
    54  	}
    55  	return fmt.Sprintf("[^styled%s]", buf.String())
    56  }
    57  
    58  // IterateKeys feeds the function with all valid indices of the styled-text.
    59  func (t Text) IterateKeys(fn func(any) bool) {
    60  	for i := 0; i < len(t); i++ {
    61  		if !fn(strconv.Itoa(i)) {
    62  			break
    63  		}
    64  	}
    65  }
    66  
    67  // Index provides access to the underlying styled-segment.
    68  func (t Text) Index(k any) (any, error) {
    69  	index, err := vals.ConvertListIndex(k, len(t))
    70  	if err != nil {
    71  		return nil, err
    72  	} else if index.Slice {
    73  		return t[index.Lower:index.Upper], nil
    74  	} else {
    75  		return t[index.Lower], nil
    76  	}
    77  }
    78  
    79  // Concat implements Text+string, Text+number, Text+Segment and Text+Text.
    80  func (t Text) Concat(rhs any) (any, error) {
    81  	switch rhs := rhs.(type) {
    82  	case string:
    83  		return Concat(t, T(rhs)), nil
    84  	case int, *big.Int, *big.Rat, float64:
    85  		return Concat(t, T(vals.ToString(rhs))), nil
    86  	case *Segment:
    87  		return Concat(t, Text{rhs}), nil
    88  	case Text:
    89  		return Concat(t, rhs), nil
    90  	}
    91  
    92  	return nil, vals.ErrConcatNotImplemented
    93  }
    94  
    95  // RConcat implements string+Text and number+Text.
    96  func (t Text) RConcat(lhs any) (any, error) {
    97  	switch lhs := lhs.(type) {
    98  	case string:
    99  		return Concat(T(lhs), t), nil
   100  	case int, *big.Int, *big.Rat, float64:
   101  		return Concat(T(vals.ToString(lhs)), t), nil
   102  	}
   103  
   104  	return nil, vals.ErrConcatNotImplemented
   105  }
   106  
   107  // Partition partitions the Text at n indices into n+1 Text values.
   108  func (t Text) Partition(indices ...int) []Text {
   109  	out := make([]Text, len(indices)+1)
   110  	segs := t.Clone()
   111  	for i, idx := range indices {
   112  		toConsume := idx
   113  		if i > 0 {
   114  			toConsume -= indices[i-1]
   115  		}
   116  		for len(segs) > 0 && toConsume > 0 {
   117  			if len(segs[0].Text) <= toConsume {
   118  				out[i] = append(out[i], segs[0])
   119  				toConsume -= len(segs[0].Text)
   120  				segs = segs[1:]
   121  			} else {
   122  				out[i] = append(out[i], &Segment{segs[0].Style, segs[0].Text[:toConsume]})
   123  				segs[0] = &Segment{segs[0].Style, segs[0].Text[toConsume:]}
   124  				toConsume = 0
   125  			}
   126  		}
   127  	}
   128  	if len(segs) > 0 {
   129  		// Don't use segs directly to avoid memory leak
   130  		out[len(indices)] = append(Text(nil), segs...)
   131  	}
   132  	return out
   133  }
   134  
   135  // Clone returns a deep copy of Text.
   136  func (t Text) Clone() Text {
   137  	newt := make(Text, len(t))
   138  	for i, seg := range t {
   139  		newt[i] = seg.Clone()
   140  	}
   141  	return newt
   142  }
   143  
   144  // CountRune counts the number of times a rune occurs in a Text.
   145  func (t Text) CountRune(r rune) int {
   146  	n := 0
   147  	for _, seg := range t {
   148  		n += seg.CountRune(r)
   149  	}
   150  	return n
   151  }
   152  
   153  // CountLines counts the number of lines in a Text. It is equal to
   154  // t.CountRune('\n') + 1.
   155  func (t Text) CountLines() int {
   156  	return t.CountRune('\n') + 1
   157  }
   158  
   159  // SplitByRune splits a Text by the given rune.
   160  func (t Text) SplitByRune(r rune) []Text {
   161  	if len(t) == 0 {
   162  		return nil
   163  	}
   164  	// Call SplitByRune for each constituent Segment, and "paste" the pairs of
   165  	// subsegments across the segment border. For instance, if Text has 3
   166  	// Segments a, b, c that results in a1, a2, a3, b1, b2, c1, then a3 and b1
   167  	// as well as b2 and c1 are pasted together, and the return value is [a1],
   168  	// [a2], [a3, b1], [b2, c1]. Pasting can coalesce: for instance, if
   169  	// Text has 3 Segments a, b, c that results in a1, a2, b1, c1, the return
   170  	// value will be [a1], [a2, b1, c1].
   171  	var result []Text
   172  	var paste TextBuilder
   173  	for _, seg := range t {
   174  		subSegs := seg.SplitByRune(r)
   175  		// Paste the first segment.
   176  		paste.WriteText(TextFromSegment(subSegs[0]))
   177  		if len(subSegs) == 1 {
   178  			// Only one subsegment. Keep the paste active.
   179  			continue
   180  		}
   181  		// Add the paste and reset it.
   182  		result = append(result, paste.Text())
   183  		paste.Reset()
   184  		// For the subsegments in the middle, just add then as is.
   185  		for i := 1; i < len(subSegs)-1; i++ {
   186  			result = append(result, TextFromSegment(subSegs[i]))
   187  		}
   188  		// The last segment becomes the new paste.
   189  		paste.WriteText(TextFromSegment(subSegs[len(subSegs)-1]))
   190  	}
   191  	result = append(result, paste.Text())
   192  	return result
   193  }
   194  
   195  // TrimWcwidth returns the largest prefix of t that does not exceed the given
   196  // visual width.
   197  func (t Text) TrimWcwidth(wmax int) Text {
   198  	var newt Text
   199  	for _, seg := range t {
   200  		w := wcwidth.Of(seg.Text)
   201  		if w >= wmax {
   202  			newt = append(newt,
   203  				&Segment{seg.Style, wcwidth.Trim(seg.Text, wmax)})
   204  			break
   205  		}
   206  		wmax -= w
   207  		newt = append(newt, seg)
   208  	}
   209  	return newt
   210  }
   211  
   212  // String returns a string representation of the styled text. This now always
   213  // assumes VT-style terminal output.
   214  //
   215  // TODO: Make string conversion sensible to environment, e.g. use HTML when
   216  // output is web.
   217  func (t Text) String() string {
   218  	return t.VTString()
   219  }
   220  
   221  // VTString renders the styled text using VT-style escape sequences. Any
   222  // existing SGR state will be cleared.
   223  func (t Text) VTString() string {
   224  	var sb strings.Builder
   225  	clean := false
   226  	for _, seg := range t {
   227  		sgr := seg.SGR()
   228  		if sgr == "" {
   229  			if !clean {
   230  				sb.WriteString("\033[m")
   231  			}
   232  			clean = true
   233  		} else {
   234  			if clean {
   235  				sb.WriteString("\033[" + sgr + "m")
   236  			} else {
   237  				sb.WriteString("\033[;" + sgr + "m")
   238  			}
   239  			clean = false
   240  		}
   241  		sb.WriteString(seg.Text)
   242  	}
   243  	if !clean {
   244  		sb.WriteString("\033[m")
   245  	}
   246  	return sb.String()
   247  }
   248  
   249  // TextFromSegment returns a [Text] with just seg if seg.Text is non-empty.
   250  // Otherwise it returns nil.
   251  func TextFromSegment(seg *Segment) Text {
   252  	if seg.Text == "" {
   253  		return nil
   254  	}
   255  	return Text{seg}
   256  }