github.com/markusbkk/elvish@v0.0.0-20231204143114-91dc52438621/pkg/eval/builtin_fn_styled.go (about)

     1  package eval
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  
     7  	"github.com/markusbkk/elvish/pkg/eval/vals"
     8  	"github.com/markusbkk/elvish/pkg/parse"
     9  	"github.com/markusbkk/elvish/pkg/ui"
    10  )
    11  
    12  var errStyledSegmentArgType = errors.New("argument to styled-segment must be a string or a styled segment")
    13  
    14  func init() {
    15  	addBuiltinFns(map[string]interface{}{
    16  		"styled-segment": styledSegment,
    17  		"styled":         styled,
    18  	})
    19  }
    20  
    21  //elvdoc:fn styled-segment
    22  //
    23  // ```elvish
    24  // styled-segment $object &fg-color=default &bg-color=default &bold=$false &dim=$false &italic=$false &underlined=$false &blink=$false &inverse=$false
    25  // ```
    26  //
    27  // Constructs a styled segment and is a helper function for styled transformers.
    28  // `$object` can be a plain string, a styled segment or a concatenation thereof.
    29  // Probably the only reason to use it is to build custom style transformers:
    30  //
    31  // ```elvish
    32  // fn my-awesome-style-transformer {|seg| styled-segment $seg &bold=(not $seg[dim]) &dim=(not $seg[italic]) &italic=$seg[bold] }
    33  // styled abc $my-awesome-style-transformer~
    34  // ```
    35  //
    36  // As just seen the properties of styled segments can be inspected by indexing into
    37  // it. Valid indices are the same as the options to `styled-segment` plus `text`.
    38  //
    39  // ```elvish
    40  // var s = (styled-segment abc &bold)
    41  // put $s[text]
    42  // put $s[fg-color]
    43  // put $s[bold]
    44  // ```
    45  
    46  // Turns a string or ui.Segment into a new ui.Segment with the attributes
    47  // from the supplied options applied to it. If the input is already a Segment its
    48  // attributes are copied and modified.
    49  func styledSegment(options RawOptions, input interface{}) (*ui.Segment, error) {
    50  	var text string
    51  	var style ui.Style
    52  
    53  	switch input := input.(type) {
    54  	case string:
    55  		text = input
    56  	case *ui.Segment:
    57  		text = input.Text
    58  		style = input.Style
    59  	default:
    60  		return nil, errStyledSegmentArgType
    61  	}
    62  
    63  	if err := style.MergeFromOptions(options); err != nil {
    64  		return nil, err
    65  	}
    66  
    67  	return &ui.Segment{
    68  		Text:  text,
    69  		Style: style,
    70  	}, nil
    71  }
    72  
    73  //elvdoc:fn styled
    74  //
    75  // ```elvish
    76  // styled $object $style-transformer...
    77  // ```
    78  //
    79  // Construct a styled text by applying the supplied transformers to the supplied
    80  // object. `$object` can be either a string, a styled segment (see below), a styled
    81  // text or an arbitrary concatenation of them. A `$style-transformer` is either:
    82  //
    83  // -   The name of a builtin style transformer, which may be one of the following:
    84  //
    85  //     -   One of the attribute names `bold`, `dim`, `italic`, `underlined`,
    86  //     `blink` or `inverse` for setting the corresponding attribute.
    87  //
    88  //     -   An attribute name prefixed by `no-` for unsetting the attribute.
    89  //
    90  //     -   An attribute name prefixed by `toggle-` for toggling the attribute
    91  //     between set and unset.
    92  //
    93  // -   A color name for setting the text color, which may be one of the
    94  // following:
    95  //
    96  //     -   One of the 8 basic ANSI colors: `black`, `red`, `green`, `yellow`,
    97  //    `blue`, `magenta`, `cyan` and `white`.
    98  //
    99  //     -   The bright variant of the 8 basic ANSI colors, with a `bright-`
   100  //    prefix.
   101  //
   102  //     -   Any color from the xterm 256-color palette, as `colorX` (such as
   103  //    `color12`).
   104  //
   105  //     -   A 24-bit RGB color written as `#RRGGBB` such as `'#778899'`.
   106  //
   107  //		   **Note**: You need to quote such values since an unquoted `#` char
   108  //		   introduces a comment. So use `'bg-#778899'` not `bg-#778899`. If
   109  //		   you omit the quotes the text after the `#` char is ignored which
   110  //		   will result in an error or unexpected behavior.
   111  //
   112  // -   A color name prefixed by `bg-` to set the background color.
   113  //
   114  // -   A color name prefixed by `fg-` to set the foreground color. This has
   115  // the same effect as specifying the color name without the `fg-` prefix.
   116  //
   117  // -   A lambda that receives a styled segment as the only argument and returns a
   118  // single styled segment.
   119  //
   120  // -   A function with the same properties as the lambda (provided via the
   121  // `$transformer~` syntax).
   122  //
   123  // When a styled text is converted to a string the corresponding
   124  // [ANSI SGR code](https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_.28Select_Graphic_Rendition.29_parameters)
   125  // is built to render the style.
   126  //
   127  // A styled text is nothing more than a wrapper around a list of styled segments.
   128  // They can be accessed by indexing into it.
   129  //
   130  // ```elvish
   131  // var s = (styled abc red)(styled def green)
   132  // put $s[0] $s[1]
   133  // ```
   134  
   135  func styled(fm *Frame, input interface{}, stylings ...interface{}) (ui.Text, error) {
   136  	var text ui.Text
   137  
   138  	switch input := input.(type) {
   139  	case string:
   140  		text = ui.Text{&ui.Segment{
   141  			Text:  input,
   142  			Style: ui.Style{},
   143  		}}
   144  	case *ui.Segment:
   145  		text = ui.Text{input.Clone()}
   146  	case ui.Text:
   147  		text = input.Clone()
   148  	default:
   149  		return nil, fmt.Errorf("expected string, styled segment or styled text; got %s", vals.Kind(input))
   150  	}
   151  
   152  	for _, styling := range stylings {
   153  		switch styling := styling.(type) {
   154  		case string:
   155  			parsedStyling := ui.ParseStyling(styling)
   156  			if parsedStyling == nil {
   157  				return nil, fmt.Errorf("%s is not a valid style transformer", parse.Quote(styling))
   158  			}
   159  			text = ui.StyleText(text, parsedStyling)
   160  		case Callable:
   161  			for i, seg := range text {
   162  				vs, err := fm.CaptureOutput(func(fm *Frame) error {
   163  					return styling.Call(fm, []interface{}{seg}, NoOpts)
   164  				})
   165  				if err != nil {
   166  					return nil, err
   167  				}
   168  
   169  				if n := len(vs); n != 1 {
   170  					return nil, fmt.Errorf("styling function must return a single segment; got %d values", n)
   171  				} else if styledSegment, ok := vs[0].(*ui.Segment); !ok {
   172  					return nil, fmt.Errorf("styling function must return a segment; got %s", vals.Kind(vs[0]))
   173  				} else {
   174  					text[i] = styledSegment
   175  				}
   176  			}
   177  
   178  		default:
   179  			return nil, fmt.Errorf("need string or callable; got %s", vals.Kind(styling))
   180  		}
   181  	}
   182  
   183  	return text, nil
   184  }