After Setting Up our project, we are ready to start coding.
You can access the code of this chapter in the Kilo-Go github repository in the rawmode branch
Currently your file structure should look something like this:
We will need to create a new file main.go where we will read keypresses from the user
package main
import (
"bufio"
"fmt"
"io"
"os"
)
func main() {
r := bufio.NewReader(os.Stdin)
for {
_, err := r.ReadByte()
if err == io.EOF {
break
} else if err != nil {
fmt.Fprintf(os.Stderr, "Error: reading key from Stdin: %s\n", err)
os.Exit(1)
}
}
}
With the code above we are reading and displaying the keypresses, but at the moment we will need to press Ctrl-C to quit the application, so lets give the option to the user when they press q to quit the application, so in our main.go file we will add the following:
func main() {
...
for {
b, err := r.ReadByte()
....
if b == 'q' {
break
}
}
}
Now once we press q the application will close, but we are reading from input until the user press the Enter key, not the behavior we want, so lets turn on raw mode
First in the command line, we will need to add the library we will be using:
go get golang.org/x/sys/unix
Turn off echoing
The ECHO feature causes each key you type to be printed to the terminal, so you can see what you're typing. This is useful in most cases, but really gets in the way when we are trying to render a user interface in raw mode. So we turn it off. The program still does the same thing as the previous step, but, it just doesn't print what you are typing. After the program quits, you will find that the terminal still doesn't echo what you type, so for now just type reset to go back to normal, and if it doesn't work, just restart your terminal. We will fix that in the next step.
File: linux/raw.go
package linux
import (
"fmt"
"golang.org/x/sys/unix"
)
func EnableRawMode() error {
termios, err := unix.IoctlGetTermios(unix.Stdin, unix.TCGETS)
if err != nil {
return fmt.Errorf("EnableRawMode: error getting terminal flags: %w", err)
}
termios.Lflag &^= unix.ECHO
if err = unix.IoctlSetTermios(unix.Stdin, unix.TCSETS, termios); err != nil {
return fmt.Errorf("EnableRawMode: error setting terminal flags: %w", err)
}
return nil
}
File: main.go
import (
...
"github.com/alcb1310/linux"
)
func main() {
linux.EnableRawMode()
...
}
Disable raw mode at exit
File: linux/raw.go
package linux
import (
"fmt"
"os"
"golang.org/x/sys/unix"
)
func EnableRawMode() (func(), error) {
termios, err := unix.IoctlGetTermios(unix.Stdin, unix.TCGETS)
if err != nil {
return nil, fmt.Errorf("EnableRawMode: error getting terminal flags: %w", err)
}
original := *termios
termios.Lflag &^= unix.ECHO
if err = unix.IoctlSetTermios(unix.Stdin, unix.TCSETS, termios); err != nil {
return nil, fmt.Errorf("EnableRawMode: error setting terminal flags: %w", err)
}
return func() {
if err := unix.IoctlSetTermios(unix.Stdin, unix.TCSETS, &original); err != nil {
fmt.Fprintf(os.Stderr, "DisableRawMode: error setting terminal flags: %s\n", err)
os.Exit(1)
}
}, nil
}
File: main.go
func main() {
restoreFunc, err := linux.EnableRawMode()
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %s\n", err)
os.Exit(1)
}
defer restoreFunc()
...
}
Now we are returning a function that allows us to restore the user's terminal to its original attributes when our program exits.
The defer statement will execute the function once the function ends. If we exit the program from another file, this function will not run, we will fix that later in the tutorial.
Turn off Canonical mode
There is an ICANON flag that allows us to turn off cannonical mode, this means that we will finally be reading byte-by-byte instead of line-by-line.
File: linux/raw.go
func EnableRawMode() (func(), error) {
...
termios.Lflag &^= unix.ECHO | unix.ICANON
...
}
Now the program will quit as soon as you press q
Display keypresses
We will like to see what the user is pressing, for the time being, we will show:
- In the case of a control-key combo, the key code
- In all other cases, the key pressed and the key code
File: main.go
func main() {
...
if b <= 0x1f || b == 0x7f { // This will make sure we've pressed a control-key combo
fmt.Fprintf(os.Stdout, "%d\n", b)
} else {
fmt.Fprintf(os.Stdout, "%d (%c)\n", b, b)
}
...
}
Turn off Ctrl-C and Ctrl-Z signals
By default, Ctrl-C sends a SIGINT signal to the current process which causes it to terminate, and Ctrl-Z sends a SIGSTOP signal which causes the process to suspend, lets disable them
File: linux/raw.go
func EnableRawMode() (func(), error) {
...
termios.Lflag &^= unix.ECHO | unix.ICANON | unix.ISIG
...
}
Turn off Ctrl-S and Ctrl-Q signals
By default, Ctrl-S and Ctrl-Q are used for software flow control, Ctrl-S stops data from being transmitted until you press Ctrl-Q. This originates in the days you might want to pause data transmission to let a device to catch up (eg. wait for a printer), lets disable them
File: raw/raw.go
func EnableRawMode() (func(), error) {
...
termios.Iflag &^= unix.IXON
...
}
Disable Ctrl-V
On some systems when you type Ctrl-V, the terminal waits for you to type another character and then sends the last character, we want to disable it
File: linux/raw.go
func EnableRawMode() (func(), error) {
...
termios.Lflag &^= unix.ECHO | unix.ICANON | unix.IEXTEN | unix.ISIG
...
}
Fixing Ctrl-M
If you run the program now and go through the whole alphabet while holding down the Ctrl key, you will see that Ctrl-J and Ctrl-M gives us the same value. This is due to (13 is /r) Enter which is 13 is interpreted as carriage return and in translated to \n which is 10, so lets turn off this feature.
File: linux/raw.go
func EnableRawMode() (func(), error) {
...
termios.Iflag &^= unix.ICRNL | unix.IXON
...
}
Turn off all output processing
The terminal translates the new line (\n) character to (\r\n), it requires both of this characters in order to start a new line. The carriage return (\r) moves the cursor back to the beginning of the current line, and the the new line (\n) moves the cursor down a line, scrolling the screen if necessary. This comes from the era of typewriters. Lets turn off this feature
File: linux/raw
func EnableRawMode() (func(), error) {
...
termios.Oflag &^= unix.OPOST
...
return func() {
if err = unix.IoctlSetTermios(unix.Stdin, unix.TCSETS, &original); err != nil {
fmt.Fprintf(os.Stderr, "EnableRawMode: error restoring terminal flags: %s\r\n", err)
os.Exit(1)
}
}, nil
}
File: main.go
func main() {
restoreFunc, err := linux.EnableRawMode()
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %s\r\n", err)
os.Exit(1)
}
defer restoreFunc()
r := bufio.NewReader(os.Stdin)
for {
b, err := r.ReadByte()
if err == io.EOF {
break
} else if err != nil {
fmt.Fprintf(os.Stderr, "Error: reading key from Stdin: %s\r\n", err)
os.Exit(1)
}
if b <= 0x1f || b == 0x7f { // This will make sure we've passed a control-key combo
fmt.Fprintf(os.Stdout, "%d\r\n", b)
} else {
fmt.Fprintf(os.Stdout, "%d (%c)\r\n", b, b)
}
if b == 'q' {
break
}
}
}
Miscellaneous flags
Let's turn off a few more flags
File: linux/raw.go
func EnableRawMode() (func(), error) {
...
termios.Iflag &^= unix.BRKINT | unix.ICRNL | unix.INPCK | unix.ISTRIP | unix.IXON
termios.Cflag |= unix.CS8
...
}
With this step you will most likely won't see any effect, but turning them off was considered to be part of enabling raw mode, so we carry on the tradition, what I've learned about the flags used:
-
BRKINT: when turned on is a break condition that will cause a
SIGINTto be sent to the program just like pressingCtrl-C - INPCK: enables parity checking
- ISTRIP: causes the 8th bit of each input byte to be stripped, meaning it will set it to 0. This is probably already turned off.
- CS8: this is not a flag, it is a bit mask which sets the character size (CS) to 8 bits per byte.
Refactor: Use interface for enabling raw mode
Interfaces are a common used patter in Go, so we are going to use it here, so if we want to add Mac and Windows support, we can just add a struct that implements that interface
File: linux/raw.go
type UnixRawMode struct{}
func NewUnixRawMode() *UnixRawMode {
return &UnixRawMode{}
}
func (r *UnixRawMode) EnableRawMode() (func(), error) {
...
}
__File: main.go
type RawMode interface {
EnableRawMode() (func(), error)
}
type EditorConfig struct {
restoreFunc func()
}
var editorState EditorConfig = EditorConfig{}
func init() {
var err error
u := linux.NewUnixRawMode()
editorState.restoreFunc, err = u.EnableRawMode()
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %s\r\n", err)
os.Exit(1)
}
}
func main() {
defer editorState.restoreFunc()
...
}
Exit strategy
Now that we have raw mode working, lets improve this by having a centralized way to exit the program so it will manage errors and any exit behavior that we want
File: exit/exit.go
package exit
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 {
exit.SafeExit(nil, fmt.Errorf("EnableRawMode: error restoring terminal flags: %w", err))
}
}, nil
}
File: main.go
func main() {
defer exit.SafeExit(editorState.restoreFunc, nil)
...
}

Top comments (0)