text_segment.go 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155
  1. package ui
  2. import (
  3. "bytes"
  4. "fmt"
  5. "math/big"
  6. "strings"
  7. "src.elv.sh/pkg/eval/vals"
  8. )
  9. // Segment is a string that has some style applied to it.
  10. type Segment struct {
  11. Style
  12. Text string
  13. }
  14. // Kind returns "styled-segment".
  15. func (*Segment) Kind() string { return "ui:text-segment" }
  16. // Repr returns the representation of this Segment. The string can be used to
  17. // construct an identical Segment. Unset or default attributes are skipped. If
  18. // the Segment represents an unstyled string only this string is returned.
  19. func (s *Segment) Repr(int) string {
  20. buf := new(bytes.Buffer)
  21. addIfNotEqual := func(key string, val, cmp any) {
  22. if val != cmp {
  23. var valString string
  24. if c, ok := val.(Color); ok {
  25. valString = c.String()
  26. } else {
  27. valString = vals.Repr(val, 0)
  28. }
  29. fmt.Fprintf(buf, "&%s=%s ", key, valString)
  30. }
  31. }
  32. addIfNotEqual("fg-color", s.Foreground, nil)
  33. addIfNotEqual("bg-color", s.Background, nil)
  34. addIfNotEqual("bold", s.Bold, false)
  35. addIfNotEqual("dim", s.Dim, false)
  36. addIfNotEqual("italic", s.Italic, false)
  37. addIfNotEqual("underlined", s.Underlined, false)
  38. addIfNotEqual("blink", s.Blink, false)
  39. addIfNotEqual("inverse", s.Inverse, false)
  40. if buf.Len() == 0 {
  41. return s.Text
  42. }
  43. return fmt.Sprintf("(ui:text-segment %s %s)", s.Text, strings.TrimSpace(buf.String()))
  44. }
  45. // IterateKeys feeds the function with all valid attributes of styled-segment.
  46. func (*Segment) IterateKeys(fn func(v any) bool) {
  47. vals.Feed(fn, "text", "fg-color", "bg-color", "bold", "dim", "italic", "underlined", "blink", "inverse")
  48. }
  49. // Index provides access to the attributes of a styled-segment.
  50. func (s *Segment) Index(k any) (v any, ok bool) {
  51. switch k {
  52. case "text":
  53. v = s.Text
  54. case "fg-color":
  55. if s.Foreground == nil {
  56. return "default", true
  57. }
  58. return s.Foreground.String(), true
  59. case "bg-color":
  60. if s.Background == nil {
  61. return "default", true
  62. }
  63. return s.Background.String(), true
  64. case "bold":
  65. v = s.Bold
  66. case "dim":
  67. v = s.Dim
  68. case "italic":
  69. v = s.Italic
  70. case "underlined":
  71. v = s.Underlined
  72. case "blink":
  73. v = s.Blink
  74. case "inverse":
  75. v = s.Inverse
  76. }
  77. return v, v != nil
  78. }
  79. // Concat implements Segment+string, Segment+float64, Segment+Segment and
  80. // Segment+Text.
  81. func (s *Segment) Concat(v any) (any, error) {
  82. switch rhs := v.(type) {
  83. case string:
  84. return Text{s, &Segment{Text: rhs}}, nil
  85. case *Segment:
  86. return Text{s, rhs}, nil
  87. case Text:
  88. return Text(append([]*Segment{s}, rhs...)), nil
  89. case int, *big.Int, *big.Rat, float64:
  90. return Text{s, &Segment{Text: vals.ToString(rhs)}}, nil
  91. }
  92. return nil, vals.ErrConcatNotImplemented
  93. }
  94. // RConcat implements string+Segment and float64+Segment.
  95. func (s *Segment) RConcat(v any) (any, error) {
  96. switch lhs := v.(type) {
  97. case string:
  98. return Text{&Segment{Text: lhs}, s}, nil
  99. case int, *big.Int, *big.Rat, float64:
  100. return Text{&Segment{Text: vals.ToString(lhs)}, s}, nil
  101. }
  102. return nil, vals.ErrConcatNotImplemented
  103. }
  104. // Clone returns a copy of the Segment.
  105. func (s *Segment) Clone() *Segment {
  106. value := *s
  107. return &value
  108. }
  109. // CountRune counts the number of times a rune occurs in a Segment.
  110. func (s *Segment) CountRune(r rune) int {
  111. return strings.Count(s.Text, string(r))
  112. }
  113. // SplitByRune splits a Segment by the given rune.
  114. func (s *Segment) SplitByRune(r rune) []*Segment {
  115. splitTexts := strings.Split(s.Text, string(r))
  116. splitSegs := make([]*Segment, len(splitTexts))
  117. for i, splitText := range splitTexts {
  118. splitSegs[i] = &Segment{s.Style, splitText}
  119. }
  120. return splitSegs
  121. }
  122. // String returns a string representation of the styled segment. This now always
  123. // assumes VT-style terminal output.
  124. // TODO: Make string conversion sensible to environment, e.g. use HTML when
  125. // output is web.
  126. func (s *Segment) String() string {
  127. return s.VTString()
  128. }
  129. // VTString renders the styled segment using VT-style escape sequences. Any
  130. // existing SGR state will be cleared.
  131. func (s *Segment) VTString() string {
  132. sgr := s.SGR()
  133. if sgr == "" {
  134. return "\033[m" + s.Text
  135. }
  136. return fmt.Sprintf("\033[;%sm%s\033[m", sgr, s.Text)
  137. }