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 }