From ef5f0025e1afe048d9e8f1dc53bc48c97a619528 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Fri, 2 Sep 2022 18:09:03 +0530 Subject: [PATCH] Start work on more sophisticated indent+wrap implementation --- tools/utils/style/indent-and-wrap.go | 302 ++++++++++++++++++ .../style/indent-and-wrap_test.go} | 4 +- 2 files changed, 304 insertions(+), 2 deletions(-) create mode 100644 tools/utils/style/indent-and-wrap.go rename tools/{cli/infrastructure_test.go => utils/style/indent-and-wrap_test.go} (89%) diff --git a/tools/utils/style/indent-and-wrap.go b/tools/utils/style/indent-and-wrap.go new file mode 100644 index 000000000..ce7bb0fbc --- /dev/null +++ b/tools/utils/style/indent-and-wrap.go @@ -0,0 +1,302 @@ +// License: GPLv3 Copyright: 2022, Kovid Goyal, + +package style + +import ( + "fmt" + "io" + "strconv" + "strings" + "unicode" + + "kitty/tools/utils" + "kitty/tools/wcswidth" +) + +type sgr_color struct { + number int + color RGBA +} + +func (self sgr_color) as_sgr(base int) string { + if self.number == 0 { + return "" + } + if self.number > 0 { + n := self.number - 1 + if n <= 15 && (base == 30 || base == 40) { + if n <= 7 { + return strconv.Itoa(base + n) + } + return strconv.Itoa(base + 60 + n - 7) + } + return fmt.Sprintf("%d:5:%d", base+8, n) + } + return fmt.Sprintf("%d:2:%d:%d:%d", base+8, self.color.Red, self.color.Green, self.color.Blue) +} + +func (self *sgr_color) from_extended(nums []int) bool { + switch nums[0] { + case 5: + if len(nums) > 1 { + self.number = 1 + nums[1] + return true + } + case 2: + if len(nums) > 3 { + self.number = -1 + self.color.Red = uint8(nums[1]) + self.color.Green = uint8(nums[2]) + self.color.Blue = uint8(nums[3]) + return true + } + } + return false +} + +type sgr_state struct { + italic, reverse, bold, dim, strikethrough bool + underline_style underline_style + fg, bg, uc sgr_color +} + +func (self *sgr_state) reset() { + *self = sgr_state{} +} + +func (self sgr_state) as_sgr() string { + ans := make([]string, 0, 4) + if self.bold { + ans = append(ans, "1") + } + if self.dim { + ans = append(ans, "2") + } + if self.italic { + ans = append(ans, "3") + } + if self.reverse { + ans = append(ans, "7") + } + if self.strikethrough { + ans = append(ans, "9") + } + if self.underline_style != no_underline && self.underline_style != nil_underline { + ans = append(ans, fmt.Sprintf("4:%d", self.underline_style)) + } + if q := self.fg.as_sgr(30); q != "" { + ans = append(ans, q) + } + if q := self.bg.as_sgr(40); q != "" { + ans = append(ans, q) + } + if q := self.uc.as_sgr(50); q != "" { + ans = append(ans, q) + } + return strings.Join(ans, ";") +} + +func (self sgr_state) as_escape_codes() string { + q := self.as_sgr() + if q == "" { + return q + } + return fmt.Sprintf("\x1b[%sm", q) +} + +func (self *sgr_state) apply_csi(raw string) { + if !strings.HasSuffix(raw, "m") { + return + } + raw = raw[:len(raw)-1] + if raw == "" { + raw = "0" + } + parts := strings.Split(raw, ";") + nums := make([]int, 0, 8) + for _, part := range parts { + subparts := strings.Split(part, ":") + nums = nums[:len(subparts)] + for i, b := range subparts { + q, err := strconv.Atoi(b) + if err != nil { + nums[i] = q + } + } + if len(nums) == 0 { + continue + } + switch nums[0] { + case 0: + self.reset() + case 1: + self.dim, self.bold = false, true + case 2: + self.dim, self.bold = true, false + case 22: + self.dim, self.bold = false, false + case 3: + self.italic = true + case 23: + self.italic = false + case 7: + self.reverse = true + case 27: + self.reverse = false + case 9: + self.strikethrough = true + case 29: + self.strikethrough = false + case 24: + self.underline_style = no_underline + case 4: + us := 1 + if len(nums) > 1 { + us = nums[1] + } + switch us { + case 0: + self.underline_style = no_underline + case 1: + self.underline_style = straight_underline + case 2: + self.underline_style = double_underline + case 3: + self.underline_style = curly_underline + case 4: + self.underline_style = dotted_underline + case 5: + self.underline_style = dashed_underline + } + case 30, 31, 32, 33, 34, 35, 36, 37: + self.fg.number = nums[0] + 1 - 30 + case 90, 91, 92, 93, 94, 95, 96, 97: + self.fg.number = nums[0] + 1 - 82 + case 38: + self.fg.from_extended(nums[1:]) + case 39: + self.fg.number = 0 + case 40, 41, 42, 43, 44, 45, 46, 47: + self.bg.number = nums[0] + 1 - 40 + case 100, 101, 102, 103, 104, 105, 106, 107: + self.bg.number = nums[0] + 1 - 92 + case 48: + self.bg.from_extended(nums[1:]) + case 49: + self.bg.number = 0 + case 58: + self.uc.from_extended(nums[1:]) + case 59: + self.uc.number = 0 + } + } +} + +type hyperlink_state struct { + id, url string +} + +func (self *hyperlink_state) apply_osc(raw string) { + parts := strings.SplitN(raw, ";", 3) + if len(parts) != 3 || parts[0] != "8" { + return + } + self.id = parts[1] + self.url = parts[2] +} + +func (self hyperlink_state) reset() { + self.id = "" + self.url = "" +} + +func (self hyperlink_state) as_escape_codes() string { + if self.id == "" && self.url == "" { + return "" + } + return fmt.Sprintf("\x1b]8;%s;%s\x1b\\", self.id, self.url) +} + +type wrapper struct { + ep wcswidth.EscapeCodeParser + output io.Writer + indent string + width, indent_width int + + sgr sgr_state + hyperlink hyperlink_state + current_word strings.Builder + x int +} + +func (self *wrapper) print_newline() { + fmt.Fprint(self.output, "\x1b[m\x1b]8;;\x1b\\\n", self.indent, self.sgr.as_escape_codes(), self.hyperlink.as_escape_codes()) + self.x = self.indent_width +} + +func (self *wrapper) print_word(ch rune) { + w := wcswidth.Stringwidth(self.current_word.String()) + if self.x+w > self.width { + self.print_newline() + s := strings.TrimSpace(self.current_word.String()) + self.current_word.Reset() + self.current_word.WriteString(s) + } + fmt.Fprint(self.output, self.current_word.String()) + self.current_word.Reset() + if ch > 0 { + self.current_word.WriteRune(ch) + } + self.x += w +} + +func (self *wrapper) handle_rune(ch rune) error { + if ch == '\n' { + self.print_newline() + } else if self.current_word.Len() != 0 && ch != 0xa0 && unicode.IsSpace(ch) { + self.print_word(ch) + } else { + self.current_word.WriteRune(ch) + } + + io.WriteString(self.output, string(ch)) + return nil +} + +func (self *wrapper) handle_csi(raw []byte) error { + self.sgr.apply_csi(utils.UnsafeBytesToString(raw)) + self.output.Write(raw) + return nil +} + +func (self *wrapper) handle_osc(raw []byte) error { + self.hyperlink.apply_osc(utils.UnsafeBytesToString(raw)) + self.output.Write(raw) + return nil +} + +func (self *wrapper) wrap_text(text string) { + self.x = self.indent_width + fmt.Fprint(self.output, self.indent) + self.ep.ParseString(text) + if self.current_word.Len() > 0 { + self.print_word(0) + } + if len(text) > 0 { + self.print_newline() + } +} + +func new_wrapper(output io.Writer, indent string, width int) *wrapper { + ans := wrapper{output: output, indent: indent, width: width, indent_width: wcswidth.Stringwidth(indent)} + ans.ep.HandleRune = ans.handle_rune + ans.ep.HandleCSI = ans.handle_csi + ans.ep.HandleOSC = ans.handle_osc + return &ans +} + +func WrapText(text string, output io.Writer, indent string, width int) { + w := new_wrapper(output, indent, width) + w.wrap_text(text) + return +} diff --git a/tools/cli/infrastructure_test.go b/tools/utils/style/indent-and-wrap_test.go similarity index 89% rename from tools/cli/infrastructure_test.go rename to tools/utils/style/indent-and-wrap_test.go index 7fe78ae22..b825bcb35 100644 --- a/tools/cli/infrastructure_test.go +++ b/tools/utils/style/indent-and-wrap_test.go @@ -1,6 +1,6 @@ // License: GPLv3 Copyright: 2022, Kovid Goyal, -package cli +package style import ( "strings" @@ -15,7 +15,7 @@ func TestFormatWithIndent(t *testing.T) { run := func(text string, expected ...string) { output.Reset() q := indent + strings.Join(expected, "\n"+indent) + "\n" - format_with_indent(&output, text, indent, screen_width) + WrapText(text, &output, indent, screen_width) if output.String() != q { t.Fatalf("expected != actual: %#v != %#v", q, output.String()) }