Add almost full option support

This commit is contained in:
Zachary Yedidia 2019-01-13 21:06:58 -05:00
parent 6c1db53b65
commit a5e7122b30
16 changed files with 285 additions and 279 deletions

View file

@ -1009,6 +1009,7 @@ func (h *BufHandler) Escape() bool {
// Quit this will close the current tab or view that is open
func (h *BufHandler) Quit() bool {
quit := func() {
h.Buf.Close()
if len(MainTab().Panes) > 1 {
h.Unsplit()
} else if len(Tabs.List) > 1 {

View file

@ -6,7 +6,6 @@ import (
"syscall"
"github.com/zyedidia/micro/cmd/micro/screen"
"github.com/zyedidia/micro/cmd/micro/util"
)
// Suspend sends micro to the background. This is the same as pressing CtrlZ in most unix programs.
@ -19,7 +18,7 @@ func (*BufHandler) Suspend() bool {
pid := syscall.Getpid()
err := syscall.Kill(pid, syscall.SIGSTOP)
if err != nil {
util.TermMessage(err)
screen.TermMessage(err)
}
screen.TempStart(screenb)

View file

@ -10,7 +10,7 @@ import (
"github.com/flynn/json5"
"github.com/zyedidia/micro/cmd/micro/config"
"github.com/zyedidia/micro/cmd/micro/util"
"github.com/zyedidia/micro/cmd/micro/screen"
"github.com/zyedidia/tcell"
)
@ -24,13 +24,13 @@ func InitBindings() {
if _, e := os.Stat(filename); e == nil {
input, err := ioutil.ReadFile(filename)
if err != nil {
util.TermMessage("Error reading bindings.json file: " + err.Error())
screen.TermMessage("Error reading bindings.json file: " + err.Error())
return
}
err = json5.Unmarshal(input, &parsed)
if err != nil {
util.TermMessage("Error reading bindings.json:", err.Error())
screen.TermMessage("Error reading bindings.json:", err.Error())
}
}
@ -45,7 +45,7 @@ func InitBindings() {
func BindKey(k, v string) {
event, ok := findEvent(k)
if !ok {
util.TermMessage(k, "is not a bindable event")
screen.TermMessage(k, "is not a bindable event")
}
switch e := event.(type) {

View file

@ -5,7 +5,7 @@ import (
"github.com/zyedidia/micro/cmd/micro/buffer"
"github.com/zyedidia/micro/cmd/micro/display"
"github.com/zyedidia/micro/cmd/micro/util"
"github.com/zyedidia/micro/cmd/micro/screen"
"github.com/zyedidia/tcell"
)
@ -28,7 +28,7 @@ func BufMapKey(k Event, action string) {
BufKeyStrings[k] = action
BufKeyBindings[k] = f
} else {
util.TermMessage("Error:", action, "does not exist")
screen.TermMessage("Error:", action, "does not exist")
}
}
@ -43,7 +43,7 @@ func BufMapMouse(k MouseEvent, action string) {
// ensure we don't double bind a key
delete(BufMouseBindings, k)
} else {
util.TermMessage("Error:", action, "does not exist")
screen.TermMessage("Error:", action, "does not exist")
}
}
@ -268,6 +268,9 @@ func (h *BufHandler) HSplitBuf(buf *buffer.Buffer) {
MainTab().Resize()
MainTab().SetActive(len(MainTab().Panes) - 1)
}
func (h *BufHandler) Close() {
h.Buf.Close()
}
// BufKeyActions contains the list of all possible key actions the bufhandler could execute
var BufKeyActions = map[string]BufKeyAction{

View file

@ -257,12 +257,86 @@ func (h *BufHandler) NewTabCmd(args []string) {
}
}
func SetGlobalOption(option, value string) error {
if _, ok := config.GlobalSettings[option]; !ok {
return config.ErrInvalidOption
}
nativeValue, err := config.GetNativeValue(option, config.GlobalSettings[option], value)
if err != nil {
return err
}
config.GlobalSettings[option] = nativeValue
if option == "colorscheme" {
// LoadSyntaxFiles()
config.InitColorscheme()
for _, b := range buffer.OpenBuffers {
b.UpdateRules()
}
}
// TODO: info and keymenu option change
// if option == "infobar" || option == "keymenu" {
// for _, tab := range tabs {
// tab.Resize()
// }
// }
if option == "mouse" {
if !nativeValue.(bool) {
screen.Screen.DisableMouse()
} else {
screen.Screen.EnableMouse()
}
}
for _, b := range buffer.OpenBuffers {
b.SetOption(option, value)
}
config.WriteSettings(config.ConfigDir + "/settings.json")
return nil
}
// SetCmd sets an option
func (h *BufHandler) SetCmd(args []string) {
if len(args) < 2 {
InfoBar.Error("Not enough arguments")
return
}
option := args[0]
value := args[1]
err := SetGlobalOption(option, value)
if err == config.ErrInvalidOption {
err := h.Buf.SetOption(option, value)
if err != nil {
InfoBar.Error(err)
}
} else if err != nil {
InfoBar.Error(err)
}
}
// SetLocalCmd sets an option local to the buffer
func (h *BufHandler) SetLocalCmd(args []string) {
if len(args) < 2 {
InfoBar.Error("Not enough arguments")
return
}
option := args[0]
value := args[1]
err := h.Buf.SetOption(option, value)
if err != nil {
InfoBar.Error(err)
}
}
// ShowCmd shows the value of the given option
@ -341,6 +415,8 @@ func (h *BufHandler) TermCmd(args []string) {
h.AddTab()
i = 0
id = MainTab().Panes[0].ID()
} else {
MainTab().Panes[i].Close()
}
v := h.GetView()

View file

@ -11,6 +11,7 @@ type Pane interface {
display.Window
ID() uint64
Name() string
Close()
}
type EditPane struct {

View file

@ -31,7 +31,10 @@ func (t *TermHandler) ID() uint64 {
return t.id
}
func (t *TermHandler) Close() {}
func (t *TermHandler) Quit() {
t.Close()
if len(MainTab().Panes) > 1 {
t.Unsplit()
} else if len(Tabs.List) > 1 {

View file

@ -1,7 +1,6 @@
package buffer
import (
"bufio"
"crypto/md5"
"errors"
"io"
@ -14,39 +13,11 @@ import (
"github.com/zyedidia/micro/cmd/micro/config"
"github.com/zyedidia/micro/cmd/micro/highlight"
"github.com/zyedidia/micro/cmd/micro/screen"
. "github.com/zyedidia/micro/cmd/micro/util"
)
// LargeFileThreshold is the number of bytes when fastdirty is forced
// because hashing is too slow
const LargeFileThreshold = 50000
// overwriteFile opens the given file for writing, truncating if one exists, and then calls
// the supplied function with the file as io.Writer object, also making sure the file is
// closed afterwards.
func overwriteFile(name string, fn func(io.Writer) error) (err error) {
var file *os.File
if file, err = os.OpenFile(name, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644); err != nil {
return
}
defer func() {
if e := file.Close(); e != nil && err == nil {
err = e
}
}()
w := bufio.NewWriter(file)
if err = fn(w); err != nil {
return
}
err = w.Flush()
return
}
var OpenBuffers []*Buffer
// The BufType defines what kind of buffer this is
type BufType struct {
@ -63,6 +34,8 @@ var (
BTScratch = BufType{3, false, true, false}
BTRaw = BufType{4, true, true, false}
BTInfo = BufType{5, false, true, false}
ErrFileTooLarge = errors.New("File is too large to hash")
)
// Buffer stores the main information about a currently open file including
@ -149,6 +122,8 @@ func NewBufferFromString(text, path string, btype BufType) *Buffer {
// Ensure that ReadSettings and InitGlobalSettings have been called before creating
// a new buffer
func NewBuffer(reader io.Reader, size int64, path string, cursorPosition []string, btype BufType) *Buffer {
absPath, _ := filepath.Abs(path)
b := new(Buffer)
b.Type = btype
@ -162,8 +137,6 @@ func NewBuffer(reader io.Reader, size int64, path string, cursorPosition []strin
b.LineArray = NewLineArray(uint64(size), FFAuto, reader)
absPath, _ := filepath.Abs(path)
b.Path = path
b.AbsPath = absPath
@ -187,7 +160,7 @@ func NewBuffer(reader io.Reader, size int64, path string, cursorPosition []strin
if b.Settings["savecursor"].(bool) || b.Settings["saveundo"].(bool) {
err := b.Unserialize()
if err != nil {
TermMessage(err)
screen.TermMessage(err)
}
}
@ -200,9 +173,23 @@ func NewBuffer(reader io.Reader, size int64, path string, cursorPosition []strin
}
}
OpenBuffers = append(OpenBuffers, b)
return b
}
// Close removes this buffer from the list of open buffers
func (b *Buffer) Close() {
for i, buf := range OpenBuffers {
if b == buf {
copy(OpenBuffers[i:], OpenBuffers[i+1:])
OpenBuffers[len(OpenBuffers)-1] = nil
OpenBuffers = OpenBuffers[:len(OpenBuffers)-1]
return
}
}
}
// GetName returns the name that should be displayed in the statusline
// for this buffer
func (b *Buffer) GetName() string {
@ -294,19 +281,37 @@ func (b *Buffer) Modified() bool {
}
// calcHash calculates md5 hash of all lines in the buffer
func calcHash(b *Buffer, out *[md5.Size]byte) {
func calcHash(b *Buffer, out *[md5.Size]byte) error {
h := md5.New()
size := 0
if len(b.lines) > 0 {
h.Write(b.lines[0].data)
n, e := h.Write(b.lines[0].data)
if e != nil {
return e
}
size += n
for _, l := range b.lines[1:] {
h.Write([]byte{'\n'})
h.Write(l.data)
n, e = h.Write([]byte{'\n'})
if e != nil {
return e
}
size += n
n, e = h.Write(l.data)
if e != nil {
return e
}
size += n
}
}
if size > LargeFileThreshold {
return ErrFileTooLarge
}
h.Sum((*out)[:0])
return nil
}
func (b *Buffer) insert(pos Loc, value []byte) {
@ -334,16 +339,16 @@ func (b *Buffer) UpdateRules() {
for _, f := range config.ListRuntimeFiles(config.RTSyntax) {
data, err := f.Data()
if err != nil {
TermMessage("Error loading syntax file " + f.Name() + ": " + err.Error())
screen.TermMessage("Error loading syntax file " + f.Name() + ": " + err.Error())
} else {
file, err := highlight.ParseFile(data)
if err != nil {
TermMessage("Error loading syntax file " + f.Name() + ": " + err.Error())
screen.TermMessage("Error loading syntax file " + f.Name() + ": " + err.Error())
continue
}
ftdetect, err := highlight.ParseFtDetect(file)
if err != nil {
TermMessage("Error loading syntax file " + f.Name() + ": " + err.Error())
screen.TermMessage("Error loading syntax file " + f.Name() + ": " + err.Error())
continue
}
@ -355,7 +360,7 @@ func (b *Buffer) UpdateRules() {
header.FtDetect = ftdetect
b.SyntaxDef, err = highlight.ParseDef(file, header)
if err != nil {
TermMessage("Error loading syntax file " + f.Name() + ": " + err.Error())
screen.TermMessage("Error loading syntax file " + f.Name() + ": " + err.Error())
continue
}
rehighlight = true
@ -367,7 +372,7 @@ func (b *Buffer) UpdateRules() {
header.FtDetect = ftdetect
b.SyntaxDef, err = highlight.ParseDef(file, header)
if err != nil {
TermMessage("Error loading syntax file " + f.Name() + ": " + err.Error())
screen.TermMessage("Error loading syntax file " + f.Name() + ": " + err.Error())
continue
}
rehighlight = true
@ -392,6 +397,14 @@ func (b *Buffer) UpdateRules() {
}
}
// ClearMatches clears all of the syntax highlighting for the buffer
func (b *Buffer) ClearMatches() {
for i := range b.lines {
b.SetMatch(i, nil)
b.SetState(i, nil)
}
}
// IndentString returns this buffer's indent method (a tabstop or n spaces
// depending on the settings)
func (b *Buffer) IndentString(tabsize int) string {

View file

@ -188,7 +188,7 @@ func (eh *EventHandler) Execute(t *TextEvent) {
// for pl := range loadedPlugins {
// ret, err := Call(pl+".onBeforeTextEvent", t)
// if err != nil && !strings.HasPrefix(err.Error(), "function does not exist") {
// util.TermMessage(err)
// screen.TermMessage(err)
// }
// if val, ok := ret.(lua.LBool); ok && val == lua.LFalse {
// return

View file

@ -1,6 +1,7 @@
package buffer
import (
"bufio"
"bytes"
"io"
"os"
@ -8,11 +9,40 @@ import (
"os/signal"
"path/filepath"
. "github.com/zyedidia/micro/cmd/micro/util"
"github.com/zyedidia/micro/cmd/micro/config"
. "github.com/zyedidia/micro/cmd/micro/util"
)
// LargeFileThreshold is the number of bytes when fastdirty is forced
// because hashing is too slow
const LargeFileThreshold = 50000
// overwriteFile opens the given file for writing, truncating if one exists, and then calls
// the supplied function with the file as io.Writer object, also making sure the file is
// closed afterwards.
func overwriteFile(name string, fn func(io.Writer) error) (err error) {
var file *os.File
if file, err = os.OpenFile(name, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644); err != nil {
return
}
defer func() {
if e := file.Close(); e != nil && err == nil {
err = e
}
}()
w := bufio.NewWriter(file)
if err = fn(w); err != nil {
return
}
err = w.Flush()
return
}
// Save saves the buffer to its default path
func (b *Buffer) Save() error {
return b.SaveAs(b.Path)

View file

@ -0,0 +1,43 @@
package buffer
import (
"github.com/zyedidia/micro/cmd/micro/config"
"github.com/zyedidia/micro/cmd/micro/screen"
)
// SetOption sets a given option to a value just for this buffer
func (b *Buffer) SetOption(option, value string) error {
if _, ok := b.Settings[option]; !ok {
return config.ErrInvalidOption
}
nativeValue, err := config.GetNativeValue(option, b.Settings[option], value)
if err != nil {
return err
}
b.Settings[option] = nativeValue
if option == "fastdirty" {
if !nativeValue.(bool) {
e := calcHash(b, &b.origHash)
if e == ErrFileTooLarge {
b.Settings["fastdirty"] = false
}
}
} else if option == "statusline" {
screen.Redraw()
} else if option == "filetype" {
b.UpdateRules()
} else if option == "fileformat" {
b.isModified = true
} else if option == "syntax" {
if !nativeValue.(bool) {
b.ClearMatches()
} else {
b.UpdateRules()
}
}
return nil
}

View file

@ -6,19 +6,26 @@ import (
"io/ioutil"
"os"
"reflect"
"strconv"
"strings"
"github.com/flynn/json5"
"github.com/zyedidia/glob"
"github.com/zyedidia/micro/cmd/micro/util"
)
type optionValidator func(string, interface{}) error
// The options that the user can set
var GlobalSettings map[string]interface{}
var (
ErrInvalidOption = errors.New("Invalid option")
ErrInvalidValue = errors.New("Invalid value")
// This is the raw parsed json
var parsedSettings map[string]interface{}
// The options that the user can set
GlobalSettings map[string]interface{}
// This is the raw parsed json
parsedSettings map[string]interface{}
)
// Options with validators
var optionValidators = map[string]optionValidator{
@ -120,22 +127,6 @@ func GetGlobalOption(name string) interface{} {
return GlobalSettings[name]
}
// GetLocalOption returns the local value of the given option
// func GetLocalOption(name string, buf *Buffer) interface{} {
// return buf.Settings[name]
// }
// TODO: get option for current buffer
// GetOption returns the value of the given option
// If there is a local version of the option, it returns that
// otherwise it will return the global version
// func GetOption(name string) interface{} {
// if GetLocalOption(name, CurView().Buf) != nil {
// return GetLocalOption(name, CurView().Buf)
// }
// return GetGlobalOption(name)
// }
func DefaultCommonSettings() map[string]interface{} {
return map[string]interface{}{
"autoindent": true,
@ -196,199 +187,35 @@ func DefaultLocalSettings() map[string]interface{} {
return common
}
// TODO: everything else
func GetNativeValue(option string, realValue interface{}, value string) (interface{}, error) {
var native interface{}
kind := reflect.TypeOf(realValue).Kind()
if kind == reflect.Bool {
b, err := util.ParseBool(value)
if err != nil {
return nil, ErrInvalidValue
}
native = b
} else if kind == reflect.String {
native = value
} else if kind == reflect.Float64 {
i, err := strconv.Atoi(value)
if err != nil {
return nil, ErrInvalidValue
}
native = float64(i)
} else {
return nil, ErrInvalidValue
}
// SetOption attempts to set the given option to the value
// By default it will set the option as global, but if the option
// is local only it will set the local version
// Use setlocal to force an option to be set locally
// func SetOption(option, value string) error {
// if _, ok := GlobalSettings[option]; !ok {
// if _, ok := CurView().Buf.Settings[option]; !ok {
// return errors.New("Invalid option")
// }
// SetLocalOption(option, value, CurView())
// return nil
// }
//
// var nativeValue interface{}
//
// kind := reflect.TypeOf(GlobalSettings[option]).Kind()
// if kind == reflect.Bool {
// b, err := ParseBool(value)
// if err != nil {
// return errors.New("Invalid value")
// }
// nativeValue = b
// } else if kind == reflect.String {
// nativeValue = value
// } else if kind == reflect.Float64 {
// i, err := strconv.Atoi(value)
// if err != nil {
// return errors.New("Invalid value")
// }
// nativeValue = float64(i)
// } else {
// return errors.New("Option has unsupported value type")
// }
//
// if err := optionIsValid(option, nativeValue); err != nil {
// return err
// }
//
// GlobalSettings[option] = nativeValue
//
// if option == "colorscheme" {
// // LoadSyntaxFiles()
// InitColorscheme()
// for _, tab := range tabs {
// for _, view := range tab.Views {
// view.Buf.UpdateRules()
// }
// }
// }
//
// if option == "infobar" || option == "keymenu" {
// for _, tab := range tabs {
// tab.Resize()
// }
// }
//
// if option == "mouse" {
// if !nativeValue.(bool) {
// screen.DisableMouse()
// } else {
// screen.EnableMouse()
// }
// }
//
// if len(tabs) != 0 {
// if _, ok := CurView().Buf.Settings[option]; ok {
// for _, tab := range tabs {
// for _, view := range tab.Views {
// SetLocalOption(option, value, view)
// }
// }
// }
// }
//
// return nil
// }
//
// // SetLocalOption sets the local version of this option
// func SetLocalOption(option, value string, view *View) error {
// buf := view.Buf
// if _, ok := buf.Settings[option]; !ok {
// return errors.New("Invalid option")
// }
//
// var nativeValue interface{}
//
// kind := reflect.TypeOf(buf.Settings[option]).Kind()
// if kind == reflect.Bool {
// b, err := ParseBool(value)
// if err != nil {
// return errors.New("Invalid value")
// }
// nativeValue = b
// } else if kind == reflect.String {
// nativeValue = value
// } else if kind == reflect.Float64 {
// i, err := strconv.Atoi(value)
// if err != nil {
// return errors.New("Invalid value")
// }
// nativeValue = float64(i)
// } else {
// return errors.New("Option has unsupported value type")
// }
//
// if err := optionIsValid(option, nativeValue); err != nil {
// return err
// }
//
// if option == "fastdirty" {
// // If it is being turned off, we have to hash every open buffer
// var empty [md5.Size]byte
// var wg sync.WaitGroup
//
// for _, tab := range tabs {
// for _, v := range tab.Views {
// if !nativeValue.(bool) {
// if v.Buf.origHash == empty {
// wg.Add(1)
//
// go func(b *Buffer) { // calculate md5 hash of the file
// defer wg.Done()
//
// if file, e := os.Open(b.AbsPath); e == nil {
// defer file.Close()
//
// h := md5.New()
//
// if _, e = io.Copy(h, file); e == nil {
// h.Sum(b.origHash[:0])
// }
// }
// }(v.Buf)
// }
// } else {
// v.Buf.IsModified = v.Buf.Modified()
// }
// }
// }
//
// wg.Wait()
// }
//
// buf.Settings[option] = nativeValue
//
// if option == "statusline" {
// view.ToggleStatusLine()
// }
//
// if option == "filetype" {
// // LoadSyntaxFiles()
// InitColorscheme()
// buf.UpdateRules()
// }
//
// if option == "fileformat" {
// buf.IsModified = true
// }
//
// if option == "syntax" {
// if !nativeValue.(bool) {
// buf.ClearMatches()
// } else {
// if buf.highlighter != nil {
// buf.highlighter.HighlightStates(buf)
// }
// }
// }
//
// return nil
// }
//
// // SetOptionAndSettings sets the given option and saves the option setting to the settings config file
// func SetOptionAndSettings(option, value string) {
// filename := ConfigDir + "/settings.json"
//
// err := SetOption(option, value)
//
// if err != nil {
// messenger.Error(err.Error())
// return
// }
//
// err = WriteSettings(filename)
// if err != nil {
// messenger.Error("Error writing to settings.json: " + err.Error())
// return
// }
// }
if err := OptionIsValid(option, native); err != nil {
return nil, err
}
return native, nil
}
func optionIsValid(option string, value interface{}) error {
// OptionIsValid checks if a value is valid for a certain option
func OptionIsValid(option string, value interface{}) error {
if validator, ok := optionValidators[option]; ok {
return validator(option, value)
}

View file

@ -13,7 +13,6 @@ import (
"github.com/zyedidia/micro/cmd/micro/buffer"
"github.com/zyedidia/micro/cmd/micro/config"
"github.com/zyedidia/micro/cmd/micro/screen"
"github.com/zyedidia/micro/cmd/micro/util"
"github.com/zyedidia/tcell"
)
@ -123,7 +122,7 @@ func LoadInput() []*buffer.Buffer {
buf, err := buffer.NewBufferFromFile(args[i], buffer.BTDefault)
if err != nil {
util.TermMessage(err)
screen.TermMessage(err)
continue
}
// If the file didn't exist, input will be empty, and we'll open an empty buffer
@ -135,7 +134,7 @@ func LoadInput() []*buffer.Buffer {
// and we should read from stdin
input, err = ioutil.ReadAll(os.Stdin)
if err != nil {
util.TermMessage("Error reading from stdin: ", err)
screen.TermMessage("Error reading from stdin: ", err)
input = []byte{}
}
buffers = append(buffers, buffer.NewBufferFromString(string(input), filename, buffer.BTDefault))
@ -154,12 +153,12 @@ func main() {
InitFlags()
err = config.InitConfigDir(*flagConfigDir)
if err != nil {
util.TermMessage(err)
screen.TermMessage(err)
}
config.InitRuntimeFiles()
err = config.ReadSettings()
if err != nil {
util.TermMessage(err)
screen.TermMessage(err)
}
config.InitGlobalSettings()
action.InitBindings()
@ -167,7 +166,7 @@ func main() {
err = config.InitColorscheme()
if err != nil {
util.TermMessage(err)
screen.TermMessage(err)
}
screen.Init()

View file

@ -1,12 +1,10 @@
package util
package screen
import (
"bufio"
"fmt"
"os"
"strconv"
"github.com/zyedidia/micro/cmd/micro/screen"
)
// TermMessage sends a message to the user in the terminal. This usually occurs before
@ -16,7 +14,7 @@ import (
// This will write the message, and wait for the user
// to press and key to continue
func TermMessage(msg ...interface{}) {
screenb := screen.TempFini()
screenb := TempFini()
fmt.Println(msg...)
fmt.Print("\nPress enter to continue")
@ -24,7 +22,7 @@ func TermMessage(msg ...interface{}) {
reader := bufio.NewReader(os.Stdin)
reader.ReadString('\n')
screen.TempStart(screenb)
TempStart(screenb)
}
// TermError sends an error to the user in the terminal. Like TermMessage except formatted

View file

@ -11,7 +11,6 @@ import (
"github.com/zyedidia/micro/cmd/micro/screen"
"github.com/zyedidia/micro/cmd/micro/shellwords"
"github.com/zyedidia/micro/cmd/micro/util"
)
// ExecCommand executes a command using exec
@ -108,7 +107,7 @@ func RunInteractiveShell(input string, wait bool, getOutput bool) (string, error
if wait {
// This is just so we don't return right away and let the user press enter to return
util.TermMessage("")
screen.TermMessage("")
}
// Start the screen back up

View file

@ -6,6 +6,7 @@ import (
"os/user"
"path/filepath"
"regexp"
"strconv"
"strings"
"time"
"unicode/utf8"
@ -309,6 +310,19 @@ func GetCharPosInLine(b []byte, visualPos int, tabsize int) int {
return i
}
// ParseBool is almost exactly like strconv.ParseBool, except it also accepts 'on' and 'off'
// as 'true' and 'false' respectively
func ParseBool(str string) (bool, error) {
if str == "on" {
return true, nil
}
if str == "off" {
return false, nil
}
return strconv.ParseBool(str)
}
// Clamp clamps a value between min and max
func Clamp(val, min, max int) int {
if val < min {
val = min