2018-08-27 22:53:10 +03:00
package config
2016-03-29 04:10:10 +03:00
import (
2017-08-06 19:02:17 +03:00
"encoding/json"
2016-08-20 04:47:37 +03:00
"errors"
2020-06-08 00:31:16 +03:00
"fmt"
2016-03-29 04:10:10 +03:00
"io/ioutil"
"os"
2020-02-02 23:30:06 +03:00
"path/filepath"
2016-04-24 22:35:16 +03:00
"reflect"
2019-01-14 05:06:58 +03:00
"strconv"
2016-08-26 03:15:58 +03:00
"strings"
"github.com/zyedidia/glob"
2020-01-02 04:44:45 +03:00
"github.com/zyedidia/json5"
2020-05-04 17:16:15 +03:00
"github.com/zyedidia/micro/v2/internal/util"
2019-01-24 03:06:20 +03:00
"golang.org/x/text/encoding/htmlindex"
2016-03-29 04:10:10 +03:00
)
2016-09-14 22:05:05 +03:00
type optionValidator func ( string , interface { } ) error
2019-01-14 05:06:58 +03:00
var (
ErrInvalidOption = errors . New ( "Invalid option" )
ErrInvalidValue = errors . New ( "Invalid value" )
2016-03-29 04:10:10 +03:00
2019-01-14 05:06:58 +03:00
// The options that the user can set
GlobalSettings map [ string ] interface { }
// This is the raw parsed json
2020-06-24 00:29:20 +03:00
parsedSettings map [ string ] interface { }
settingsParseError bool
2020-06-09 05:19:15 +03:00
// ModifiedSettings is a map of settings which should be written to disk
// because they have been modified by the user in this session
ModifiedSettings map [ string ] bool
2019-01-14 05:06:58 +03:00
)
2017-08-06 19:02:17 +03:00
2019-08-19 01:13:43 +03:00
func init ( ) {
2020-06-09 05:19:15 +03:00
ModifiedSettings = make ( map [ string ] bool )
2019-08-19 01:13:43 +03:00
parsedSettings = make ( map [ string ] interface { } )
}
2016-09-14 22:05:05 +03:00
// Options with validators
var optionValidators = map [ string ] optionValidator {
2020-02-09 00:53:08 +03:00
"autosave" : validateNonNegativeValue ,
2020-07-05 03:00:39 +03:00
"clipboard" : validateClipboard ,
2016-09-14 22:05:05 +03:00
"tabsize" : validatePositiveValue ,
"scrollmargin" : validateNonNegativeValue ,
"scrollspeed" : validateNonNegativeValue ,
"colorscheme" : validateColorscheme ,
"colorcolumn" : validateNonNegativeValue ,
2017-08-24 20:13:14 +03:00
"fileformat" : validateLineEnding ,
2019-01-24 03:06:20 +03:00
"encoding" : validateEncoding ,
2016-09-14 22:05:05 +03:00
}
2018-08-26 06:06:44 +03:00
func ReadSettings ( ) error {
2020-02-02 23:30:06 +03:00
filename := filepath . Join ( ConfigDir , "settings.json" )
2016-03-29 04:10:10 +03:00
if _ , e := os . Stat ( filename ) ; e == nil {
input , err := ioutil . ReadFile ( filename )
2018-08-26 06:06:44 +03:00
if err != nil {
2020-06-24 00:29:20 +03:00
settingsParseError = true
2018-08-26 06:06:44 +03:00
return errors . New ( "Error reading settings.json file: " + err . Error ( ) )
}
2016-08-30 18:19:51 +03:00
if ! strings . HasPrefix ( string ( input ) , "null" ) {
2018-08-26 06:06:44 +03:00
// Unmarshal the input into the parsed map
err = json5 . Unmarshal ( input , & parsedSettings )
2016-08-30 18:19:51 +03:00
if err != nil {
2020-06-24 00:29:20 +03:00
settingsParseError = true
2018-08-26 06:06:44 +03:00
return errors . New ( "Error reading settings.json: " + err . Error ( ) )
2016-08-30 18:19:51 +03:00
}
2020-02-09 00:53:08 +03:00
// check if autosave is a boolean and convert it to float if so
if v , ok := parsedSettings [ "autosave" ] ; ok {
s , ok := v . ( bool )
if ok {
if s {
parsedSettings [ "autosave" ] = 8.0
} else {
parsedSettings [ "autosave" ] = 0.0
}
}
}
2016-04-24 16:01:01 +03:00
}
2016-04-30 22:02:33 +03:00
}
2018-08-26 06:06:44 +03:00
return nil
}
2016-04-30 22:02:33 +03:00
2020-06-08 00:31:16 +03:00
func verifySetting ( option string , value reflect . Type , def reflect . Type ) bool {
var interfaceArr [ ] interface { }
switch option {
case "pluginrepos" , "pluginchannels" :
return value . AssignableTo ( reflect . TypeOf ( interfaceArr ) )
default :
return def . AssignableTo ( value )
}
}
2018-08-26 06:06:44 +03:00
// InitGlobalSettings initializes the options map and sets all options to their default values
// Must be called after ReadSettings
2020-06-08 00:31:16 +03:00
func InitGlobalSettings ( ) error {
var err error
2018-08-27 22:53:10 +03:00
GlobalSettings = DefaultGlobalSettings ( )
2018-08-26 06:06:44 +03:00
for k , v := range parsedSettings {
2016-08-26 03:15:58 +03:00
if ! strings . HasPrefix ( reflect . TypeOf ( v ) . String ( ) , "map" ) {
2020-06-08 00:31:16 +03:00
if _ , ok := GlobalSettings [ k ] ; ok && ! verifySetting ( k , reflect . TypeOf ( v ) , reflect . TypeOf ( GlobalSettings [ k ] ) ) {
2020-09-16 07:08:01 +03:00
err = fmt . Errorf ( "Global Error: setting '%s' has incorrect type (%s), using default value: %v (%s)" , k , reflect . TypeOf ( v ) , GlobalSettings [ k ] , reflect . TypeOf ( GlobalSettings [ k ] ) )
2020-06-08 00:31:16 +03:00
continue
}
2018-08-27 22:53:10 +03:00
GlobalSettings [ k ] = v
2016-08-26 03:15:58 +03:00
}
2016-04-30 22:02:33 +03:00
}
2020-06-08 00:31:16 +03:00
return err
2016-08-26 03:15:58 +03:00
}
// InitLocalSettings scans the json in settings.json and sets the options locally based
2018-08-27 22:53:10 +03:00
// on whether the filetype or path matches ft or glob local settings
2018-08-26 06:06:44 +03:00
// Must be called after ReadSettings
2018-08-27 22:53:10 +03:00
func InitLocalSettings ( settings map [ string ] interface { } , path string ) error {
2018-08-26 06:06:44 +03:00
var parseError error
for k , v := range parsedSettings {
2016-08-26 03:15:58 +03:00
if strings . HasPrefix ( reflect . TypeOf ( v ) . String ( ) , "map" ) {
2017-12-04 07:38:09 +03:00
if strings . HasPrefix ( k , "ft:" ) {
2018-08-27 22:53:10 +03:00
if settings [ "filetype" ] . ( string ) == k [ 3 : ] {
2017-12-04 07:38:09 +03:00
for k1 , v1 := range v . ( map [ string ] interface { } ) {
2020-06-08 00:31:16 +03:00
if _ , ok := settings [ k1 ] ; ok && ! verifySetting ( k1 , reflect . TypeOf ( v1 ) , reflect . TypeOf ( settings [ k1 ] ) ) {
2020-09-16 07:08:01 +03:00
parseError = fmt . Errorf ( "Error: setting '%s' has incorrect type (%s), using default value: %v (%s)" , k , reflect . TypeOf ( v1 ) , settings [ k1 ] , reflect . TypeOf ( settings [ k1 ] ) )
2020-06-08 00:31:16 +03:00
continue
}
2018-08-27 22:53:10 +03:00
settings [ k1 ] = v1
2017-12-04 07:38:09 +03:00
}
}
} else {
g , err := glob . Compile ( k )
if err != nil {
2018-08-26 06:06:44 +03:00
parseError = errors . New ( "Error with glob setting " + k + ": " + err . Error ( ) )
2017-12-04 07:38:09 +03:00
continue
}
2016-08-26 03:15:58 +03:00
2018-08-27 22:53:10 +03:00
if g . MatchString ( path ) {
2017-12-04 07:38:09 +03:00
for k1 , v1 := range v . ( map [ string ] interface { } ) {
2020-06-08 00:31:16 +03:00
if _ , ok := settings [ k1 ] ; ok && ! verifySetting ( k1 , reflect . TypeOf ( v1 ) , reflect . TypeOf ( settings [ k1 ] ) ) {
2020-09-16 07:08:01 +03:00
parseError = fmt . Errorf ( "Error: setting '%s' has incorrect type (%s), using default value: %v (%s)" , k , reflect . TypeOf ( v1 ) , settings [ k1 ] , reflect . TypeOf ( settings [ k1 ] ) )
2020-06-08 00:31:16 +03:00
continue
}
2018-08-27 22:53:10 +03:00
settings [ k1 ] = v1
2017-12-04 07:38:09 +03:00
}
2016-08-26 03:15:58 +03:00
}
}
}
2016-03-29 04:10:10 +03:00
}
2018-08-26 06:06:44 +03:00
return parseError
2016-03-29 04:10:10 +03:00
}
// WriteSettings writes the settings to the specified filename as JSON
func WriteSettings ( filename string ) error {
2020-06-24 00:29:20 +03:00
if settingsParseError {
// Don't write settings if there was a parse error
// because this will delete the settings.json if it
// is invalid. Instead we should allow the user to fix
// it manually.
return nil
}
2016-04-18 04:49:36 +03:00
var err error
2018-08-27 22:53:10 +03:00
if _ , e := os . Stat ( ConfigDir ) ; e == nil {
2020-06-08 22:33:38 +03:00
defaults := DefaultGlobalSettings ( )
// remove any options froms parsedSettings that have since been marked as default
for k , v := range parsedSettings {
if ! strings . HasPrefix ( reflect . TypeOf ( v ) . String ( ) , "map" ) {
cur , okcur := GlobalSettings [ k ]
if def , ok := defaults [ k ] ; ok && okcur && reflect . DeepEqual ( cur , def ) {
delete ( parsedSettings , k )
}
}
}
// add any options to parsedSettings that have since been marked as non-default
2018-08-27 22:53:10 +03:00
for k , v := range GlobalSettings {
2020-06-08 22:33:38 +03:00
if def , ok := defaults [ k ] ; ! ok || ! reflect . DeepEqual ( v , def ) {
2020-06-09 05:19:15 +03:00
if _ , wr := ModifiedSettings [ k ] ; wr {
parsedSettings [ k ] = v
}
2020-06-08 22:33:38 +03:00
}
2016-08-26 03:15:58 +03:00
}
2018-08-26 06:06:44 +03:00
txt , _ := json . MarshalIndent ( parsedSettings , "" , " " )
2016-09-04 17:10:57 +03:00
err = ioutil . WriteFile ( filename , append ( txt , '\n' ) , 0644 )
2016-04-18 04:49:36 +03:00
}
2016-03-29 04:10:10 +03:00
return err
}
2020-06-08 22:33:38 +03:00
// OverwriteSettings writes the current settings to settings.json and
// resets any user configuration of local settings present in settings.json
2020-02-02 23:30:06 +03:00
func OverwriteSettings ( filename string ) error {
2020-06-08 22:33:38 +03:00
settings := make ( map [ string ] interface { } )
2020-02-02 23:30:06 +03:00
var err error
if _ , e := os . Stat ( ConfigDir ) ; e == nil {
2020-06-08 22:33:38 +03:00
defaults := DefaultGlobalSettings ( )
for k , v := range GlobalSettings {
if def , ok := defaults [ k ] ; ! ok || ! reflect . DeepEqual ( v , def ) {
2020-06-09 05:19:15 +03:00
if _ , wr := ModifiedSettings [ k ] ; wr {
settings [ k ] = v
}
2020-06-08 22:33:38 +03:00
}
}
txt , _ := json . MarshalIndent ( settings , "" , " " )
2020-02-02 23:30:06 +03:00
err = ioutil . WriteFile ( filename , append ( txt , '\n' ) , 0644 )
}
return err
}
2020-02-02 22:35:30 +03:00
// RegisterCommonOptionPlug creates a new option (called pl.name). This is meant to be called by plugins to add options.
func RegisterCommonOptionPlug ( pl string , name string , defaultvalue interface { } ) error {
name = pl + "." + name
2020-06-21 03:07:33 +03:00
if _ , ok := GlobalSettings [ name ] ; ! ok {
2019-08-04 01:19:28 +03:00
defaultCommonSettings [ name ] = defaultvalue
GlobalSettings [ name ] = defaultvalue
2020-02-11 21:09:17 +03:00
err := WriteSettings ( filepath . Join ( ConfigDir , "settings.json" ) )
2019-08-04 01:19:28 +03:00
if err != nil {
return errors . New ( "Error writing settings.json file: " + err . Error ( ) )
}
} else {
2020-06-21 03:07:33 +03:00
defaultCommonSettings [ name ] = defaultvalue
2019-08-04 01:19:28 +03:00
}
return nil
}
2020-02-02 22:35:30 +03:00
// RegisterGlobalOptionPlug creates a new global-only option (named pl.name)
func RegisterGlobalOptionPlug ( pl string , name string , defaultvalue interface { } ) error {
return RegisterGlobalOption ( pl + "." + name , defaultvalue )
}
// RegisterGlobalOption creates a new global-only option
2019-08-04 01:19:28 +03:00
func RegisterGlobalOption ( name string , defaultvalue interface { } ) error {
if v , ok := GlobalSettings [ name ] ; ! ok {
2020-02-12 21:18:59 +03:00
DefaultGlobalOnlySettings [ name ] = defaultvalue
2019-08-04 01:19:28 +03:00
GlobalSettings [ name ] = defaultvalue
2020-02-02 23:30:06 +03:00
err := WriteSettings ( filepath . Join ( ConfigDir , "settings.json" ) )
2019-08-04 01:19:28 +03:00
if err != nil {
return errors . New ( "Error writing settings.json file: " + err . Error ( ) )
}
} else {
2020-02-12 21:18:59 +03:00
DefaultGlobalOnlySettings [ name ] = v
2016-04-30 19:13:21 +03:00
}
2018-08-26 06:06:44 +03:00
return nil
2016-04-30 19:13:21 +03:00
}
2016-08-26 00:24:13 +03:00
// GetGlobalOption returns the global value of the given option
2016-08-25 02:55:44 +03:00
func GetGlobalOption ( name string ) interface { } {
2018-08-27 22:53:10 +03:00
return GlobalSettings [ name ]
2016-08-25 02:55:44 +03:00
}
2019-08-04 01:19:28 +03:00
var defaultCommonSettings = map [ string ] interface { } {
"autoindent" : true ,
2020-02-14 04:51:56 +03:00
"autosu" : false ,
2019-12-22 00:35:09 +03:00
"backup" : true ,
2020-06-23 00:54:56 +03:00
"backupdir" : "" ,
2019-08-04 01:19:28 +03:00
"basename" : false ,
"colorcolumn" : float64 ( 0 ) ,
"cursorline" : true ,
2020-02-10 08:30:13 +03:00
"diffgutter" : false ,
2019-08-04 01:19:28 +03:00
"encoding" : "utf-8" ,
2020-02-25 06:31:05 +03:00
"eofnewline" : true ,
2020-02-14 04:51:56 +03:00
"fastdirty" : false ,
2019-08-04 01:19:28 +03:00
"fileformat" : "unix" ,
2019-08-04 03:12:23 +03:00
"filetype" : "unknown" ,
2021-01-27 21:49:38 +03:00
"incsearch" : true ,
2020-11-05 23:39:05 +03:00
"ignorecase" : true ,
2019-08-04 01:19:28 +03:00
"indentchar" : " " ,
"keepautoindent" : false ,
2019-08-05 01:11:09 +03:00
"matchbrace" : true ,
2019-08-04 07:01:57 +03:00
"mkparents" : false ,
2020-06-23 00:54:56 +03:00
"permbackup" : false ,
2019-08-04 03:12:23 +03:00
"readonly" : false ,
2019-08-04 01:19:28 +03:00
"rmtrailingws" : false ,
"ruler" : true ,
2020-05-29 05:24:09 +03:00
"relativeruler" : false ,
2019-08-04 01:19:28 +03:00
"savecursor" : false ,
"saveundo" : false ,
"scrollbar" : false ,
"scrollmargin" : float64 ( 3 ) ,
"scrollspeed" : float64 ( 2 ) ,
"smartpaste" : true ,
"softwrap" : false ,
"splitbottom" : true ,
"splitright" : true ,
2020-02-07 19:32:12 +03:00
"statusformatl" : "$(filename) $(modified)($(line),$(col)) $(status.paste)| ft:$(opt:filetype) | $(opt:fileformat) | $(opt:encoding)" ,
2019-12-26 20:25:42 +03:00
"statusformatr" : "$(bind:ToggleKeyMenu): bindings, $(bind:ToggleHelp): help" ,
2019-08-04 01:19:28 +03:00
"statusline" : true ,
"syntax" : true ,
"tabmovement" : false ,
"tabsize" : float64 ( 4 ) ,
"tabstospaces" : false ,
"useprimary" : true ,
2016-08-25 22:03:37 +03:00
}
2019-01-15 06:16:44 +03:00
func GetInfoBarOffset ( ) int {
2019-01-17 02:37:45 +03:00
offset := 0
2019-01-15 06:16:44 +03:00
if GetGlobalOption ( "infobar" ) . ( bool ) {
2019-01-17 02:37:45 +03:00
offset ++
2019-01-15 06:16:44 +03:00
}
2019-01-17 02:37:45 +03:00
if GetGlobalOption ( "keymenu" ) . ( bool ) {
offset += 2
}
return offset
2019-01-15 06:16:44 +03:00
}
2019-08-04 03:12:23 +03:00
// DefaultCommonSettings returns the default global settings for micro
// Note that colorscheme is a global only option
func DefaultCommonSettings ( ) map [ string ] interface { } {
commonsettings := make ( map [ string ] interface { } )
for k , v := range defaultCommonSettings {
commonsettings [ k ] = v
}
return commonsettings
}
2020-01-21 06:03:32 +03:00
// a list of settings that should only be globally modified and their
// default values
2020-02-12 21:18:59 +03:00
var DefaultGlobalOnlySettings = map [ string ] interface { } {
2020-02-09 00:53:08 +03:00
"autosave" : float64 ( 0 ) ,
2020-07-05 03:00:39 +03:00
"clipboard" : "external" ,
2020-02-02 07:54:38 +03:00
"colorscheme" : "default" ,
2020-05-21 21:35:54 +03:00
"divchars" : "|-" ,
"divreverse" : true ,
2020-02-02 07:54:38 +03:00
"infobar" : true ,
"keymenu" : false ,
"mouse" : true ,
2020-05-29 21:55:24 +03:00
"parsecursor" : false ,
2020-02-02 07:54:38 +03:00
"paste" : false ,
"savehistory" : true ,
"sucmd" : "sudo" ,
"pluginchannels" : [ ] string { "https://raw.githubusercontent.com/micro-editor/plugin-channel/master/channel.json" } ,
"pluginrepos" : [ ] string { } ,
2020-02-15 20:53:17 +03:00
"xterm" : false ,
2019-08-04 01:19:28 +03:00
}
2020-01-21 06:03:32 +03:00
// a list of settings that should never be globally modified
var LocalSettings = [ ] string {
"filetype" ,
"readonly" ,
}
2018-08-26 06:06:44 +03:00
// DefaultGlobalSettings returns the default global settings for micro
// Note that colorscheme is a global only option
func DefaultGlobalSettings ( ) map [ string ] interface { } {
2019-08-04 01:19:28 +03:00
globalsettings := make ( map [ string ] interface { } )
for k , v := range defaultCommonSettings {
globalsettings [ k ] = v
}
2020-02-12 21:18:59 +03:00
for k , v := range DefaultGlobalOnlySettings {
2019-08-04 01:19:28 +03:00
globalsettings [ k ] = v
}
return globalsettings
2018-08-26 06:06:44 +03:00
}
2019-08-04 01:19:28 +03:00
// DefaultAllSettings returns a map of all settings and their
2019-08-04 03:12:23 +03:00
// default values (both common and global settings)
2019-06-18 00:45:38 +03:00
func DefaultAllSettings ( ) map [ string ] interface { } {
2019-08-04 01:19:28 +03:00
allsettings := make ( map [ string ] interface { } )
for k , v := range defaultCommonSettings {
allsettings [ k ] = v
}
2020-02-12 21:18:59 +03:00
for k , v := range DefaultGlobalOnlySettings {
2019-08-04 01:19:28 +03:00
allsettings [ k ] = v
}
return allsettings
2019-06-18 00:45:38 +03:00
}
2020-01-21 06:03:32 +03:00
// GetNativeValue parses and validates a value for a given option
2019-01-14 05:06:58 +03:00
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
}
2018-08-26 06:06:44 +03:00
2019-01-14 05:06:58 +03:00
if err := OptionIsValid ( option , native ) ; err != nil {
return nil , err
}
return native , nil
}
2016-09-14 22:05:05 +03:00
2019-01-14 05:06:58 +03:00
// OptionIsValid checks if a value is valid for a certain option
func OptionIsValid ( option string , value interface { } ) error {
2016-09-14 22:05:05 +03:00
if validator , ok := optionValidators [ option ] ; ok {
return validator ( option , value )
}
return nil
}
// Option validators
func validatePositiveValue ( option string , value interface { } ) error {
tabsize , ok := value . ( float64 )
if ! ok {
return errors . New ( "Expected numeric type for " + option )
}
if tabsize < 1 {
return errors . New ( option + " must be greater than 0" )
}
return nil
}
func validateNonNegativeValue ( option string , value interface { } ) error {
nativeValue , ok := value . ( float64 )
if ! ok {
return errors . New ( "Expected numeric type for " + option )
}
if nativeValue < 0 {
return errors . New ( option + " must be non-negative" )
}
return nil
}
func validateColorscheme ( option string , value interface { } ) error {
colorscheme , ok := value . ( string )
if ! ok {
return errors . New ( "Expected string type for colorscheme" )
}
if ! ColorschemeExists ( colorscheme ) {
return errors . New ( colorscheme + " is not a valid colorscheme" )
}
return nil
}
2017-08-24 20:13:14 +03:00
2020-07-05 03:00:39 +03:00
func validateClipboard ( option string , value interface { } ) error {
val , ok := value . ( string )
if ! ok {
return errors . New ( "Expected string type for clipboard" )
}
switch val {
case "internal" , "external" , "terminal" :
default :
return errors . New ( option + " must be 'internal', 'external', or 'terminal'" )
}
return nil
}
2017-08-24 20:13:14 +03:00
func validateLineEnding ( option string , value interface { } ) error {
endingType , ok := value . ( string )
if ! ok {
return errors . New ( "Expected string type for file format" )
}
if endingType != "unix" && endingType != "dos" {
return errors . New ( "File format must be either 'unix' or 'dos'" )
}
return nil
}
2019-01-24 03:06:20 +03:00
func validateEncoding ( option string , value interface { } ) error {
_ , err := htmlindex . Get ( value . ( string ) )
return err
}