DEV Community

Cover image for Adding Config File
Andres Court
Andres Court

Posted on

Adding Config File

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 MacOS and Linux, do not have a Windows machine 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
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

File: utils/window.go

package utils

import (
    "os"

    "golang.org/x/term"
)

func GetWindowSize() (int, int, error) {
    return term.GetSize(int(os.Stdout.Fd()))
}
Enter fullscreen mode Exit fullscreen mode

File: main.go

func init() {
    ...
    restoreFunc, err = linux.EnableRawMode()
    if err != nil {
        fmt.Fprintf(os.Stderr, "Error: %s\r\n", err)
        os.Exit(1)
    }
}
Enter fullscreen mode Exit fullscreen mode

File: editor/editor.go

func NewEditor(f func()) *EditorConfig {
    cols, rows, err := utils.GetWindowSize()
    if err != nil {
        utils.SafeExit(f, err)
    }
    ...
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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}
)
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

os.UserConfigDir() will return the user configuration directory, in Linux like systems will return the .config folder 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 returning nil for an error

File: main.go

func init() {
    ...
    utils.LoadTOML()
}
Enter fullscreen mode Exit fullscreen mode

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]
Enter fullscreen mode Exit fullscreen mode

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
    }
    ...
}
Enter fullscreen mode Exit fullscreen mode

Top comments (2)

Collapse
 
jim_medlock_3772bba7ea16b profile image
Jim Medlock

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

Collapse
 
alcb1310 profile image
Andres Court

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 .config folder, so you can distribute a single executable for this project and by changing that config file you can adjust the app's behavior