On my previous posts, I've been creating a text editor based on the Kilo text Editor.
The changes explained in this post can be found in the Kilo-go github repository, in the config branch
Now we are going to improve it, and since we've finished the guide, I'm not going to continue with the series.
In this article we are going to go through the process of:
- Being able to use in other operating systems and not only in Linux
- Adding a configuration file so we can change some of the editor's behavior without changing the code
Multiplatform support
First thing we need to address is to be able to run the text editor in any operating system. Upon finishing the series, I found a library that will help us go to raw mode regardless of the operating system we are working in.
Note: I was able to test this feature in both
MacOSandLinux, do not have aWindowsmachine for me to test in
Install the required dependency
The library that will help us with this is golang.org/x/term so let's proceed to install it
go get golang.org/x/term
Implementing the feature
Now that we have the library on our project, let's use it to go to raw mode
File: linux/raw.go
package linux
import (
"fmt"
"os"
"github.com/alcb1310/kilo-go/utils"
"golang.org/x/term"
)
func EnableRawMode() (func(), error) {
oldState, err := term.MakeRaw(int(os.Stdin.Fd()))
return func() {
if err = term.Restore(int(os.Stdin.Fd()), oldState); err != nil {
utils.SafeExit(nil, fmt.Errorf("EnableRawMode: error restoring terminal flags: %w", err))
}
}, err
}
File: utils/window.go
package utils
import (
"os"
"golang.org/x/term"
)
func GetWindowSize() (int, int, error) {
return term.GetSize(int(os.Stdout.Fd()))
}
File: main.go
func init() {
...
restoreFunc, err = linux.EnableRawMode()
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %s\r\n", err)
os.Exit(1)
}
}
File: editor/editor.go
func NewEditor(f func()) *EditorConfig {
cols, rows, err := utils.GetWindowSize()
if err != nil {
utils.SafeExit(f, err)
}
...
}
This refactor doesn't only allows us to run the text editor in different platforms, but also simplified it a lot
Cleaning up unused libraries
Now that we've changed the library we use to go to enable raw mode, we need to remove unnecessary libraries
go mod tidy
Adding a configuration file
Next step in our process is to be able to configure the Text Editor using a configuration file, that way we can change the behavior without modifying the application
Using variables instead of constants
We will be using variables instead of constants so we can modify them with the ones in the configuration files, the values in that we assign on declaration will be the default behavior the application will have if no config file or if it is not set in the config.
File: utils/constants.go
var (
KILO_TAB_STOP int = 8
KILO_QUIT_TIMES int = 3
KILO_DEFAULT_COLOR [3]uint8 = [3]uint8{255, 255, 255}
KILO_NUMBER_COLOR [3]uint8 = [3]uint8{255, 0, 0}
KILO_MATCH_COLOR [3]uint8 = [3]uint8{51, 255, 0}
KILO_STRING_COLOR [3]uint8 = [3]uint8{255, 39, 155}
KILO_COMMENT_COLOR [3]uint8 = [3]uint8{0, 255, 255}
KILO_KEYWORD_COLOR [3]uint8 = [3]uint8{255, 239, 0}
KILO_TYPE_COLOR [3]uint8 = [3]uint8{126, 239, 55}
)
File: editor/syntax.go
func editorSyntaxToColor(hl utils.EditorHighlight) (r uint8, g uint8, b uint8) {
switch hl {
case utils.HL_NUMBER:
r = utils.KILO_NUMBER_COLOR[0]
g = utils.KILO_NUMBER_COLOR[1]
b = utils.KILO_NUMBER_COLOR[2]
case utils.HL_MATCH:
r = utils.KILO_MATCH_COLOR[0]
g = utils.KILO_MATCH_COLOR[1]
b = utils.KILO_MATCH_COLOR[2]
case utils.HL_STRING:
r = utils.KILO_STRING_COLOR[0]
g = utils.KILO_STRING_COLOR[1]
b = utils.KILO_STRING_COLOR[2]
case utils.HL_COMMENT, utils.HL_MLCOMMENT:
r = utils.KILO_COMMENT_COLOR[0]
g = utils.KILO_COMMENT_COLOR[1]
b = utils.KILO_COMMENT_COLOR[2]
case utils.HL_KEYWORD:
r = utils.KILO_KEYWORD_COLOR[0]
g = utils.KILO_KEYWORD_COLOR[1]
b = utils.KILO_KEYWORD_COLOR[2]
case utils.HL_TYPE_KEYWORD:
r = utils.KILO_TYPE_COLOR[0]
g = utils.KILO_TYPE_COLOR[1]
b = utils.KILO_TYPE_COLOR[2]
default:
r = utils.KILO_DEFAULT_COLOR[0]
g = utils.KILO_DEFAULT_COLOR[1]
b = utils.KILO_DEFAULT_COLOR[2]
}
return
}
Reading a TOML file
We now have to be able to read a .toml file, file in which we will be able to save any configuration variables that we will be creating. To do so we will be using the github.com/BurntSushi/toml library, so let's install it
go get github.com/BurntSushi/toml
File: utils/toml.go
package utils
import (
"fmt"
"os"
"path"
"github.com/BurntSushi/toml"
)
type Settings struct {
QuitTimes int `toml:"quit_times"`
TabStop int `toml:"tab_stop"`
}
type TomlConfig struct {
Settings Settings
Theme map[string][3]uint8
}
func LoadTOML() error {
var config TomlConfig = TomlConfig{}
dir, err := os.UserConfigDir()
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %s\r\n", err)
return err
}
if err = os.MkdirAll(path.Join(dir, "kilo"), 0o755); err != nil {
fmt.Fprintf(os.Stderr, "Error: %s\r\n", err)
return err
}
filepath := path.Join(dir, "kilo", "config.toml")
if _, err = os.Stat(filepath); os.IsNotExist(err) {
return nil
}
_, err = toml.DecodeFile(filepath, &config)
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %s\r\n", err)
return err
}
return nil
}
os.UserConfigDir()will return the user configuration directory, in Linux like systems will return the.configfolder inside the user's home directory
os.MkdirAll()will create a directory if it doesn't exists, if it does exists it will do nothing returningnilfor an error
File: main.go
func init() {
...
utils.LoadTOML()
}
File: ${XDG_CONFIG}/kilo/config.toml
[settings]
tab_stop = 2
quit_times = 2
[theme]
comment=[0,25,255]
default=[240,240, 240]
keyword=[255,239,0]
number=[255,0,0]
search=[51,255,0]
string=[255,39,155]
type=[12,239,55]
Assigning the values
The final step is to assign these values to their respective variables within the application. It is important to note that we've created values that will flag if the config file is modifying a variable
File: utils/toml.go
func LoadTOML() error {
var config TomlConfig = TomlConfig{
Settings: Settings{
QuitTimes: -1,
TabStop: -1,
},
}
...
if config.Settings.QuitTimes >= 0 {
KILO_QUIT_TIMES = config.Settings.QuitTimes
}
if config.Settings.TabStop >= 0 {
KILO_TAB_STOP = config.Settings.TabStop
}
var val [3]uint8
var ok bool
if val, ok = config.Theme["default"]; ok {
KILO_DEFAULT_COLOR = val
}
if val, ok = config.Theme["number"]; ok {
KILO_NUMBER_COLOR = val
}
if val, ok = config.Theme["match"]; ok {
KILO_MATCH_COLOR = val
}
if val, ok = config.Theme["string"]; ok {
KILO_STRING_COLOR = val
}
if val, ok = config.Theme["comment"]; ok {
KILO_COMMENT_COLOR = val
}
if val, ok = config.Theme["keyword"]; ok {
KILO_KEYWORD_COLOR = val
}
if val, ok = config.Theme["type"]; ok {
KILO_TYPE_COLOR = val
}
...
}
Top comments (2)
Another great article @alcb1310. One reminder for readers is not to add confidential information or app secrets (like API keys) in configuration files that are maintained in a public repo
Thanks, however in this case it is not a config file that will live in the repo. This configuration file will be located inside the
.configfolder, so you can distribute a single executable for this project and by changing that config file you can adjust the app's behavior