DEV Community

Andres Court
Andres Court

Posted on

Create a Text Editor in Go - Moving the Cursor

You can access the code of this chapter in the Kilo-Go github repository in the movingaround branch

Currently your file structure should look something like this:

Cursor state

First we need a way to track the cursor position

File: editor/editor.go

type EditorConfig struct {
    ...
    cx, cy      int
}

func NewEditor(f func()) *EditorConfig {
    ...
    return &EditorConfig{
        ...
        cx:          0,
        cy:          0,
    }
}
Enter fullscreen mode Exit fullscreen mode

File: editor/output.go

func (e *EditorConfig) editorRefreshScreen() {
    ...
    e.editorDrawRows(abuf)
    fmt.Fprintf(abuf, "%c[%d;%dH", utils.ESC, e.cy + 1, e.cx+1)
    fmt.Fprintf(abuf, "%c[?25h", utils.ESC)
    ...
}

func (e *EditorConfig) editorDrawRows(abuf *ab.AppendBuffer) {
    for y := range e.rows {
        if y == e.rows/3 {
            ...
            welcomeLen := min(len(welcomeMessage), e.cols)
            ...
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Cursor movement

Now that we've tracked the cursor position, we can add the logic to move the cursor with vim-like keys so we will be using h, j, k and l

File: editor/input.go

func (e *EditorConfig) editorProcessKeypress() {
    ...
    switch b {
    case utils.CtrlKey('q'):
        utils.SafeExit(e.restoreFunc, nil)
    case 'h', 'j', 'k', 'l':
        e.editorMoveCursor(b)
    }
}

func (e *EditorConfig) editorMoveCursor(key byte) {
    switch key {
    case 'h':
        e.cx--
    case 'j':
        e.cy++
    case 'k':
        e.cy--
    case 'l':
        e.cx++
    }
}
Enter fullscreen mode Exit fullscreen mode

Reading the arrow keys

Lets read the arrow keys next, each one is represented by 3 bytes:

  • Up Arrow is represented by <Esc> [ A
  • Down Arrow is represented by <Esc> [ B
  • Right Arrow is represented by <Esc> [ C
  • Left Arrow is represented by <Esc> [ D

So with that in mind, lets read them

File: editor/terminal.go

import (
    "github.com/alcb1310/kilo-go/utils"
)

func (e *EditorConfig) editorReadKey() (byte, error) {
    b, err := e.reader.ReadByte()

    if b == utils.ESC {
        seq := make([]byte, 2)

        seq[0], err = e.reader.ReadByte()
        if err != nil {
            return utils.ESC, nil
        }
        seq[1], err = e.reader.ReadByte()
        if err != nil {
            return utils.ESC, nil
        }

        if seq[0] == '[' {
            switch seq[1] {
            case 'A':
                return 'k', nil
            case 'B':
                return 'j', nil
            case 'C':
                return 'l', nil
            case 'D':
                return 'h', nil
            }
        }

        return utils.ESC, nil
    }

    return b, err
}
Enter fullscreen mode Exit fullscreen mode

Refactor: improve readability

Now that we have the arrow keys working, lets improve the readability by creating constants for it

File: utils/constants.go

const (
    ARROW_UP    = 'k'
    ARROW_DOWN  = 'j'
    ARROW_LEFT  = 'h'
    ARROW_RIGHT = 'l'
)
Enter fullscreen mode Exit fullscreen mode

File: editor/input.go

func (e *EditorConfig) editorProcessKeypress() {
    ...
    switch b {
    case utils.CtrlKey('q'):
        utils.SafeExit(e.restoreFunc, nil)
    case utils.ARROW_DOWN, utils.ARROW_LEFT, utils.ARROW_RIGHT, utils.ARROW_UP:
        e.editorMoveCursor(b)
    }
}

func (e *EditorConfig) editorMoveCursor(key byte) {
    switch key {
    case utils.ARROW_LEFT:
        e.cx--
    case utils.ARROW_DOWN:
        e.cy++
    case utils.ARROW_UP:
        e.cy--
    case utils.ARROW_RIGHT:
        e.cx++
    }
}
Enter fullscreen mode Exit fullscreen mode

File: editor/terminal.go

func (e *EditorConfig) editorReadKey() (byte, error) {
        ...
        if seq[0] == '[' {
            switch seq[1] {
            case 'A':
                return utils.ARROW_UP, nil
            case 'B':
                return utils.ARROW_DOWN, nil
            case 'C':
                return utils.ARROW_RIGHT, nil
            case 'D':
                return utils.ARROW_LEFT, nil
            }
        }
        ...
}
Enter fullscreen mode Exit fullscreen mode

Refactor: only use arrows to move

Now that we've added constants to the arrow keys we can give them a value outside of the byte range and start using them as an int. However doing so, we will need to change several files in order to return int instead of byte

File: utils/constant.go

const (
    ARROW_UP = iota + 1000
    ARROW_DOWN
    ARROW_LEFT
    ARROW_RIGHT
)
Enter fullscreen mode Exit fullscreen mode

File: utils/ctrl.go

func CtrlKey(key byte) int {
    return int(key & 0x1f)
}
Enter fullscreen mode Exit fullscreen mode

File editor/input.go

func (e *EditorConfig) editorMoveCursor(key int) {
    ...
}
Enter fullscreen mode Exit fullscreen mode

File editor/terminal.go

func (e *EditorConfig) editorReadKey() (int, error) {
    ...
    return int(b), err
}
Enter fullscreen mode Exit fullscreen mode

Note: We mostly needed to change the function signature for it to work

Prevent moving the cursor off screen

Currently, we can have the cx and cy values to go into the negatives or go past the right and bottom edges of the screen, so lets prevent that

File: editor/input.go

func (e *EditorConfig) editorMoveCursor(key int) {
    switch key {
    case utils.ARROW_LEFT:
        if e.cx != 0 {
            e.cx--
        }
    case utils.ARROW_DOWN:
        if e.cy != e.rows-1 {
            e.cy++
        }
    case utils.ARROW_UP:
        if e.cy != 0 {
            e.cy--
        }
    case utils.ARROW_RIGHT:
        if e.cx != e.cols-1 {
            e.cx++
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

The Page Up and Page Down keys

To complete our movements, we need to detect a few more special keypresses that use escape sequences, like the arrow keys did. We'll start with the Page Up which is sent as <Esc> [ 5 ~ and Page Down which is sent as <Esc> [ 6 ~

File utils/constants.go

const (
    ARROW_UP = iota + 1000
    ARROW_DOWN
    ARROW_LEFT
    ARROW_RIGHT
    PAGE_UP
    PAGE_DOWN
)
Enter fullscreen mode Exit fullscreen mode

File: editor/terminal.go

func (e *EditorConfig) editorProcessKeypress() {
    ...
    case utils.PAGE_DOWN, utils.PAGE_UP:
        times := e.rows
        for range times {
            if b == utils.PAGE_DOWN {
                e.editorMoveCursor(utils.ARROW_DOWN)
            } else {
                e.editorMoveCursor(utils.ARROW_UP)
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

File: editor/terminal.go

func (e *EditorConfig) editorReadKey() (int, error) {
    ...
    if b == utils.ESC {
        seq := make([]byte, 3)
        ...
        if seq[0] == '[' {
            if seq[1] >= '0' && seq[1] <= '9' {
                seq[2], err = e.reader.ReadByte()
                if err != nil {
                    return utils.ESC, nil
                }

                if seq[2] == '~' {
                    switch seq[1] {
                    case '5':
                        return utils.PAGE_UP, nil
                    case '6':
                        return utils.PAGE_DOWN, nil
                    }
                }
            } else {
                switch seq[1] {
                case 'A':
                    return utils.ARROW_UP, nil
                case 'B':
                    return utils.ARROW_DOWN, nil
                case 'C':
                    return utils.ARROW_RIGHT, nil
                case 'D':
                    return utils.ARROW_LEFT, nil
                }
            }
        }
    ...
}
Enter fullscreen mode Exit fullscreen mode

The Home and End keys

Like the previous keys, these keys also send escape sequences. Unlike previous keys, there are many different escape sequences that could be sent by these keys.

  • The Home key could be sent as <Esc> [ 1 ~, <Esc> [ 7 ~, <Esc> [ H or <Esc> O H
  • The End key could be sent as <Esc> [ 4 ~, <Esc> [ 8 ~, <Esc> [ F or <Esc> O F

File: utils/constants.go

const (
    ARROW_UP = iota + 1000
    ARROW_DOWN
    ARROW_LEFT
    ARROW_RIGHT
    HOME_KEY
    END_KEY
    PAGE_UP
    PAGE_DOWN
)
Enter fullscreen mode Exit fullscreen mode

File: editor/terminal.go

func (e *EditorConfig) editorReadKey() (int, error) {
        ...
        if seq[0] == '[' {
            if seq[1] >= '0' && seq[1] <= '9' {
                ...
                if seq[2] == '~' {
                    switch seq[1] {
                    case '1':
                        return utils.HOME_KEY, nil
                    case '4':
                        return utils.END_KEY, nil
                    case '5':
                        return utils.PAGE_UP, nil
                    case '6':
                        return utils.PAGE_DOWN, nil
                    case '7':
                        return utils.HOME_KEY, nil
                    case '8':
                        return utils.END_KEY, nil
                    }
                }
            } else {
                switch seq[1] {
                case 'A':
                    return utils.ARROW_UP, nil
                case 'B':
                    return utils.ARROW_DOWN, nil
                case 'C':
                    return utils.ARROW_RIGHT, nil
                case 'D':
                    return utils.ARROW_LEFT, nil
                case 'H':
                    return utils.HOME_KEY, nil
                case 'F':
                    return utils.END_KEY, nil
                }
            }
        } else if seq[0] == 'O' {
            switch seq[1] {
            case 'H':
                return utils.HOME_KEY, nil
            case 'F':
                return utils.END_KEY, nil
            }
        }
        ...
}
Enter fullscreen mode Exit fullscreen mode

File: editor/input.go

func (e *EditorConfig) editorProcessKeypress() {
    ...
    switch b {
    ...
    case utils.HOME_KEY:
        e.cx = 0
    case utils.END_KEY:
        e.cx = e.cols - 1
    }
}
Enter fullscreen mode Exit fullscreen mode

The Delete key

Lastly we will detect when the Delete key is pressed. It simply sends the escape sequence <Esc> [ 3 ~, so it will be easy to add it to our switch statement. For now we will just log when the key is pressed

File: utils/constants.go

const (
    ARROW_LEFT = iota + 1000
    ARROW_RIGHT
    ARROW_UP
    ARROW_DOWN
    DEL_KEY
    HOME_KEY
    END_KEY
    PAGE_UP
    PAGE_DOWN
)
Enter fullscreen mode Exit fullscreen mode

File: editor/terminal.go

func (e *EditorConfig) editorReadKey() (int, error) {
    ...
    if b == utils.ESC {
        ...
        switch seq[0] {
        case '[':
            if seq[1] >= '0' && seq[1] <= '9' {
                ...
                if seq[2] == '~' {
                    switch seq[1] {
                    case '1':
                        return utils.HOME_KEY, nil
                    case '3':
                        return utils.DEL_KEY, nil
                    ...
                    }
                }
            ...
        case 'O':
            switch seq[1] {
            case 'H':
                return utils.HOME_KEY, nil
            case 'F':
                return utils.END_KEY, nil
            }
        }

    ...
}
Enter fullscreen mode Exit fullscreen mode

File: editor/input.go

func (e *EditorConfig) editorProcessKeypress() {
    ...
    switch b {
    ...
    case utils.DEL_KEY:
        slog.Info("DEL_KEY")
    ...
}
Enter fullscreen mode Exit fullscreen mode

Top comments (0)