Basic wrap is working
This commit is contained in:
parent
ef5f0025e1
commit
1ac7d9c10d
@ -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)
|
||||
}
|
||||
|
||||
@ -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")
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user