Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: can solve character that need 2 width now. #3

Merged
merged 1 commit into from
Sep 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ Write a string into VirtualTerm, you will know what would your string be like if


## Now Support 😎

### Character Encoding
* ASCII
* UTF-8

### Control Characters
* `\b` backspace
* `\r` carriage return
* `\n` feed line
Expand Down
206 changes: 150 additions & 56 deletions term.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ import (
"strings"
)

var ErrNotSupportedCSI = errors.New("this csi not supported")
var INF = math.MaxInt - 1
var ErrCannotHandle = errors.New("this csi is not supported or syntax error")
var ErrNonDetermistics = errors.New("non-deterministic")

// VirtualTerm this is created to simulate a terminal,handle the special character such as '\r','\b', "\033[1D".
// For example: if you input "cute\rhat", the result of String() would be "hate"
Expand All @@ -21,6 +23,9 @@ type VirtualTerm struct {
// the content of virtual terminal
content [][]rune

// xOffset is the offset on X.
xOffset int

// silence will shut down the log.By default, it is true
silence bool
}
Expand Down Expand Up @@ -68,7 +73,7 @@ func (vt *VirtualTerm) handleCSI(csi string) error {
// an example \033[A
// csi should not be shorter than 3
if len(csi) < 3 {
return fmt.Errorf("too short, %w", ErrNotSupportedCSI)
return fmt.Errorf("too short, %w", ErrCannotHandle)
}
param := csi[2 : len(csi)-1]
// final byte is a single character
Expand All @@ -78,97 +83,141 @@ func (vt *VirtualTerm) handleCSI(csi string) error {
case 'A':
// move the cursor up
var cnt int
cnt, err = strconv.Atoi(param)
if len(param) == 0 {
cnt = 1
} else {
cnt, err = strconv.Atoi(param)
}
if err != nil {
return fmt.Errorf("%w:ESC[#A param may be wrong", err)
}
vt.cursorMove(0, -cnt)
case 'B':
// move the cursor down
var cnt int
cnt, err = strconv.Atoi(param)
if len(param) == 0 {
cnt = 1
} else {
cnt, err = strconv.Atoi(param)
}
if err != nil {
return fmt.Errorf("%w:ESC[#B param may be wrong", err)
}
vt.cursorMove(0, cnt)
case 'C':
// move the cursor right
var cnt int
cnt, err = strconv.Atoi(param)
if len(param) == 0 {
cnt = 1
} else {
cnt, err = strconv.Atoi(param)
}
if err != nil {
return fmt.Errorf("%w:ESC[#C param may be wrong", err)
}
vt.cursorMove(cnt, 0)
case 'D':
// move the cursor left
var cnt int
cnt, err = strconv.Atoi(param)
if len(param) == 0 {
cnt = 1
} else {
cnt, err = strconv.Atoi(param)
}
if err != nil {
return fmt.Errorf("%w:ESC[#D param may be wrong", err)
}
vt.cursorMove(-cnt, 0)
case 'H':
// move the cursor to the home position
vt.cursorHome()
default:
return fmt.Errorf("%q%w", csi, ErrNotSupportedCSI)
}
if err != nil {
return fmt.Errorf("failed to handle csi: %w", err)
return fmt.Errorf("%q%w", csi, ErrCannotHandle)
}
return nil
}

