elvish/pkg/ui/style.go
Kurtis Rader 4b4726b9a6 Make ttyshots scripted and reproducible
The main benefits of this change are:

1) It uses a hermetic "home" directory with a known command and location
   history. Which means it no longer depends on the interactive history
   and directory layout of the person creating the ttyshot. Which also
   means it no longer leaks the private history of anyone creating a
   ttyshot. This produces reproducible results when updating ttyshots.

2) The user no longer has to augment the ttyshot by manually adding the
   output of the commands to the generated HTML file. A process that is
   error prone. The output of the commands that generate the ttyshot is
   now captured and automatically included in the resulting HTML.

3) It makes it trivial to recreate every ttyshot. Simply execute these
   commands:

   ```
   make ttyshot
   for f [website/ttyshot/**.spec] { put $f; ./ttyshot $f }
   ```

4) It makes it easy to introduce new "ttyshot" images by creating a
   shell session "spec" file. This makes it easy to replace the existing
   "```elvish-transcript...```" examples with ttyshots in order to
   ensure a consistent representation and visual consistency with the
   transcripts that are currently generated as ttyshots.

The downside of this change is the introduction of a dependency on the
Tmux application. But that seems reasonable since Tmux is a mature
application available on Linux, macOS, BSD, and probably every other
UNIX like OS we care about. Note that generating the Elvish
documentation already depends on similar apps such as Pandoc.

Related #1459
2022-06-19 10:51:29 +01:00

106 lines
2.1 KiB
Go

package ui
import (
"fmt"
"strings"
)
// Style specifies how something (mostly a string) shall be displayed.
type Style struct {
Foreground Color
Background Color
Bold bool
Dim bool
Italic bool
Underlined bool
Blink bool
Inverse bool
}
// SGRValues returns an array of the individual SGR values for the style.
func (s Style) SGRValues() []string {
var sgr []string
addIf := func(b bool, code string) {
if b {
sgr = append(sgr, code)
}
}
addIf(s.Bold, "1")
addIf(s.Dim, "2")
addIf(s.Italic, "3")
addIf(s.Underlined, "4")
addIf(s.Blink, "5")
addIf(s.Inverse, "7")
if s.Foreground != nil {
sgr = append(sgr, s.Foreground.fgSGR())
}
if s.Background != nil {
sgr = append(sgr, s.Background.bgSGR())
}
return sgr
}
// SGR returns, for the Style, a string that can be included in an ANSI X3.64 SGR sequence.
func (s Style) SGR() string {
return strings.Join(s.SGRValues(), ";")
}
// MergeFromOptions merges all recognized values from a map to the current
// Style.
func (s *Style) MergeFromOptions(options map[string]any) error {
assignColor := func(val any, colorField *Color) string {
if val == "default" {
*colorField = nil
return ""
} else if s, ok := val.(string); ok {
color := parseColor(s)
if color != nil {
*colorField = color
return ""
}
}
return "valid color string"
}
assignBool := func(val any, attrField *bool) string {
if b, ok := val.(bool); ok {
*attrField = b
} else {
return "bool value"
}
return ""
}
for k, v := range options {
var need string
switch k {
case "fg-color":
need = assignColor(v, &s.Foreground)
case "bg-color":
need = assignColor(v, &s.Background)
case "bold":
need = assignBool(v, &s.Bold)
case "dim":
need = assignBool(v, &s.Dim)
case "italic":
need = assignBool(v, &s.Italic)
case "underlined":
need = assignBool(v, &s.Underlined)
case "blink":
need = assignBool(v, &s.Blink)
case "inverse":
need = assignBool(v, &s.Inverse)
default:
return fmt.Errorf("unrecognized option '%s'", k)
}
if need != "" {
return fmt.Errorf("value for option '%s' must be a %s", k, need)
}
}
return nil
}