Basic wrap is working

This commit is contained in:
Kovid Goyal 2022-09-02 23:18:33 +05:30
parent ef5f0025e1
commit 1ac7d9c10d
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
2 changed files with 173 additions and 76 deletions

View File

@ -4,7 +4,6 @@ package style
import (
"fmt"
"io"
"strconv"
"strings"
"unicode"
@ -64,40 +63,67 @@ func (self *sgr_state) reset() {
*self = sgr_state{}
}
func (self sgr_state) as_sgr() string {
func (self sgr_state) as_sgr(for_close bool) 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)
if for_close {
if self.bold || self.dim {
ans = append(ans, "22")
}
if self.italic {
ans = append(ans, "23")
}
if self.reverse {
ans = append(ans, "27")
}
if self.strikethrough {
ans = append(ans, "29")
}
if self.underline_style != no_underline && self.underline_style != nil_underline {
ans = append(ans, "4:0")
}
if self.fg.number != 0 {
ans = append(ans, "39")
}
if self.bg.number != 0 {
ans = append(ans, "49")
}
if self.uc.number != 0 {
ans = append(ans, "59")
}
} else {
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()
func (self sgr_state) as_escape_codes(for_close bool) string {
q := self.as_sgr(for_close)
if q == "" {
return q
}
@ -116,11 +142,11 @@ func (self *sgr_state) apply_csi(raw string) {
nums := make([]int, 0, 8)
for _, part := range parts {
subparts := strings.Split(part, ":")
nums = nums[:len(subparts)]
for i, b := range subparts {
nums = nums[:0]
for _, b := range subparts {
q, err := strconv.Atoi(b)
if err != nil {
nums[i] = q
if err == nil {
nums = append(nums, q)
}
}
if len(nums) == 0 {
@ -210,93 +236,164 @@ func (self hyperlink_state) reset() {
self.url = ""
}
func (self hyperlink_state) as_escape_codes() string {
func (self hyperlink_state) as_escape_codes(for_close bool) string {
if self.id == "" && self.url == "" {
return ""
}
if for_close {
return "\x1b]8;;\x1b\\"
}
return fmt.Sprintf("\x1b]8;%s;%s\x1b\\", self.id, self.url)
}
type line_builder struct {
buf strings.Builder
last_text_pos, cursor_pos int
}
func (self *line_builder) reset() string {
ans := self.buf.String()
if len(ans) > self.last_text_pos {
ans = ans[:self.last_text_pos]
}
sz := self.buf.Len()
self.buf.Reset()
self.last_text_pos = 0
self.cursor_pos = 0
if sz > 1024 {
self.buf.Grow(sz)
} else {
self.buf.Grow(1024)
}
return ans
}
func (self *line_builder) has_space_for_width(w, max_width int) bool {
return w+self.cursor_pos <= max_width
}
func (self *line_builder) add_char(ch rune) {
self.buf.WriteRune(ch)
self.last_text_pos = self.buf.Len()
self.cursor_pos += wcswidth.Runewidth(ch)
}
func (self *line_builder) add_word(word string, width int) {
self.buf.WriteString(word)
self.last_text_pos = self.buf.Len()
self.cursor_pos += width
}
func (self *line_builder) add_escape_code(code string) {
self.buf.WriteString(code)
}
func (self *line_builder) add_escape_code2(prefix string, body []byte, suffix string) {
self.buf.WriteString(prefix)
self.buf.Write(body)
self.buf.WriteString(suffix)
}
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
current_line line_builder
lines []string
}
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) newline_prefix() {
self.current_line.add_escape_code(self.sgr.as_escape_codes(true))
self.current_line.add_escape_code(self.hyperlink.as_escape_codes(true))
self.current_line.add_word(self.indent, self.indent_width)
self.current_line.add_escape_code(self.sgr.as_escape_codes(false))
self.current_line.add_escape_code(self.hyperlink.as_escape_codes(false))
}
func (self *wrapper) print_word(ch rune) {
func (self *wrapper) end_current_line() {
line := self.current_line.reset()
line = strings.TrimRightFunc(line, unicode.IsSpace)
if strings.HasSuffix(line, self.indent) && wcswidth.Stringwidth(line) == self.indent_width {
line = line[:len(line)-len(self.indent)]
}
self.lines = append(self.lines, line)
self.newline_prefix()
}
func (self *wrapper) print_word() {
w := wcswidth.Stringwidth(self.current_word.String())
if self.x+w > self.width {
self.print_newline()
if !self.current_line.has_space_for_width(w, self.width) {
self.end_current_line()
s := strings.TrimSpace(self.current_word.String())
self.current_word.Reset()
self.current_word.WriteString(s)
w = wcswidth.Stringwidth(s)
}
fmt.Fprint(self.output, self.current_word.String())
self.current_line.add_word(self.current_word.String(), w)
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()
self.print_word()
self.end_current_line()
} else if self.current_word.Len() != 0 && ch != 0xa0 && unicode.IsSpace(ch) {
self.print_word(ch)
self.print_word()
self.current_line.add_char(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)
self.current_line.add_escape_code2("\x1b[", raw, "")
return nil
}
func (self *wrapper) handle_osc(raw []byte) error {
self.hyperlink.apply_osc(utils.UnsafeBytesToString(raw))
self.output.Write(raw)
self.current_line.add_escape_code2("\x1b]", raw, "\x1b\\")
return nil
}
func (self *wrapper) wrap_text(text string) {
self.x = self.indent_width
fmt.Fprint(self.output, self.indent)
func (self *wrapper) wrap_text(text string) string {
if text == "" {
return text
}
self.current_line.reset()
self.current_word.Reset()
self.lines = self.lines[:0]
self.current_line.add_word(self.indent, self.indent_width)
self.ep.ParseString(text)
if self.current_word.Len() > 0 {
self.print_word(0)
self.print_word()
}
if len(text) > 0 {
self.print_newline()
self.end_current_line()
last_line := self.current_line.reset()
self.newline_prefix()
if last_line == self.current_line.reset() {
last_line = ""
}
self.lines = append(self.lines, last_line)
return strings.Join(self.lines, "\n")
}
func new_wrapper(output io.Writer, indent string, width int) *wrapper {
ans := wrapper{output: output, indent: indent, width: width, indent_width: wcswidth.Stringwidth(indent)}
func new_wrapper(indent string, width int) *wrapper {
ans := wrapper{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
ans.lines = make([]string, 0, 32)
return &ans
}
func WrapText(text string, output io.Writer, indent string, width int) {
w := new_wrapper(output, indent, width)
w.wrap_text(text)
return
func WrapText(text string, indent string, width int) string {
w := new_wrapper(indent, width)
return w.wrap_text(text)
}

View File

@ -8,19 +8,19 @@ import (
)
func TestFormatWithIndent(t *testing.T) {
var output strings.Builder
indent := "__"
screen_width := 11
run := func(text string, expected ...string) {
output.Reset()
q := indent + strings.Join(expected, "\n"+indent) + "\n"
WrapText(text, &output, indent, screen_width)
if output.String() != q {
t.Fatalf("expected != actual: %#v != %#v", q, output.String())
tx := func(text string, expected ...string) {
q := indent + strings.Join(expected, "") + "\n"
actual := WrapText(text, indent, screen_width)
if actual != q {
t.Fatalf("%#v\nexpected: %#v\nactual: %#v", text, q, actual)
}
}
run("testing \x1b[31mstyled\x1b[m", "testing ", "\x1b[31mstyled\x1b[m")
run("testing\n\ntwo", "testing", "", "two")
run("testing\n \ntwo", "testing", "", "two")
tx("testing\n\ntwo", "testing\n\n__two")
tx("testing\n \ntwo", "testing\n\n__two")
tx("123456 \x1b[31m789a", "123456\n\x1b[39m__\x1b[31m789a")
tx("12 \x1b[31m789 abcd", "12 \x1b[31m789\n\x1b[39m__\x1b[31mabcd")
}