package main import ( "bufio" "fmt" "io" "log" "os" "syscall" "github.com/pkg/term/termios" ) var ( stdin = bufio.NewReader(os.Stdin) ) const ( KEY_ESC = rune('\x1b') KEY_BACKSPACE = rune('\x08') KEY_ENTER = rune('\x0d') KEY_TAB = rune('\x09') KEY_UP_ARROW = (iota + 1000) KEY_DOWN_ARROW = (iota + 1000) KEY_LEFT_ARROW = (iota + 1000) KEY_RIGHT_ARROW = (iota + 1000) KEY_PAGEUP = (iota + 1000) KEY_PAGEDOWN = (iota + 1000) KEY_HOME = (iota + 1000) KEY_END = (iota + 1000) KEY_DELETE = (iota + 1000) ) func editorKeyPresses() bool { char := editorReadKey() state.StatusLines[1] = "" if char == ctrl_key('r') { uiRefresh() } else if char == ctrl_key('s') { fileSave() } else if char == ctrl_key('j') { for i := state.rowOffset + state.Cy; i > 0; i-- { editorMoveCaret(KEY_UP_ARROW) } } else if char == ctrl_key('k') { for i := state.rowOffset + state.Cy; i < state.getCurrentRowLen()-1; i++ { editorMoveCaret(KEY_DOWN_ARROW) } } else if char == ctrl_key('q') { return false } else if char == KEY_BACKSPACE || char == KEY_DELETE { editorRemoveChar(char) } else if char >= 32 && char <= 126 || char == KEY_ENTER || char == KEY_TAB { editorInsertChar(char) } else { editorMoveCaret(char) } return true } func editorRemoveChar(char rune) { cx := state.Cx cy := state.Cy rc := len(state.rows) lt := "" rl := 0 if rc > 0 { lt = state.rows[cy] rl = state.getCurrentRowLen() } // file is empty if rc == 0 && lt == "" { return } /* 1. Backspace at beginning of a line 2. Backspace in middle of a line 3. Backspace at end of a line 4. Backspace when there are 0 characters in the file */ if char == KEY_BACKSPACE { if cx == 0 && cy == 0 { return } // backspace in the middle of a line if cx > 0 && cx < rl { state.rows[cy] = fmt.Sprintf("%s%s", lt[:cx-1], lt[cx:]) editorMoveCaret(KEY_LEFT_ARROW) return } // backspace at start of line // join lines together and remove if cx == 0 && cy > 0 { plt := state.rows[cy-1] _, highlightedRowLen := renderLine(plt) state.rows[cy-1] = fmt.Sprintf("%s%s", plt, lt) editorMoveCaret(KEY_UP_ARROW) for x := 0; x < highlightedRowLen; x++ { editorMoveCaret(KEY_RIGHT_ARROW) } // if we're at the last row, make sure to only remove the last line if cy == rc-1 { state.rows = state.rows[:cy] } else { state.rows = append(state.rows[:cy], state.rows[cy+1:]...) } return } // backspace at end of line if cx == rl { state.rows[cy] = lt[:cx-1] editorMoveCaret(KEY_LEFT_ARROW) return } } else if char == KEY_DELETE { // in middle of line if cx < rl-1 { state.rows[state.Cy] = fmt.Sprintf("%s%s", lt[:cx], lt[cx+1:]) } } } func editorInsertChar(char rune) { cx := state.Cx cy := state.Cy rc := len(state.rows) lt := "" rl := 0 if rc > 0 { lt = state.rows[cy] rl = state.getCurrentRowLen() } /* 1. Enter at beginning line 2. Enter at middle of line 3. Enter at end of line */ if char == KEY_ENTER { // beginning of line if cx == 0 { if cy == rc { state.rows = append(state.rows, lt) } else if cy < rc { state.rows = append(append(state.rows[:cy], lt), state.rows[cy:]...) } state.rows[cy] = "" // end of a line } else if cx >= rl { if cy < rc { state.rows = append(append(state.rows[:cy], ""), state.rows[cy:]...) state.rows[cy] = lt } else if cy == rc { state.rows = append(state.rows[:cy], "") } // mid line } else if cx > 0 { state.rows = append(append(state.rows[:cy], lt[cx:]), state.rows[cy:]...) state.rows[cy] = lt[:cx] } state.Cx = 0 editorMoveCaret(KEY_DOWN_ARROW) } else { state.StatusLines[1] = fmt.Sprintf("%c %d", char, char) state.rows[cy] = fmt.Sprintf("%s%c%s", lt[:cx], char, lt[cx:]) editorMoveCaret(KEY_RIGHT_ARROW) } } func editorMoveCaret(char rune) { x, y := state.Cx, state.Cy if len(state.rows) == 0 { return } rx := editorCxoRx(state.rows[y], x) switch char { case KEY_UP_ARROW: if y != 0 { y-- } break case KEY_DOWN_ARROW: if state.rows != nil && y < len(state.rows)-1 { y++ } break case KEY_LEFT_ARROW: if x > 0 { x-- } else if y != 0 { y-- x = state.getRowLen(y) + 1 } break case KEY_RIGHT_ARROW: if state.rows != nil && rx < state.getRowLen(y) { x++ } else if y < len(state.rows)-1 { y++ x = 0 } break case KEY_HOME: x = 0 break case KEY_END: x = state.Cols - 1 break default: return } if char == ctrl_key(KEY_DOWN_ARROW) { y = len(state.rows) - 1 } if char == ctrl_key(KEY_UP_ARROW) { y = 0 } if x > state.getRowLen(y)+1 { x = state.getRowLen(y) + 1 } state.Cx = x state.Cy = y editorScroll() } func editorScroll() { gutterWidth, _ := calcMaxGutterWidth() displayWidth := state.Cols - gutterWidth - 2 rx := gutterWidth + editorCxoRx(state.rows[state.Cy], state.Cx) if rx >= state.colOffset+displayWidth { state.colOffset = rx - displayWidth } if rx < state.colOffset { state.colOffset = rx } if state.Cx >= state.colOffset+displayWidth { state.colOffset = state.Cx - displayWidth } if state.Cx < state.colOffset { state.colOffset = state.Cx } if state.Cy >= state.rowOffset+(state.Rows-2) { state.rowOffset = state.Cy - (state.Rows - 2) } if state.Cy < state.rowOffset { state.rowOffset = state.Cy } } func editorReadKey() rune { in := '\000' var err error var read int for read != 1 { in, read, err = stdin.ReadRune() if err != nil && err != io.EOF { return KEY_ESC //log.Fatal("FAILED TO READ:", err) } } if in == KEY_ESC { seq := make([]rune, 3) if seq[0], read, err = stdin.ReadRune(); err != nil || read != 1 { return KEY_ESC } if seq[1], read, err = stdin.ReadRune(); err != nil || read != 1 { return KEY_ESC } if seq[0] == '[' { if seq[1] >= '0' && seq[1] <= '9' { if seq[2], read, err = stdin.ReadRune(); err != nil || read != 1 { return KEY_ESC } // handle page up/down and home keys if seq[2] == '~' { switch seq[1] { case '1': case '7': return KEY_HOME case 3: return KEY_DELETE case '4': case '8': return KEY_END case '5': return KEY_PAGEUP case '6': return KEY_PAGEDOWN } } } else { // handle arrow keys and home keys switch seq[1] { case 'A': return KEY_UP_ARROW case 'B': return KEY_DOWN_ARROW case 'C': return KEY_RIGHT_ARROW case 'D': return KEY_LEFT_ARROW case 'H': return KEY_HOME case 'F': return KEY_END } } } else if seq[0] == 'O' { switch seq[1] { case 'H': return KEY_HOME case 'F': return KEY_END } } return KEY_ESC } switch in { case '\x7f': // backspace return KEY_BACKSPACE case '\x0d': return KEY_ENTER } return in } func enableRawMode() { if err := termios.Tcgetattr(uintptr(0), &state.orig); err != nil { log.Fatal("Tcgetattr, ", err) } raw := state.orig raw.Iflag &^= syscall.IXON | syscall.ICRNL | syscall.BRKINT | syscall.INPCK | syscall.ISTRIP raw.Oflag &^= syscall.OPOST raw.Cflag &^= syscall.CS8 raw.Lflag &^= syscall.ECHO | syscall.ICANON | syscall.ISIG | syscall.IEXTEN raw.Cc[syscall.VMIN] = 0 raw.Cc[syscall.VTIME] = 1 if err := termios.Tcsetattr(uintptr(0), termios.TCSAFLUSH, &raw); err != nil { log.Fatal("tcsetattr, ", err) } } func disableRawMode() { if err := termios.Tcsetattr(uintptr(0), termios.TCSAFLUSH, &state.orig); err != nil { log.Fatal(err) } } func ctrl_key(key rune) rune { return rune(int(key) & 0x1f) }