From 91c61478dd5ea0b804f57575d4e1f7aff7bec01c Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 27 Aug 2022 13:40:48 +0530 Subject: [PATCH] Start work on easily generating ANSI formatted strings --- go.mod | 1 + go.sum | 2 + kitty/rgb.py | 2 +- tools/utils/style/wrapper.go | 369 +++++++++++++++++++++++++++++++++++ 4 files changed, 373 insertions(+), 1 deletion(-) create mode 100644 tools/utils/style/wrapper.go diff --git a/go.mod b/go.mod index 2d7a48d4a..508c95d44 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.19 require ( github.com/ALTree/bigfloat v0.0.0-20220102081255-38c8b72a9924 + github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 github.com/google/uuid v1.3.0 github.com/jamesruan/go-rfc1924 v0.0.0-20170108144916-2767ca7c638f github.com/seancfoley/ipaddress-go v1.2.1 diff --git a/go.sum b/go.sum index b09dd5f34..4c313c810 100644 --- a/go.sum +++ b/go.sum @@ -1,6 +1,8 @@ github.com/ALTree/bigfloat v0.0.0-20220102081255-38c8b72a9924 h1:DG4UyTVIujioxwJc8Zj8Nabz1L1wTgQ/xNBSQDfdP3I= github.com/ALTree/bigfloat v0.0.0-20220102081255-38c8b72a9924/go.mod h1:+NaH2gLeY6RPBPPQf4aRotPPStg+eXc8f9ZaE4vRfD4= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= diff --git a/kitty/rgb.py b/kitty/rgb.py index 3f81c1950..6dffad49e 100644 --- a/kitty/rgb.py +++ b/kitty/rgb.py @@ -66,7 +66,7 @@ def to_color(raw: str, validate: bool = False) -> Optional[Color]: with suppress(Exception): if raw.startswith('#'): val = parse_sharp(raw[1:]) - elif raw[:4].lower() == 'rgb:': + elif raw.startswith('rgb:'): val = parse_rgb(raw[4:]) if val is None and validate: raise ValueError(f'Invalid color name: {raw}') diff --git a/tools/utils/style/wrapper.go b/tools/utils/style/wrapper.go new file mode 100644 index 000000000..e4af08664 --- /dev/null +++ b/tools/utils/style/wrapper.go @@ -0,0 +1,369 @@ +// License: GPLv3 Copyright: 2022, Kovid Goyal, + +package style + +import ( + "fmt" + "strconv" + "strings" + + "github.com/google/shlex" +) + +type escape_code interface { + prefix() string + suffix() string + is_empty() bool +} + +// bool values {{{ +type bool_value struct { + is_set, val bool +} + +func (self bool_value) as_sgr(start, end string, prefix, suffix []string) ([]string, []string) { + if self.is_set { + if !self.val { + start, end = end, start + } + prefix = append(prefix, start) + suffix = append(suffix, start) + } + return prefix, suffix +} + +func (self *bool_value) from_string(raw string) bool { + switch strings.ToLower(raw) { + case "y", "yes", "true", "1": + self.is_set = true + self.val = true + return true + case "n", "no", "false", "0": + self.is_set = true + self.val = false + return true + default: + return false + } +} + +// }}} + +// color values {{{ +type RGBA struct { + Red, Green, Blue, Inverse_alpha uint8 +} + +func (self *RGBA) parse_rgb_strings(r string, g string, b string) bool { + var rv, gv, bv uint64 + var err error + if rv, err = strconv.ParseUint(r, 16, 8); err != nil { + return false + } + if gv, err = strconv.ParseUint(g, 16, 8); err != nil { + return false + } + if bv, err = strconv.ParseUint(b, 16, 8); err != nil { + return false + } + self.Red, self.Green, self.Blue = uint8(rv), uint8(gv), uint8(bv) + return true +} + +type color_type struct { + is_numbered bool + val RGBA +} + +func (self color_type) as_sgr(number_base int, prefix, suffix []string) ([]string, []string) { + suffix = append(suffix, strconv.Itoa(number_base+9)) + if self.is_numbered { + num := int(self.val.Red) + if num < 16 { + if num > 7 { + number_base += 60 + num -= 8 + } + prefix = append(prefix, strconv.Itoa(number_base+num)) + } else { + prefix = append(prefix, fmt.Sprintf("%d:5:%d", number_base+8, num)) + } + } else { + prefix = append(prefix, fmt.Sprintf("%d:2:%d", number_base+8, self.val.Red, self.val.Green, self.val.Blue)) + } + return prefix, suffix +} + +type color_value struct { + is_set bool + val color_type +} + +func parse_sharp(color string) (ans RGBA, err error) { + if len(color)%3 != 0 { + return RGBA{}, fmt.Errorf("Not a valid color: #%s", color) + } + part_size := len(color) / 3 + r, g, b := color[:part_size], color[part_size:2*part_size], color[part_size*2:] + if part_size == 1 { + r += r + g += g + b += b + } + if !ans.parse_rgb_strings(r, g, b) { + err = fmt.Errorf("Not a valid color: #%s", color) + } + return +} + +func parse_rgb(color string) (ans RGBA, err error) { + colors := strings.Split(color, "/") + if len(colors) == 3 && ans.parse_rgb_strings(colors[0], colors[1], colors[2]) { + return + } + err = fmt.Errorf("Not a valid RGB color: %#v", color) + return +} + +func ParseColor(color string) (RGBA, error) { + raw := strings.TrimSpace(strings.ToLower(color)) + if val, ok := ColorNames[raw]; ok { + return val, nil + } + if strings.HasPrefix(raw, "#") { + return parse_sharp(raw[1:]) + } + if strings.HasPrefix(raw, "rgb:") { + return parse_rgb(raw[4:]) + } + return RGBA{}, fmt.Errorf("Not a valid color name: %#v", color) +} + +var named_colors = map[string]uint8{ + "black": 0, "red": 1, "green": 2, "yellow": 3, "blue": 4, "magenta": 5, "cyan": 6, "gray": 7, "white": 7, + + "hi-black": 8, "hi-red": 9, "hi-green": 10, "hi-yellow": 11, "hi-blue": 12, "hi-magenta": 13, "hi-cyan": 14, "hi-gray": 15, "hi-white": 15, + + "bright-black": 8, "bright-red": 9, "bright-green": 10, "bright-yellow": 11, "bright-blue": 12, "bright-magenta": 13, "bright-cyan": 14, "bright-gray": 15, "bright-white": 15, + + "intense-black": 8, "intense-red": 9, "intense-green": 10, "intense-yellow": 11, "intense-blue": 12, "intense-magenta": 13, "intense-cyan": 14, "intense-gray": 15, "intense-white": 15, +} + +func ColorNumberAsRGB(n uint8) (ans RGBA) { + val := ColorTable[n] + ans.Red = uint8((val >> 16) & 0xff) + ans.Green = uint8((val >> 8) & 0xff) + ans.Blue = uint8(val & 0xff) + return +} + +func (self *color_value) from_string(raw string, allow_numbered bool) bool { + if n, ok := named_colors[raw]; ok { + self.is_set = true + if allow_numbered { + self.val = color_type{val: RGBA{Red: n}, is_numbered: true} + } else { + self.val = color_type{val: ColorNumberAsRGB(n)} + } + return true + } + a, err := strconv.Atoi(raw) + if err == nil && 0 <= a && a <= 255 { + self.is_set = true + if allow_numbered { + self.val = color_type{val: RGBA{Red: uint8(a)}, is_numbered: true} + } else { + self.val = color_type{val: ColorNumberAsRGB(uint8(a))} + } + return true + } + c, err := ParseColor(raw) + if err != nil { + return false + } + self.is_set = true + self.val = color_type{val: c} + return true +} + +func (self color_value) as_sgr(number_base int, prefix, suffix []string) ([]string, []string) { + if self.is_set { + prefix, suffix = self.val.as_sgr(number_base, prefix, suffix) + } + return prefix, suffix +} + +// }}} + +// underline values {{{ +type underline_style uint8 + +const ( + no_underline underline_style = 0 + straight_underline underline_style = 1 + double_underline underline_style = 2 + curly_underline underline_style = 3 + dotted_underline underline_style = 4 + dashed_underline underline_style = 5 + + nil_underline underline_style = 255 +) + +type underline_value struct { + is_set bool + style underline_style +} + +func (self *underline_value) from_string(val string) bool { + ans := nil_underline + switch val { + case "true", "yes", "y", "straight", "single": + ans = straight_underline + case "false", "no", "n", "none": + ans = no_underline + case "double": + ans = double_underline + case "curly": + ans = curly_underline + case "dotted": + ans = dotted_underline + case "dashed": + ans = dashed_underline + } + if ans != nil_underline { + return false + } + self.is_set = true + self.style = ans + return true +} + +func (self underline_value) as_sgr(prefix, suffix []string) ([]string, []string) { + if self.is_set { + s, e := "0", "0" + if self.style != no_underline { + s = strconv.Itoa(int(self.style)) + } + prefix = append(prefix, s) + suffix = append(suffix, e) + } + return prefix, suffix +} + +// }}} + +type sgr_code struct { + bold, italic, reverse, dim bool_value + fg, bg, uc color_value + underline underline_value + + _prefix, _suffix string +} + +func (self sgr_code) prefix() string { + return self._prefix +} + +func (self sgr_code) suffix() string { + return self._suffix +} + +func (self sgr_code) is_empty() bool { + return self._prefix == "" +} + +func (self *sgr_code) update() { + p := make([]string, 0, 1) + s := make([]string, 0, 1) + p, s = self.bold.as_sgr("1", "22", p, s) + p, s = self.italic.as_sgr("3", "23", p, s) + p, s = self.reverse.as_sgr("7", "27", p, s) + p, s = self.dim.as_sgr("2", "22", p, s) + p, s = self.underline.as_sgr(p, s) + p, s = self.fg.as_sgr(30, p, s) + p, s = self.bg.as_sgr(30, p, s) + p, s = self.uc.as_sgr(50, p, s) + self._prefix = "\x1b[" + strings.Join(p, ";") + "m" + self._suffix = "\x1b[" + strings.Join(s, ";") + "m" +} + +func parse_spec(spec string) []escape_code { + ans := make([]escape_code, 0, 1) + sgr := sgr_code{} + sparts, _ := shlex.Split(spec) + for _, p := range sparts { + parts := strings.SplitN(p, "=", 2) + key := parts[0] + val := "" + if len(parts) == 1 { + val = "true" + } else { + val = parts[1] + } + switch key { + case "fg": + sgr.fg.from_string(val, true) + case "bg": + sgr.bg.from_string(val, true) + case "bold", "b": + sgr.bold.from_string(val) + case "italic", "i": + sgr.italic.from_string(val) + case "reverse": + sgr.reverse.from_string(val) + case "dim": + sgr.dim.from_string(val) + case "underline", "u": + sgr.underline.from_string(val) + case "ucol", "underline_color", "uc": + sgr.uc.from_string(val, false) + } + } + sgr.update() + if !sgr.is_empty() { + ans = append(ans, &sgr) + } + return ans +} + +var parsed_spec_cache = make(map[string][]escape_code) + +func cached_parse_spec(spec string) []escape_code { + if val, ok := parsed_spec_cache[spec]; ok { + return val + } + ans := parse_spec(spec) + parsed_spec_cache[spec] = ans + return ans +} + +func prefix_for_spec(spec string) string { + sb := strings.Builder{} + for _, ec := range cached_parse_spec(spec) { + sb.WriteString(ec.prefix()) + } + return sb.String() +} + +func suffix_for_spec(spec string) string { + sb := strings.Builder{} + for _, ec := range cached_parse_spec(spec) { + sb.WriteString(ec.suffix()) + } + return sb.String() +} + +func Styler(spec string) func(args ...interface{}) string { + p := prefix_for_spec(spec) + s := suffix_for_spec(spec) + + return func(args ...interface{}) string { + body := fmt.Sprint(args...) + b := strings.Builder{} + b.Grow(len(p) + len(body) + len(s)) + b.WriteString(p) + b.WriteString(body) + b.WriteString(s) + return b.String() + } +}