You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
244 lines
4.8 KiB
244 lines
4.8 KiB
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"os"
|
|
"runtime"
|
|
"syscall"
|
|
"unicode/utf8"
|
|
"unsafe"
|
|
)
|
|
|
|
const (
|
|
CLEAR = "\x1b[K"
|
|
TAB_SIZE = 8
|
|
EMPTY_LINE_GUTTER = " ~ "
|
|
LINE_GUTTER = " "
|
|
STATUSLINEHEIGHT = 2
|
|
WHITESPACE = "\xB8"
|
|
NEWLINE = '\xAC'
|
|
)
|
|
|
|
func uiDrawRows() {
|
|
buf := state.Buffer
|
|
currentRow := ""
|
|
if len(state.rows) > 0 {
|
|
currentRow = state.rows[state.Cy]
|
|
}
|
|
|
|
_, highlightedRowLen := renderLine(currentRow)
|
|
rx := editorCxoRx(currentRow, state.Cx)
|
|
gutterWidth, gutterFmtStr := calcMaxGutterWidth()
|
|
|
|
file := state.file
|
|
if file == "" {
|
|
file = "NO FILE"
|
|
}
|
|
|
|
state.StatusLines[0] = fmt.Sprintf(
|
|
"%s\x1b[1;7m '%s' \x1b[m L:%d/%d C:%d/%d | H:%d W: %d",
|
|
CLEAR,
|
|
file,
|
|
state.Cy+1,
|
|
len(state.rows),
|
|
rx+1,
|
|
highlightedRowLen+1,
|
|
state.Rows-1,
|
|
state.Cols-gutterWidth-1)
|
|
|
|
|
|
for y := 0; y < state.Rows; y++ {
|
|
if y >= state.Rows-STATUSLINEHEIGHT {
|
|
buf.WriteString(drawStatusLine(y - (state.Rows - STATUSLINEHEIGHT)))
|
|
} else if state.Cy < len(state.rows) && y + state.rowOffset < len(state.rows) {
|
|
rowIdx := y + state.rowOffset
|
|
|
|
if rowIdx < len(state.rows) {
|
|
// wrie the gutter w/ line numbers
|
|
fmt.Fprintf(buf, gutterFmtStr, rowIdx+1)
|
|
|
|
// calc how much room left for text
|
|
editorCols := (state.Cols - gutterWidth) - 1
|
|
|
|
// get length and content for the current row
|
|
row, rowLen := renderLine(state.rows[rowIdx])
|
|
|
|
// calculate offset from the left
|
|
renderLen := rowLen
|
|
rowEnd := editorCols + state.colOffset
|
|
if renderLen > editorCols && rowEnd <= rowLen {
|
|
renderLen = rowEnd
|
|
}
|
|
|
|
if state.colOffset <= renderLen {
|
|
buf.WriteString(fmt.Sprintf("%s%c", row[state.colOffset:renderLen], NEWLINE))
|
|
}
|
|
|
|
if rowLen > rowEnd {
|
|
buf.WriteByte('>')
|
|
}
|
|
}
|
|
} else {
|
|
buf.WriteString(EMPTY_LINE_GUTTER)
|
|
}
|
|
|
|
buf.WriteString(CLEAR)
|
|
|
|
if y < state.Rows-1 {
|
|
buf.WriteString("\r\n")
|
|
}
|
|
}
|
|
}
|
|
|
|
func drawStatusLine(row int) string {
|
|
statusLine := state.StatusLines[row]
|
|
statusLen := len(statusLine)
|
|
if statusLen > state.Cols {
|
|
statusLen = state.Cols
|
|
}
|
|
return statusLine[:statusLen]
|
|
}
|
|
|
|
|
|
func uiRefresh() {
|
|
buf := state.Buffer
|
|
fmt.Fprintf(buf, "\x1b[?251")
|
|
fmt.Fprintf(buf, "\x1b[H")
|
|
|
|
var err error
|
|
if state.Rows, state.Cols, err = getWindowSize(); err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
uiDrawRows()
|
|
|
|
gutterWidth, _ := calcMaxGutterWidth()
|
|
|
|
y := state.Cy - state.rowOffset
|
|
currentRow := ""
|
|
if len(state.rows) > 0 {
|
|
currentRow = state.rows[y]
|
|
}
|
|
|
|
rx := gutterWidth + editorCxoRx(currentRow, state.Cx) - state.colOffset
|
|
|
|
setCursorPosition(rx, y)
|
|
|
|
fmt.Fprintf(buf, "\x1b[?25h")
|
|
|
|
os.Stdout.Write(buf.Bytes())
|
|
buf.Reset()
|
|
}
|
|
|
|
func uiClearScreen() {
|
|
fmt.Fprintf(os.Stdout, "\x1b[2J")
|
|
fmt.Fprintf(os.Stdout, "\x1b[H")
|
|
}
|
|
|
|
func setCursorPosition(x, y int) {
|
|
fmt.Fprintf(state.Buffer, "\x1b[%d;%dH", y+1, x+1)
|
|
}
|
|
|
|
func getCursorPosition() (int, int, error) {
|
|
if _, err := os.Stdout.Write([]byte("\x1b[6n")); err != nil && err != io.EOF {
|
|
return -1, -1, err
|
|
}
|
|
|
|
buf := make([]byte, 32)
|
|
temp := make([]byte, 1)
|
|
for i := 0; i < len(buf)-1; i++ {
|
|
if _, err := os.Stdin.Read(temp); err != nil && err != io.EOF {
|
|
return -1, -1, err
|
|
}
|
|
buf[i] = temp[0]
|
|
if bytes.ContainsRune(temp, 'R') {
|
|
break
|
|
}
|
|
}
|
|
|
|
if buf[0] != byte(KEY_ESC) || buf[1] != '[' {
|
|
return 0, 0, errors.New("can't read pos")
|
|
}
|
|
|
|
var rows, cols int
|
|
if _, err := fmt.Fscanf(bytes.NewBuffer(buf[2:]), "%d;%d", &rows, &cols); err != nil && err != io.EOF {
|
|
return 0, 0, errors.New("can't read pos")
|
|
}
|
|
|
|
return rows, cols, nil
|
|
}
|
|
|
|
type window struct {
|
|
Row uint16
|
|
Col uint16
|
|
Xpixel uint16
|
|
Ypixel uint16
|
|
}
|
|
|
|
func getWindowSize() (int, int, error) {
|
|
w := new(window)
|
|
_, _, err := syscall.Syscall(syscall.SYS_IOCTL,
|
|
os.Stdout.Fd(),
|
|
syscall.TIOCGWINSZ,
|
|
uintptr(unsafe.Pointer(w)),
|
|
)
|
|
runtime.GC()
|
|
if int(err) == -1 || w.Col == 0 {
|
|
if _, err := os.Stdout.Write([]byte("\x1b[999C\x1b[999B")); err != nil {
|
|
return -1, -1, errors.New("ioctl FALLBACK")
|
|
}
|
|
|
|
return getCursorPosition()
|
|
}
|
|
|
|
return int(w.Row), int(w.Col), nil
|
|
}
|
|
|
|
func calcMaxGutterWidth() (int, string) {
|
|
lineCount := len(state.rows)
|
|
maxDigits := len([]rune(fmt.Sprintf("%d", lineCount)))
|
|
gutterStr := fmt.Sprintf("%s%s%%%dd ", CLEAR, LINE_GUTTER, maxDigits)
|
|
|
|
// 3 accounts for ' ~ '
|
|
return len([]rune(LINE_GUTTER)) + maxDigits + 1, gutterStr
|
|
}
|
|
|
|
func editorCxoRx(row string, cx int) int {
|
|
rx := 0
|
|
for idx, c := range row {
|
|
if idx < cx {
|
|
if c == '\t' {
|
|
// rx += (TAB_SIZE - 1) - (rx % TAB_SIZE)
|
|
rx += (TAB_SIZE - 1)
|
|
}
|
|
rx++
|
|
continue
|
|
}
|
|
break
|
|
}
|
|
|
|
return rx
|
|
}
|
|
|
|
func renderLine(line string) (string, int) {
|
|
rl := ""
|
|
tabCount := 0
|
|
for _, char := range line {
|
|
if char == '\t' {
|
|
tabCount++
|
|
for i := 0; i < TAB_SIZE; i++ {
|
|
rl += WHITESPACE
|
|
}
|
|
continue
|
|
}
|
|
|
|
rl = rl + string(char)
|
|
}
|
|
|
|
return rl, utf8.RuneCountInString(rl) //+ (TAB_SIZE * tabCount) - tabCount
|
|
}
|