// isIntermediateByte check whether it is CSI param byte
func isIntermediateByte(c byte) bool {
func isIntermediateRune(c rune) bool {
return c >= 0x20 && c <= 0x2F
}

// isParameterByte check whether it is CSI parameter byte
func isParameterByte(c byte) bool {
func isParameterRune(c rune) bool {
return c >= 0x30 && c <= 0x3F
}

// isCSIFinalByte check whether it is CSI final byte
func isCSIFinalByte(c byte) bool {
func isCSIFinalRune(c rune) bool {
return c >= 0x40 && c <= 0x7E
}

// Write implements the io.Writer interface
func (vt *VirtualTerm) Write(p []byte) (n int, err error) {
for i := 0; i < len(p); i++ {
switch p[i] {
case '\r':
// Carriage Return
vt.cursorMove(math.MinInt, 0)
case '\n':
// NewLine
// If the cursor is on the last line, add a new line
vt.cursorMove(math.MinInt, 1)
case '\b':
vt.cursorMove(-1, 0)
case '\033':
idx := i + 1
for idx < len(p) && (!isCSIFinalByte(p[idx]) || (idx == i+1 && p[idx] == '[')) {
idx++
}
// stopped because it is the final byte
if idx != len(p) {
csi := p[i : idx+1]
err = vt.handleCSI(string(csi))
if err != nil {
if errors.Is(err, ErrNotSupportedCSI) {
vt.log("warning, some sci is not supported")
} else {
return 0, err
}
}
}
i = idx
default:
vt.writeRune(rune(p[i]))
}

}
return len(p), nil
func (vt *VirtualTerm) Write(b []byte) (n int, err error) {
return vt.WriteRunes([]rune(string(b)))
}

// WriteString write string to virtual terminal
func (vt *VirtualTerm) WriteString(s string) (n int, err error) {
return vt.Write([]byte(s))
}

// runeWidth
func (*VirtualTerm) runeWidth(r rune) int {
if len(string(r)) >= 3 {
return 2
} else {
return 1
}
}

// cursorMove can control the cursor
func (vt *VirtualTerm) cursorMove(x int, y int) {
vt.cx = max(vt.cx+x, 0)
// handle the offset first
if vt.xOffset != 0 {
x += vt.xOffset
vt.xOffset = 0
}
if x < 0 {
// move the cursor left
// this may cause offset
x = -x
var far int
for vt.cx > 0 && x > 0 {
far = vt.runeWidth(vt.content[vt.cy][vt.cx-1])
if x >= far {
x -= far
vt.cx--
} else {
break
}
}
if x == 1 && vt.cx != 0 {
vt.xOffset = -1
}
} else if x > 0 {
// move the cursor right
// this situation will not cause offset
for x > 0 {
// if cx is out of bound, add empty element
if vt.cx >= len(vt.content[vt.cy])-1 {
vt.content[vt.cy] = append(vt.content[vt.cy], ' ')
}
far := vt.runeWidth(vt.content[vt.cy][vt.cx])
if far <= x {
x -= far
vt.cx++
} else {
break
}
}
if x == 1 {
vt.xOffset = -1
vt.cx++
}
}

// avoid index out of bound
vt.cy = max(vt.cy+y, 0)
for vt.cy >= len(vt.content) {
vt.content = append(vt.content, []rune{' '})
Expand All @@ -182,19 +231,64 @@ func (vt *VirtualTerm) cursorMove(x int, y int) {
func (vt *VirtualTerm) cursorHome() {
vt.cx = 0
vt.cy = 0
vt.xOffset = 0
}

// writeRune write Rune to content.
func (vt *VirtualTerm) writeRune(r rune) {
func (vt *VirtualTerm) writeRune(r rune) error {
// if the offset of cursor is not zero, means that there will be non-deterministic for the output
// For example, if your output is "你好\bCOOL", than it might be "你好OOL"(git bash in Windows) or 你 COOL("Windows powershell")
// So it should be treated as an error.
if vt.xOffset != 0 {
return ErrNonDetermistics
}
// get the width of rune
far := vt.runeWidth(r)
vt.content[vt.cy][vt.cx] = r
vt.cursorMove(1, 0)
vt.cursorMove(far, 0)

return nil
}

// writeRunes write Rune array to content
func (vt *VirtualTerm) writeRunes(rs []rune) {
for _, r := range rs {
vt.writeRune(r)
// WriteRunes write Rune array to content
func (vt *VirtualTerm) WriteRunes(p []rune) (n int, err error) {
for i := 0; i < len(p); i++ {
switch p[i] {
case '\r':
// Carriage Return
vt.cursorMove(-INF, 0)
case '\n':
// NewLine
// If the cursor is on the last line, add a new line
vt.cursorMove(-INF, 1)
case '\b':
vt.cursorMove(-1, 0)
case '\033':
idx := i + 1
for idx < len(p) && (!isCSIFinalRune(p[idx]) || (idx == i+1 && p[idx] == '[')) {
idx++
}
// stopped because it is the final byte
if idx != len(p) {
csi := p[i : idx+1]
err = vt.handleCSI(string(csi))
if err != nil {
if errors.Is(err, ErrCannotHandle) {
vt.log("warning, some sci is not supported")
} else {
return 0, err
}
}
}
i = idx
default:
err = vt.writeRune(p[i])
if err != nil {
return 0, err
}
}
}
return len(p), nil
}

// writeString write String to content
Expand Down
Loading
Loading