mirror of
https://github.com/go-sylixos/elvish.git
synced 2024-12-13 09:57:51 +08:00
42c6c3b1aa
Styled text is not supposed to "inherit" the current SGR styling context when written to the terminal. This has always been the intention, but not correctly implemented. This commit fixes that for both styled segments and styled texts. Tests are amended to account for the difference in the output. With context insensitivity correctly implemented, there is now no need for a "default" color. It is functionally equivalent to a lack of color. The parsing of SGR still needs to be aware of the codes 39 (default foreground) and 49 (default background), but these codes are now translated into FgDefault and BgDefault, which resets the foreground and background color fields.
156 lines
4.0 KiB
Go
156 lines
4.0 KiB
Go
package ui
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"math/big"
|
|
"strings"
|
|
|
|
"src.elv.sh/pkg/eval/vals"
|
|
)
|
|
|
|
// Segment is a string that has some style applied to it.
|
|
type Segment struct {
|
|
Style
|
|
Text string
|
|
}
|
|
|
|
// Kind returns "styled-segment".
|
|
func (*Segment) Kind() string { return "ui:text-segment" }
|
|
|
|
// Repr returns the representation of this Segment. The string can be used to
|
|
// construct an identical Segment. Unset or default attributes are skipped. If
|
|
// the Segment represents an unstyled string only this string is returned.
|
|
func (s *Segment) Repr(int) string {
|
|
buf := new(bytes.Buffer)
|
|
addIfNotEqual := func(key string, val, cmp any) {
|
|
if val != cmp {
|
|
var valString string
|
|
if c, ok := val.(Color); ok {
|
|
valString = c.String()
|
|
} else {
|
|
valString = vals.Repr(val, 0)
|
|
}
|
|
fmt.Fprintf(buf, "&%s=%s ", key, valString)
|
|
}
|
|
}
|
|
|
|
addIfNotEqual("fg-color", s.Foreground, nil)
|
|
addIfNotEqual("bg-color", s.Background, nil)
|
|
addIfNotEqual("bold", s.Bold, false)
|
|
addIfNotEqual("dim", s.Dim, false)
|
|
addIfNotEqual("italic", s.Italic, false)
|
|
addIfNotEqual("underlined", s.Underlined, false)
|
|
addIfNotEqual("blink", s.Blink, false)
|
|
addIfNotEqual("inverse", s.Inverse, false)
|
|
|
|
if buf.Len() == 0 {
|
|
return s.Text
|
|
}
|
|
|
|
return fmt.Sprintf("(ui:text-segment %s %s)", s.Text, strings.TrimSpace(buf.String()))
|
|
}
|
|
|
|
// IterateKeys feeds the function with all valid attributes of styled-segment.
|
|
func (*Segment) IterateKeys(fn func(v any) bool) {
|
|
vals.Feed(fn, "text", "fg-color", "bg-color", "bold", "dim", "italic", "underlined", "blink", "inverse")
|
|
}
|
|
|
|
// Index provides access to the attributes of a styled-segment.
|
|
func (s *Segment) Index(k any) (v any, ok bool) {
|
|
switch k {
|
|
case "text":
|
|
v = s.Text
|
|
case "fg-color":
|
|
if s.Foreground == nil {
|
|
return "default", true
|
|
}
|
|
return s.Foreground.String(), true
|
|
case "bg-color":
|
|
if s.Background == nil {
|
|
return "default", true
|
|
}
|
|
return s.Background.String(), true
|
|
case "bold":
|
|
v = s.Bold
|
|
case "dim":
|
|
v = s.Dim
|
|
case "italic":
|
|
v = s.Italic
|
|
case "underlined":
|
|
v = s.Underlined
|
|
case "blink":
|
|
v = s.Blink
|
|
case "inverse":
|
|
v = s.Inverse
|
|
}
|
|
|
|
return v, v != nil
|
|
}
|
|
|
|
// Concat implements Segment+string, Segment+float64, Segment+Segment and
|
|
// Segment+Text.
|
|
func (s *Segment) Concat(v any) (any, error) {
|
|
switch rhs := v.(type) {
|
|
case string:
|
|
return Text{s, &Segment{Text: rhs}}, nil
|
|
case *Segment:
|
|
return Text{s, rhs}, nil
|
|
case Text:
|
|
return Text(append([]*Segment{s}, rhs...)), nil
|
|
case int, *big.Int, *big.Rat, float64:
|
|
return Text{s, &Segment{Text: vals.ToString(rhs)}}, nil
|
|
}
|
|
return nil, vals.ErrConcatNotImplemented
|
|
}
|
|
|
|
// RConcat implements string+Segment and float64+Segment.
|
|
func (s *Segment) RConcat(v any) (any, error) {
|
|
switch lhs := v.(type) {
|
|
case string:
|
|
return Text{&Segment{Text: lhs}, s}, nil
|
|
case int, *big.Int, *big.Rat, float64:
|
|
return Text{&Segment{Text: vals.ToString(lhs)}, s}, nil
|
|
}
|
|
return nil, vals.ErrConcatNotImplemented
|
|
}
|
|
|
|
// Clone returns a copy of the Segment.
|
|
func (s *Segment) Clone() *Segment {
|
|
value := *s
|
|
return &value
|
|
}
|
|
|
|
// CountRune counts the number of times a rune occurs in a Segment.
|
|
func (s *Segment) CountRune(r rune) int {
|
|
return strings.Count(s.Text, string(r))
|
|
}
|
|
|
|
// SplitByRune splits a Segment by the given rune.
|
|
func (s *Segment) SplitByRune(r rune) []*Segment {
|
|
splitTexts := strings.Split(s.Text, string(r))
|
|
splitSegs := make([]*Segment, len(splitTexts))
|
|
for i, splitText := range splitTexts {
|
|
splitSegs[i] = &Segment{s.Style, splitText}
|
|
}
|
|
return splitSegs
|
|
}
|
|
|
|
// String returns a string representation of the styled segment. This now always
|
|
// assumes VT-style terminal output.
|
|
// TODO: Make string conversion sensible to environment, e.g. use HTML when
|
|
// output is web.
|
|
func (s *Segment) String() string {
|
|
return s.VTString()
|
|
}
|
|
|
|
// VTString renders the styled segment using VT-style escape sequences. Any
|
|
// existing SGR state will be cleared.
|
|
func (s *Segment) VTString() string {
|
|
sgr := s.SGR()
|
|
if sgr == "" {
|
|
return "\033[m" + s.Text
|
|
}
|
|
return fmt.Sprintf("\033[;%sm%s\033[m", sgr, s.Text)
|
|
}
|