You can access the code of this chapter in the Kilo-Go github repository in the welcomescreen branch
Currently your file structure should look something like this:
Refactor: rename the exit module to utils
Since we will be adding more features to the utils module, it only makes sense if we rename it from exit to utils
mv exit utils
File: utils/exit.go
package utils
import (
"fmt"
"os"
)
// SafeExit is a function that allows us to safely exit the program
//
// # It will call the provided function and exit with the provided error
// if no error is provided, it will exit with 0
//
// @param f - The function to call
// @param err - The error to exit with
func SafeExit(f func(), err error) {
if f != nil {
f()
}
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %s\r\n", err)
os.Exit(1)
}
os.Exit(0)
}
File: linux/raw.go
func (r *UnixRawMode) EnableRawMode() (func(), error) {
...
return func() {
if err = unix.IoctlSetTermios(unix.Stdin, unix.TCSETS, &original); err != nil {
utils.SafeExit(nil, fmt.Errorf("EnableRawMode: error restoring terminal flags: %w", err))
}
}, nil
}
File: main.go
func main() {
defer utils.SafeExit(editorState.restoreFunc, nil)
...
for {
b, err := r.ReadByte()
if err == io.EOF {
break
} else if err != nil {
utils.SafeExit(editorState.restoreFunc, err)
}
...
}
}
Press Ctrl-Q to quit
At the moment, when we press q the program exits, lets change it to be Ctrl-Q.
We will need to first recognize if the key pressed corresponds to a control-key combo, so we will write a function in the utility module
File: utils/ctrl.go
package utils
func CtrlKey(key byte) byte {
return key & 0x1f
}
File: main.go
func main() {
...
for {
...
if b == utils.CtrlKey('q') {
break
}
}
}
Refactor keyboard input
First we want to make a new package editor, where we will be mostly working and manage our state there, and also we are going to refactor the read and process key press
File: editor/editor.go
package editor
import (
"bufio"
"os"
"github.com/alcb1310/kilo-go/utils"
)
type EditorConfig struct {
restoreFunc func()
reader *bufio.Reader
}
func NewEditor(f func()) *EditorConfig {
return &EditorConfig{
restoreFunc: f,
reader: bufio.NewReader(os.Stdin),
}
}
func (e *EditorConfig) EditorLoop() {
defer utils.SafeExit(e.restoreFunc, nil)
for {
e.editorProcessKeypress()
}
}
File: editor/input.go
package editor
import "github.com/alcb1310/kilo-go/utils"
func (e *EditorConfig) editorProcessKeypress() {
b, err := e.editorReadKey()
if err != nil {
utils.SafeExit(e.restoreFunc, err)
}
switch b {
case utils.CtrlKey('q'):
utils.SafeExit(e.restoreFunc, nil)
}
}
File: editor/terminal.go
package editor
func (e *EditorConfig) editorReadKey() (byte, error) {
b, err := e.reader.ReadByte()
return b, err
}
File: main.go
package main
import (
"fmt"
"os"
"github.com/alcb1310/kilo-go/editor"
"github.com/alcb1310/kilo-go/linux"
)
var restoreFunc func()
func init() {
var err error
u := linux.NewUnixRawMode()
restoreFunc, err = u.EnableRawMode()
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %s\r\n", err)
os.Exit(1)
}
}
func main() {
editor := editor.NewEditor(restoreFunc)
editor.EditorLoop()
}
So now our main function is very simple, where we just enable raw mode and start the editor, so lets keep it this way
Clear the screen
First we want to clear the screen so we don’t have anything in it so we can work on, we will be using the VT100 User Guide which is a series of key combos that interacts with the screen.
File: editor/output.go
package editor
import (
"fmt"
"os"
"github.com/alcb1310/kilo-go/utils"
)
func (e *EditorConfig) editorRefreshScreen() {
fmt.Fprintf(os.Stdout, "%c[2J", utils.ESC)
}
File: utils/constants.go
package utils
const (
ESC = 0x1b
)
File: editor/editor.go
func (e *EditorConfig) EditorLoop() {
defer utils.SafeExit(e.restoreFunc, nil)
for {
e.editorRefreshScreen()
e.editorProcessKeypress()
}
}
Now the problem is that the cursor is left wherever it was before we clear the screen
Reposition the cursor
We will be using the H command that manages the cursor position, it takes two arguments, the row and column we want to have the cursor at, if no arguments are passed, then it is assumed to be 1 so ESC[H is the same as ESC[1;1H, remember that rows and columns start at number 1 and not 0
File: editor/output.go
func (e *EditorConfig) editorRefreshScreen() {
fmt.Fprintf(os.Stdout, "%c[2J", utils.ESC)
fmt.Fprintf(os.Stdout, "%c[H", utils.ESC)
}
Clear the screen on exit
Lets use what we’ve achieved so far so when our program exits it will clear the screen. This way if an error occurs we will not have a bunch of garbage on the screen improving their experience, and also when on exit we will not show anything that was rendered.
File: utils/exit.go
func SafeExit(f func(), err error) {
fmt.Fprintf(os.Stdout, "%c[2J", ESC)
fmt.Fprintf(os.Stdout, "%c[H", ESC)
...
}
Tildes
Finally we are in a point where we will start drawing thing to the screen. First we will give it a Vim feel by drawing some tildes (~) at the left of the screen of every line that come after the end of the file being edited
File: editor/output.go
func (e *EditorConfig) editorRefreshScreen() {
fmt.Fprintf(os.Stdout, "%c[2J", utils.ESC)
fmt.Fprintf(os.Stdout, "%c[H", utils.ESC)
e.editorDrawRows()
fmt.Fprintf(os.Stdout, "%c[H", utils.ESC)
}
func (e *EditorConfig) editorDrawRows() {
for range 24 {
fmt.Fprintf(os.Stdout, "~\r\n")
}
}
Window Size
At the moment we forced a total of 24 rows, but we want our editor to use all of the rows in your monitor, so we need to find the window size
File: utils/window.go
package utils
import (
"fmt"
"os"
"golang.org/x/sys/unix"
)
func GetWindowSize() (rows int, cols int, err error) {
ws, err := unix.IoctlGetWinsize(unix.Stdin, unix.TIOCGWINSZ)
if err != nil {
fmt.Fprintf(os.Stderr, "getWindowSize: Error getting window size: %v\r\n", err)
return
}
rows = int(ws.Row)
cols = int(ws.Col)
return
}
File: editor/editor.go
type EditorConfig struct {
restoreFunc func()
reader *bufio.Reader
rows, cols int
}
func NewEditor(f func()) *EditorConfig {
rows, cols, err := utils.GetWindowSize()
if err != nil {
utils.SafeExit(f, err)
}
return &EditorConfig{
restoreFunc: f,
reader: bufio.NewReader(os.Stdin),
rows: rows,
cols: cols,
}
}
File: editor/output.go
func (e *EditorConfig) editorDrawRows() {
for range e.rows {
fmt.Fprintf(os.Stdout, "~\r\n")
}
}
The last line
At the moment we always print the \r\n sequence in all lines making us see a blank line at the bottom and loose the first line, lets fix that
File: editor/output.go
func (e *EditorConfig) editorDrawRows() {
for y := range e.rows {
fmt.Fprintf(os.Stdout, "~")
if y < e.rows-1 {
fmt.Fprintf(os.Stdout, "\r\n")
}
}
}
Enable logging
Because the nature of the application, if we need to print any information about the process, we will need to save it to a file, there comes the log/slog package, so lets setup the logger to work as we like
File: utils/logger.go
package utils
import (
"fmt"
"os"
"path"
"time"
)
func CreateLoggerFile(userTempDir string) (*os.File, error) {
now := time.Now()
date := fmt.Sprintf("%s.log", now.Format("2006-01-02"))
if err := os.MkdirAll(path.Join(userTempDir, "kilo-go"), 0o755); err != nil {
return nil, err
}
fileFullPath := path.Join(userTempDir, "kilo-go", date)
file, err := os.OpenFile(fileFullPath, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0o666)
if err != nil {
return nil, err
}
return file, nil
}
File: main.go
func init() {
var f *os.File
var err error
userTempDir, _ := os.UserConfigDir()
if f, err = utils.CreateLoggerFile(userTempDir); err != nil {
utils.SafeExit(nil, err)
}
handlerOptions := &slog.HandlerOptions{}
handlerOptions.Level = slog.LevelDebug
loggerHandler := slog.NewTextHandler(f, handlerOptions)
slog.SetDefault(slog.New(loggerHandler))
...
}
This will create a log file inside the .config/kilo-go directory with the current date as its name
Append Buffer
It is not a good idea to make a lot of Fprintf since all input/output operations are expensive and can cause unexpected behaviors or screen flickering.
We want to replace all of our Fprintf with code that appends all those strings to a buffer and then write this buffer at the end.
We are going to use one of Go features and create a Writer interface which will save in a byte array the information we pass.
File: appendbuffer/appendbuffer.go
package appendbuffer
type AppendBuffer struct {
buf []byte
}
func New() *AppendBuffer {
return &AppendBuffer{}
}
func (ab *AppendBuffer) Write(p []byte) (int, error) {
ab.buf = append(ab.buf, p...)
return len(p), nil
}
func (ab *AppendBuffer) Bytes() []byte {
return ab.buf
}
File: editor/output.go
package editor
import (
"fmt"
"os"
ab "github.com/alcb1310/kilo-go/appendbuffer"
"github.com/alcb1310/kilo-go/utils"
)
func (e *EditorConfig) editorRefreshScreen() {
abuf := ab.New()
fmt.Fprintf(abuf, "%c[2J", utils.ESC)
fmt.Fprintf(abuf, "%c[H", utils.ESC)
e.editorDrawRows(abuf)
fmt.Fprintf(abuf, "%c[H", utils.ESC)
fmt.Fprintf(os.Stdout, "%s", abuf.Bytes())
}
func (e *EditorConfig) editorDrawRows(abuf *ab.AppendBuffer) {
for y := range e.rows {
fmt.Fprintf(abuf, "~")
if y < e.rows-1 {
fmt.Fprintf(abuf, "\r\n")
}
}
}
Hide the cursor while repainting
There is another possible source of flickering, it’s possible that the cursor might be displayed in the middle of the screen somewhere for a split second while it is drawing to the screen. To make sure that doesn’t happen, we can hide it while repainting the screen, and show it again once it finishes
File: editor/output.go
func (e *EditorConfig) editorRefreshScreen() {
...
fmt.Fprintf(abuf, "%c[?25l", utils.ESC)
...
fmt.Fprintf(abuf, "%c[?25h", utils.ESC)
...
}
Clears lines one at a time
Instead of clearing the entire screen before each refresh, it seems more optional to clear each line as we redraw them. Lets remove the escape sequence, and instead put a sequence at the end of each line we draw
File: editor/output.go
func (e *EditorConfig) editorRefreshScreen() {
abuf := ab.New()
fmt.Fprintf(abuf, "%c[?25l", utils.ESC)
fmt.Fprintf(abuf, "%c[H", utils.ESC)
e.editorDrawRows(abuf)
...
}
func (e *EditorConfig) editorDrawRows(abuf *ab.AppendBuffer) {
for y := range e.rows {
fmt.Fprintf(abuf, "~")
fmt.Fprintf(abuf, "%c[K", utils.ESC)
if y < e.rows-1 {
fmt.Fprintf(abuf, "\r\n")
}
}
}
Welcome message
It is finally time we will display a welcome message to our editor
File: editor/output.go
func (e *EditorConfig) editorDrawRows(abuf *ab.AppendBuffer) {
for y := range e.rows {
if y == e.rows/3 {
welcomeMessage := fmt.Sprintf("Kilo editor -- version %s", utils.KILO_VERSION)
fmt.Fprintf(abuf, "%s", welcomeMessage)
} else {
fmt.Fprintf(abuf, "~")
}
...
}
}
File: utils/constants.go
package utils
const (
ESC = 0x1b
KILO_VERSION = "0.0.1"
)
Center the message
Now that we’ve shown the welcome screen, it seems odd with the message not centered, so lets do that
File: editor/output.go
func (e *EditorConfig) editorDrawRows(abuf *ab.AppendBuffer) {
for y := range e.rows {
if y == e.rows/3 {
welcomeMessage := fmt.Sprintf("Kilo editor -- version %s", utils.KILO_VERSION)
welcomeLen := len(welcomeMessage)
if welcomeLen > e.cols {
welcomeLen = e.cols
}
padding := (e.cols - welcomeLen) / 2
if padding > 0 {
fmt.Fprintf(abuf, "~")
padding--
}
for range padding {
fmt.Fprintf(abuf, " ")
}
fmt.Fprintf(abuf, "%s", welcomeMessage)
...
}
}

