mirror of
https://github.com/go-sylixos/elvish.git
synced 2024-12-13 01:47:51 +08:00
Add styled
and styled-segment
builtins (#674)
* Add compatibility test with old implementation * Add color type * Add basic style structs and utilities * Add structs for styled segments and texts * Add default style transformers to reimplement $edit:styled~ * Add builtins to manipulate styled segments and texts * Rename style 'underline' -> 'underlined' * Fix test case * Add conversion from styled text to ansi sequences * Return errors rather than throwing * Validate the type of boolean options * Delegate old to new styled function * Rebase for new test framework api and expand test cases * Remove old builtin function $edit:styled~ * Use strings to represent colors * Convert bool pointers to simple bool values * Validate color strings * Do no longer expose builtin style transformers * Fix confusion about pointers * Make outputs more stable * Expand tests * Use pointers instead of passing setter functions * Unexport and rename color check * Use the empty string for default colors * Expand tests * Simplify styled transformers Now there are three transformers for each boolean style attribute that allow setting, unsetting and toggling the corresponding attribute. * Rework and add doc comments
This commit is contained in:
parent
d120747bbf
commit
e89fe48870
|
@ -93,7 +93,6 @@ func makeNs(ed *editor) eval.Ns {
|
||||||
"binding-table": eddefs.MakeBindingMap,
|
"binding-table": eddefs.MakeBindingMap,
|
||||||
"insert-at-dot": ed.InsertAtDot,
|
"insert-at-dot": ed.InsertAtDot,
|
||||||
"replace-input": ed.replaceInput,
|
"replace-input": ed.replaceInput,
|
||||||
"styled": styled,
|
|
||||||
"key": ui.ToKey,
|
"key": ui.ToKey,
|
||||||
"wordify": wordifyBuiltin,
|
"wordify": wordifyBuiltin,
|
||||||
"-dump-buf": ed.dumpBuf,
|
"-dump-buf": ed.dumpBuf,
|
||||||
|
|
|
@ -1,30 +0,0 @@
|
||||||
package edcore
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
|
|
||||||
"github.com/elves/elvish/edit/ui"
|
|
||||||
"github.com/xiaq/persistent/vector"
|
|
||||||
)
|
|
||||||
|
|
||||||
var errStyledStyles = errors.New("styles must either be a string or list of strings")
|
|
||||||
|
|
||||||
// A constructor for *ui.Styled, for use in Elvish script.
|
|
||||||
func styled(text string, styles interface{}) (*ui.Styled, error) {
|
|
||||||
switch styles := styles.(type) {
|
|
||||||
case string:
|
|
||||||
return &ui.Styled{text, ui.StylesFromString(styles)}, nil
|
|
||||||
case vector.Vector:
|
|
||||||
converted := make([]string, 0, styles.Len())
|
|
||||||
for it := styles.Iterator(); it.HasElem(); it.Next() {
|
|
||||||
elem, ok := it.Elem().(string)
|
|
||||||
if !ok {
|
|
||||||
return nil, errStyledStyles
|
|
||||||
}
|
|
||||||
converted = append(converted, elem)
|
|
||||||
}
|
|
||||||
return &ui.Styled{text, ui.Styles(converted)}, nil
|
|
||||||
default:
|
|
||||||
return nil, errStyledStyles
|
|
||||||
}
|
|
||||||
}
|
|
100
eval/builtin_fn_styled.go
Normal file
100
eval/builtin_fn_styled.go
Normal file
|
@ -0,0 +1,100 @@
|
||||||
|
package eval
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/elves/elvish/eval/vals"
|
||||||
|
"github.com/elves/elvish/styled"
|
||||||
|
)
|
||||||
|
|
||||||
|
var errStyledSegmentArgType = errors.New("argument to styled-segment must be a string or a styled segment")
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
addBuiltinFns(map[string]interface{}{
|
||||||
|
"styled-segment": styledSegment,
|
||||||
|
"styled": styledBuiltin,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Turns a string or styled Segment into a new styled Segment with the attributes
|
||||||
|
// from the supplied options applied to it. If the input is already a Segment its
|
||||||
|
// attributes are copied and modified.
|
||||||
|
func styledSegment(options RawOptions, input interface{}) (*styled.Segment, error) {
|
||||||
|
var text string
|
||||||
|
var style styled.Style
|
||||||
|
|
||||||
|
switch input := input.(type) {
|
||||||
|
case string:
|
||||||
|
text = input
|
||||||
|
case *styled.Segment:
|
||||||
|
text = input.Text
|
||||||
|
style = input.Style
|
||||||
|
default:
|
||||||
|
return nil, errStyledSegmentArgType
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := style.ImportFromOptions(options); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &styled.Segment{
|
||||||
|
Text: text,
|
||||||
|
Style: style,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Turns a string, a styled Segment or a styled Text into a styled Text. This is done by
|
||||||
|
// applying a range of transformers to the input.
|
||||||
|
func styledBuiltin(fm *Frame, input interface{}, transformers ...interface{}) (*styled.Text, error) {
|
||||||
|
var text styled.Text
|
||||||
|
|
||||||
|
switch input := input.(type) {
|
||||||
|
case string:
|
||||||
|
text = styled.Text{styled.Segment{
|
||||||
|
Text: input,
|
||||||
|
Style: styled.Style{},
|
||||||
|
}}
|
||||||
|
case *styled.Segment:
|
||||||
|
text = styled.Text{*input}
|
||||||
|
case *styled.Text:
|
||||||
|
text = *input
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("expected string, styled segment or styled text; got %s", vals.Kind(input))
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, transformer := range transformers {
|
||||||
|
switch transformer := transformer.(type) {
|
||||||
|
case string:
|
||||||
|
transformerFn := styled.FindTransformer(transformer)
|
||||||
|
if transformerFn == nil {
|
||||||
|
return nil, fmt.Errorf("'%s' is no valid style transformer", transformer)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, segment := range text {
|
||||||
|
text[i] = transformerFn(segment)
|
||||||
|
}
|
||||||
|
|
||||||
|
case Callable:
|
||||||
|
for i, segment := range text {
|
||||||
|
vs, err := fm.CaptureOutput(transformer, []interface{}{&segment}, NoOpts)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if n := len(vs); n != 1 {
|
||||||
|
return nil, fmt.Errorf("style transformers must return a single styled segment; got %d", n)
|
||||||
|
} else if transformedSegment, ok := vs[0].(*styled.Segment); !ok {
|
||||||
|
return nil, fmt.Errorf("style transformers must return a styled segment; got %s", vals.Kind(vs[0]))
|
||||||
|
} else {
|
||||||
|
text[i] = *transformedSegment
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("need string or callable; got %s", vals.Kind(transformer))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &text, nil
|
||||||
|
}
|
100
eval/builtin_fn_styled_test.go
Normal file
100
eval/builtin_fn_styled_test.go
Normal file
|
@ -0,0 +1,100 @@
|
||||||
|
package eval
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestStyledString(t *testing.T) {
|
||||||
|
Test(t,
|
||||||
|
That("print (styled abc hopefully-never-exists)").ErrorsWith(errors.New("'hopefully-never-exists' is no valid style transformer")),
|
||||||
|
That("print (styled abc bold)").Prints("\033[1mabc\033[m"),
|
||||||
|
That("print (styled abc red cyan)").Prints("\033[36mabc\033[m"),
|
||||||
|
That("print (styled abc bg-green)").Prints("\033[42mabc\033[m"),
|
||||||
|
That("print (styled abc no-dim)").Prints("abc"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStyledSegment(t *testing.T) {
|
||||||
|
Test(t,
|
||||||
|
That("print (styled (styled-segment abc &fg-color=cyan) bold)").Prints("\033[1;36mabc\033[m"),
|
||||||
|
That("print (styled (styled-segment (styled-segment abc &fg-color=magenta) &dim=$true) cyan)").Prints("\033[2;36mabc\033[m"),
|
||||||
|
That("print (styled (styled-segment abc &inverse=$true) inverse)").Prints("\033[7mabc\033[m"),
|
||||||
|
That("print (styled (styled-segment abc) toggle-inverse)").Prints("\033[7mabc\033[m"),
|
||||||
|
That("print (styled (styled-segment abc &inverse=$true) no-inverse)").Prints("abc"),
|
||||||
|
That("print (styled (styled-segment abc &inverse=$true) toggle-inverse)").Prints("abc"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStyledText(t *testing.T) {
|
||||||
|
Test(t,
|
||||||
|
That("print (styled (styled abc red) blue)").Prints("\033[34mabc\033[m"),
|
||||||
|
That("print (styled (styled abc italic) red)").Prints("\033[3;31mabc\033[m"),
|
||||||
|
That("print (styled (styled abc inverse) inverse)").Prints("\033[7mabc\033[m"),
|
||||||
|
That("print (styled (styled abc inverse) no-inverse)").Prints("abc"),
|
||||||
|
That("print (styled (styled abc inverse) toggle-inverse)").Prints("abc"),
|
||||||
|
That("print (styled (styled abc inverse) toggle-inverse toggle-inverse)").Prints("\033[7mabc\033[m"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStyledConcat(t *testing.T) {
|
||||||
|
Test(t,
|
||||||
|
// string+segment
|
||||||
|
That("print abc(styled-segment abc &fg-color=red)").Prints("abc\033[31mabc\033[m"),
|
||||||
|
// segment+string
|
||||||
|
That("print (styled-segment abc &fg-color=red)abc").Prints("\033[31mabc\033[mabc"),
|
||||||
|
// segment+segment
|
||||||
|
That("print (styled-segment abc &bg-color=red)(styled-segment abc &fg-color=red)").Prints("\033[41mabc\033[m\033[31mabc\033[m"),
|
||||||
|
// segment+text
|
||||||
|
That("print (styled-segment abc &underlined=$true)(styled abc lightcyan)").Prints("\033[4mabc\033[m\033[96mabc\033[m"),
|
||||||
|
)
|
||||||
|
|
||||||
|
Test(t,
|
||||||
|
// string+text
|
||||||
|
That("print abc(styled abc blink)").Prints("abc\033[5mabc\033[m"),
|
||||||
|
// text+string
|
||||||
|
That("print (styled abc blink)abc").Prints("\033[5mabc\033[mabc"),
|
||||||
|
// text+segment
|
||||||
|
That("print (styled abc inverse)(styled-segment abc &bg-color=white)").Prints("\033[7mabc\033[m\033[107mabc\033[m"),
|
||||||
|
// text+text
|
||||||
|
That("print (styled abc bold)(styled abc dim)").Prints("\033[1mabc\033[m\033[2mabc\033[m"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFunctionalStyleTransformers(t *testing.T) {
|
||||||
|
// lambda
|
||||||
|
Test(t,
|
||||||
|
That("print (styled abc [s]{ put $s })").Prints("abc"),
|
||||||
|
That("print (styled abc [s]{ styled-segment $s &bold=$true &italic=$false })").Prints("\033[1mabc\033[m"),
|
||||||
|
That("print (styled abc italic [s]{ styled-segment $s &bold=$true &italic=$false })").Prints("\033[1mabc\033[m"),
|
||||||
|
)
|
||||||
|
|
||||||
|
// fn
|
||||||
|
Test(t,
|
||||||
|
That("fn f [s]{ put $s }; print (styled abc $f~)").Prints("abc"),
|
||||||
|
That("fn f [s]{ styled-segment $s &bold=$true &italic=$false }; print (styled abc $f~)").Prints("\033[1mabc\033[m"),
|
||||||
|
That("fn f [s]{ styled-segment $s &bold=$true &italic=$false }; print (styled abc italic $f~)").Prints("\033[1mabc\033[m"),
|
||||||
|
)
|
||||||
|
|
||||||
|
// var
|
||||||
|
Test(t,
|
||||||
|
That("f = [s]{ put $s }; print (styled abc $f)").Prints("abc"),
|
||||||
|
That("f = [s]{ styled-segment $s &bold=$true &italic=$false }; print (styled abc $f)").Prints("\033[1mabc\033[m"),
|
||||||
|
That("f = [s]{ styled-segment $s &bold=$true &italic=$false }; print (styled abc italic $f)").Prints("\033[1mabc\033[m"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStyledIndexing(t *testing.T) {
|
||||||
|
Test(t,
|
||||||
|
That("put (styled-segment abc &italic=$true &fg-color=red)[bold]").Puts(false),
|
||||||
|
That("put (styled-segment abc &italic=$true &fg-color=red)[italic]").Puts(true),
|
||||||
|
That("put (styled-segment abc &italic=$true &fg-color=red)[fg-color]").Puts("red"),
|
||||||
|
)
|
||||||
|
|
||||||
|
Test(t,
|
||||||
|
That("put (styled abc red)[0][bold]").Puts(false),
|
||||||
|
That("put (styled abc red)[0][bg-color]").Puts("default"),
|
||||||
|
That("t = (styled-segment abc &underlined=$true)(styled abc lightcyan); put $t[1][fg-color]").Puts("lightcyan"),
|
||||||
|
That("t = (styled-segment abc &underlined=$true)(styled abc lightcyan); put $t[1][underlined]").Puts(false),
|
||||||
|
)
|
||||||
|
}
|
|
@ -32,23 +32,23 @@ debug-mode = $false
|
||||||
|
|
||||||
fn -debug [text]{
|
fn -debug [text]{
|
||||||
if $debug-mode {
|
if $debug-mode {
|
||||||
print (edit:styled '=> ' blue)
|
print (styled '=> ' blue)
|
||||||
echo $text
|
echo $text
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn -info [text]{
|
fn -info [text]{
|
||||||
print (edit:styled '=> ' green)
|
print (styled '=> ' green)
|
||||||
echo $text
|
echo $text
|
||||||
}
|
}
|
||||||
|
|
||||||
fn -warn [text]{
|
fn -warn [text]{
|
||||||
print (edit:styled '=> ' yellow)
|
print (styled '=> ' yellow)
|
||||||
echo $text
|
echo $text
|
||||||
}
|
}
|
||||||
|
|
||||||
fn -error [text]{
|
fn -error [text]{
|
||||||
print (edit:styled '=> ' red)
|
print (styled '=> ' red)
|
||||||
echo $text
|
echo $text
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -257,20 +257,20 @@ fn metadata [pkg]{
|
||||||
fn query [pkg]{
|
fn query [pkg]{
|
||||||
data = (metadata $pkg)
|
data = (metadata $pkg)
|
||||||
special-keys = [name method installed src dst]
|
special-keys = [name method installed src dst]
|
||||||
echo (edit:styled "Package "$data[name] cyan)
|
echo (styled "Package "$data[name] cyan)
|
||||||
if $data[installed] {
|
if $data[installed] {
|
||||||
echo (edit:styled "Installed at "$data[dst] green)
|
echo (styled "Installed at "$data[dst] green)
|
||||||
} else {
|
} else {
|
||||||
echo (edit:styled "Not installed" red)
|
echo (styled "Not installed" red)
|
||||||
}
|
}
|
||||||
echo (edit:styled "Source:" blue) $data[method] $data[src]
|
echo (styled "Source:" blue) $data[method] $data[src]
|
||||||
keys $data | each [key]{
|
keys $data | each [key]{
|
||||||
if (not (has-value $special-keys $key)) {
|
if (not (has-value $special-keys $key)) {
|
||||||
val = $data[$key]
|
val = $data[$key]
|
||||||
if (eq (kind-of $val) list) {
|
if (eq (kind-of $val) list) {
|
||||||
val = (joins ", " $val)
|
val = (joins ", " $val)
|
||||||
}
|
}
|
||||||
echo (edit:styled (-first-upper $key)":" blue) $val
|
echo (styled (-first-upper $key)":" blue) $val
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
108
styled/segment.go
Normal file
108
styled/segment.go
Normal file
|
@ -0,0 +1,108 @@
|
||||||
|
package styled
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/elves/elvish/eval/vals"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Segment is a string that has some style applied to it.
|
||||||
|
type Segment struct {
|
||||||
|
Style
|
||||||
|
Text string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (Segment) Kind() string { return "styled-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(indent int) string {
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
addIfNotEqual := func(key string, val, cmp interface{}) {
|
||||||
|
if val != cmp {
|
||||||
|
fmt.Fprintf(buf, "&%s=%s ", key, vals.Repr(val, 0))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addIfNotEqual("fg-color", s.Foreground, "")
|
||||||
|
addIfNotEqual("bg-color", s.Background, "")
|
||||||
|
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("(styled-segment %s %s)", s.Text, strings.TrimSpace(buf.String()))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s Segment) IterateKeys(fn func(v interface{}) bool) {
|
||||||
|
vals.Feed(fn, "text", "fg-color", "bg-color", "bold", "dim", "italic", "underlined", "blink", "inverse")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Index provides access to the attributes of the Segment.
|
||||||
|
func (s Segment) Index(k interface{}) (v interface{}, ok bool) {
|
||||||
|
switch k {
|
||||||
|
case "text":
|
||||||
|
v = s.Text
|
||||||
|
case "fg-color":
|
||||||
|
v = s.Foreground
|
||||||
|
case "bg-color":
|
||||||
|
v = s.Background
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
if v == "" {
|
||||||
|
v = "default"
|
||||||
|
}
|
||||||
|
|
||||||
|
return v, v != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Concat implements Segment+string, Segment+Segment and Segment+Text.
|
||||||
|
func (s Segment) Concat(v interface{}) (interface{}, 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
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, vals.ErrConcatNotImplemented
|
||||||
|
}
|
||||||
|
|
||||||
|
// RConcat implements string+Segment.
|
||||||
|
func (s Segment) RConcat(v interface{}) (interface{}, error) {
|
||||||
|
switch lhs := v.(type) {
|
||||||
|
case string:
|
||||||
|
return Text{
|
||||||
|
Segment{Text: lhs},
|
||||||
|
s,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, vals.ErrConcatNotImplemented
|
||||||
|
}
|
101
styled/styled.go
Normal file
101
styled/styled.go
Normal file
|
@ -0,0 +1,101 @@
|
||||||
|
package styled
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Style specifies how something (mostly a string) shall be displayed.
|
||||||
|
type Style struct {
|
||||||
|
Foreground string
|
||||||
|
Background string
|
||||||
|
Bold bool
|
||||||
|
Dim bool
|
||||||
|
Italic bool
|
||||||
|
Underlined bool
|
||||||
|
Blink bool
|
||||||
|
Inverse bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// ImportFromOptions assigns all recognized values from a map to the current
|
||||||
|
// Style.
|
||||||
|
func (s *Style) ImportFromOptions(options map[string]interface{}) error {
|
||||||
|
assignColor := func(key string, colorField *string) error {
|
||||||
|
if c, ok := options[key]; ok {
|
||||||
|
if c, ok := c.(string); ok && isValidColorName(c) {
|
||||||
|
if c == "default" {
|
||||||
|
*colorField = ""
|
||||||
|
} else {
|
||||||
|
*colorField = c
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return fmt.Errorf("value to option '%s' must be a valid color string", key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
assignBool := func(key string, attrField *bool) error {
|
||||||
|
if b, ok := options[key]; ok {
|
||||||
|
if b, ok := b.(bool); ok {
|
||||||
|
*attrField = b
|
||||||
|
} else {
|
||||||
|
return fmt.Errorf("value to option '%s' must be a bool value", key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := assignColor("fg-color", &s.Foreground); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := assignColor("bg-color", &s.Background); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := assignBool("bold", &s.Bold); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := assignBool("dim", &s.Dim); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := assignBool("italic", &s.Italic); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := assignBool("underlined", &s.Underlined); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := assignBool("blink", &s.Blink); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := assignBool("inverse", &s.Inverse); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func isValidColorName(col string) bool {
|
||||||
|
switch col {
|
||||||
|
case
|
||||||
|
"default",
|
||||||
|
"black",
|
||||||
|
"red",
|
||||||
|
"green",
|
||||||
|
"yellow",
|
||||||
|
"blue",
|
||||||
|
"magenta",
|
||||||
|
"cyan",
|
||||||
|
"lightgray",
|
||||||
|
"gray",
|
||||||
|
"lightred",
|
||||||
|
"lightgreen",
|
||||||
|
"lightyellow",
|
||||||
|
"lightblue",
|
||||||
|
"lightmagenta",
|
||||||
|
"lightcyan",
|
||||||
|
"white":
|
||||||
|
return true
|
||||||
|
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
68
styled/text.go
Normal file
68
styled/text.go
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
package styled
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/elves/elvish/eval/vals"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Text contains of a list of styled Segments.
|
||||||
|
type Text []Segment
|
||||||
|
|
||||||
|
func (t Text) Kind() string { return "styled-text" }
|
||||||
|
|
||||||
|
// Repr returns the representation of the current Text. It is just a wrapper
|
||||||
|
// around the containing Segments.
|
||||||
|
func (t Text) Repr(indent int) string {
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
for _, s := range t {
|
||||||
|
buf.WriteString(s.Repr(indent + 1))
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("(styled %s)", buf.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t Text) IterateKeys(fn func(interface{}) bool) {
|
||||||
|
for i := 0; i < len(t); i++ {
|
||||||
|
if !fn(strconv.Itoa(i)) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Index provides access to the underlying Segments.
|
||||||
|
func (t Text) Index(k interface{}) (interface{}, error) {
|
||||||
|
index, err := vals.ConvertListIndex(k, len(t))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else if index.Slice {
|
||||||
|
return t[index.Lower:index.Upper], nil
|
||||||
|
} else {
|
||||||
|
return t[index.Lower], nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Concat implements Text+string, Text+Segment and Text+Text.
|
||||||
|
func (t Text) Concat(v interface{}) (interface{}, error) {
|
||||||
|
switch rhs := v.(type) {
|
||||||
|
case string:
|
||||||
|
return Text(append(t, Segment{Text: rhs})), nil
|
||||||
|
case *Segment:
|
||||||
|
return Text(append(t, *rhs)), nil
|
||||||
|
case *Text:
|
||||||
|
return Text(append(t, *rhs...)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, vals.ErrConcatNotImplemented
|
||||||
|
}
|
||||||
|
|
||||||
|
// RConcat implements string+Text.
|
||||||
|
func (t Text) RConcat(v interface{}) (interface{}, error) {
|
||||||
|
switch lhs := v.(type) {
|
||||||
|
case string:
|
||||||
|
return Text(append([]Segment{{Text: lhs}}, t...)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, vals.ErrConcatNotImplemented
|
||||||
|
}
|
91
styled/to_string.go
Normal file
91
styled/to_string.go
Normal file
|
@ -0,0 +1,91 @@
|
||||||
|
package styled
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// todo: Make string conversion variable to environment.
|
||||||
|
// E.g. the shell displays colors different than HTML.
|
||||||
|
func (t Text) String() string {
|
||||||
|
buf := make([]byte, 0, 64)
|
||||||
|
for _, segment := range t {
|
||||||
|
styleBuf := make([]string, 0, 8)
|
||||||
|
|
||||||
|
if segment.Bold {
|
||||||
|
styleBuf = append(styleBuf, "1")
|
||||||
|
}
|
||||||
|
if segment.Dim {
|
||||||
|
styleBuf = append(styleBuf, "2")
|
||||||
|
}
|
||||||
|
if segment.Italic {
|
||||||
|
styleBuf = append(styleBuf, "3")
|
||||||
|
}
|
||||||
|
if segment.Underlined {
|
||||||
|
styleBuf = append(styleBuf, "4")
|
||||||
|
}
|
||||||
|
if segment.Blink {
|
||||||
|
styleBuf = append(styleBuf, "5")
|
||||||
|
}
|
||||||
|
if segment.Inverse {
|
||||||
|
styleBuf = append(styleBuf, "7")
|
||||||
|
}
|
||||||
|
|
||||||
|
if segment.Foreground != "" {
|
||||||
|
if col, ok := colorTranslationTable[segment.Foreground]; ok {
|
||||||
|
styleBuf = append(styleBuf, col)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if segment.Background != "" {
|
||||||
|
if col, ok := colorTranslationTable["bg-"+segment.Background]; ok {
|
||||||
|
styleBuf = append(styleBuf, col)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(styleBuf) > 0 {
|
||||||
|
buf = append(buf, "\033["...)
|
||||||
|
buf = append(buf, strings.Join(styleBuf, ";")...)
|
||||||
|
buf = append(buf, 'm')
|
||||||
|
buf = append(buf, segment.Text...)
|
||||||
|
buf = append(buf, "\033[m"...)
|
||||||
|
} else {
|
||||||
|
buf = append(buf, segment.Text...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return string(buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
var colorTranslationTable = map[string]string{
|
||||||
|
"black": "30",
|
||||||
|
"red": "31",
|
||||||
|
"green": "32",
|
||||||
|
"yellow": "33",
|
||||||
|
"blue": "34",
|
||||||
|
"magenta": "35",
|
||||||
|
"cyan": "36",
|
||||||
|
"lightgray": "37",
|
||||||
|
"gray": "90",
|
||||||
|
"lightred": "91",
|
||||||
|
"lightgreen": "92",
|
||||||
|
"lightyellow": "93",
|
||||||
|
"lightblue": "94",
|
||||||
|
"lightmagenta": "95",
|
||||||
|
"lightcyan": "96",
|
||||||
|
"white": "97",
|
||||||
|
|
||||||
|
"bg-black": "40",
|
||||||
|
"bg-red": "41",
|
||||||
|
"bg-green": "42",
|
||||||
|
"bg-yellow": "43",
|
||||||
|
"bg-blue": "44",
|
||||||
|
"bg-magenta": "45",
|
||||||
|
"bg-cyan": "46",
|
||||||
|
"bg-lightgray": "47",
|
||||||
|
"bg-gray": "100",
|
||||||
|
"bg-lightred": "101",
|
||||||
|
"bg-lightgreen": "102",
|
||||||
|
"bg-lightyellow": "103",
|
||||||
|
"bg-lightblue": "104",
|
||||||
|
"bg-lightmagenta": "105",
|
||||||
|
"bg-lightcyan": "106",
|
||||||
|
"bg-white": "107",
|
||||||
|
}
|
91
styled/transform.go
Normal file
91
styled/transform.go
Normal file
|
@ -0,0 +1,91 @@
|
||||||
|
package styled
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// FindTransformer looks up a transformer name and if successful returns a
|
||||||
|
// function that can be used to transform a styled Segment.
|
||||||
|
func FindTransformer(transformerName string) func(Segment) Segment {
|
||||||
|
var innerTransformer func(*Segment)
|
||||||
|
|
||||||
|
switch {
|
||||||
|
// Catch special colors early
|
||||||
|
case transformerName == "default":
|
||||||
|
innerTransformer = func(s *Segment) { s.Foreground = "" }
|
||||||
|
case transformerName == "bg-default":
|
||||||
|
innerTransformer = func(s *Segment) { s.Background = "" }
|
||||||
|
|
||||||
|
case strings.HasPrefix(transformerName, "bg-"):
|
||||||
|
innerTransformer = buildColorTransformer(strings.TrimPrefix(transformerName, "bg-"), false)
|
||||||
|
case strings.HasPrefix(transformerName, "no-"):
|
||||||
|
innerTransformer = buildBoolTransformer(strings.TrimPrefix(transformerName, "no-"), false, false)
|
||||||
|
case strings.HasPrefix(transformerName, "toggle-"):
|
||||||
|
innerTransformer = buildBoolTransformer(strings.TrimPrefix(transformerName, "toggle-"), false, true)
|
||||||
|
|
||||||
|
default:
|
||||||
|
innerTransformer = buildColorTransformer(transformerName, true)
|
||||||
|
|
||||||
|
if innerTransformer == nil {
|
||||||
|
innerTransformer = buildBoolTransformer(transformerName, true, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if innerTransformer == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return func(segment Segment) Segment {
|
||||||
|
innerTransformer(&segment)
|
||||||
|
return segment
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildColorTransformer(transformerName string, setForeground bool) func(*Segment) {
|
||||||
|
if isValidColorName(transformerName) {
|
||||||
|
if setForeground {
|
||||||
|
return func(s *Segment) { s.Foreground = transformerName }
|
||||||
|
} else {
|
||||||
|
return func(s *Segment) { s.Background = transformerName }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildBoolTransformer(transformerName string, val, toggle bool) func(*Segment) {
|
||||||
|
switch transformerName {
|
||||||
|
case "bold":
|
||||||
|
if toggle {
|
||||||
|
return func(s *Segment) { s.Bold = !s.Bold }
|
||||||
|
}
|
||||||
|
return func(s *Segment) { s.Bold = val }
|
||||||
|
case "dim":
|
||||||
|
if toggle {
|
||||||
|
return func(s *Segment) { s.Dim = !s.Dim }
|
||||||
|
}
|
||||||
|
return func(s *Segment) { s.Dim = val }
|
||||||
|
case "italic":
|
||||||
|
if toggle {
|
||||||
|
return func(s *Segment) { s.Italic = !s.Italic }
|
||||||
|
}
|
||||||
|
return func(s *Segment) { s.Italic = val }
|
||||||
|
case "underlined":
|
||||||
|
if toggle {
|
||||||
|
return func(s *Segment) { s.Underlined = !s.Underlined }
|
||||||
|
}
|
||||||
|
return func(s *Segment) { s.Underlined = val }
|
||||||
|
case "blink":
|
||||||
|
if toggle {
|
||||||
|
return func(s *Segment) { s.Blink = !s.Blink }
|
||||||
|
}
|
||||||
|
return func(s *Segment) { s.Blink = val }
|
||||||
|
case "inverse":
|
||||||
|
if toggle {
|
||||||
|
return func(s *Segment) { s.Inverse = !s.Inverse }
|
||||||
|
}
|
||||||
|
return func(s *Segment) { s.Inverse = val }
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user