2018-08-27 19:53:10 +00:00
package config
2016-03-29 01:10:10 +00:00
import (
2017-08-06 16:02:17 +00:00
"encoding/json"
2016-08-20 01:47:37 +00:00
"errors"
2020-06-07 21:31:16 +00:00
"fmt"
2016-03-29 01:10:10 +00:00
"io/ioutil"
"os"
2020-02-02 20:30:06 +00:00
"path/filepath"
2016-04-24 19:35:16 +00:00
"reflect"
2024-03-04 21:20:02 +00:00
"runtime"
2019-01-14 02:06:58 +00:00
"strconv"
2016-08-26 00:15:58 +00:00
"strings"
"github.com/zyedidia/glob"
2020-01-02 01:44:45 +00:00
"github.com/zyedidia/json5"
2020-05-04 14:16:15 +00:00
"github.com/zyedidia/micro/v2/internal/util"
2019-01-24 00:06:20 +00:00
"golang.org/x/text/encoding/htmlindex"
2016-03-29 01:10:10 +00:00
)
2016-09-14 19:05:05 +00:00
type optionValidator func ( string , interface { } ) error
2024-03-15 21:20:39 +00:00
// a list of settings that need option validators
var optionValidators = map [ string ] optionValidator {
"autosave" : validateNonNegativeValue ,
"clipboard" : validateChoice ,
"colorcolumn" : validateNonNegativeValue ,
"colorscheme" : validateColorscheme ,
"detectlimit" : validateNonNegativeValue ,
"encoding" : validateEncoding ,
"fileformat" : validateChoice ,
"matchbracestyle" : validateChoice ,
"multiopen" : validateChoice ,
"reload" : validateChoice ,
"scrollmargin" : validateNonNegativeValue ,
"scrollspeed" : validateNonNegativeValue ,
"tabsize" : validatePositiveValue ,
}
// a list of settings with pre-defined choices
var OptionChoices = map [ string ] [ ] string {
"clipboard" : { "internal" , "external" , "terminal" } ,
"fileformat" : { "unix" , "dos" } ,
"matchbracestyle" : { "underline" , "highlight" } ,
"multiopen" : { "tab" , "hsplit" , "vsplit" } ,
"reload" : { "prompt" , "auto" , "disabled" } ,
}
// a list of settings that can be globally and locally modified and their
// default values
var defaultCommonSettings = map [ string ] interface { } {
"autoindent" : true ,
"autosu" : false ,
"backup" : true ,
"backupdir" : "" ,
"basename" : false ,
"colorcolumn" : float64 ( 0 ) ,
"cursorline" : true ,
"detectlimit" : float64 ( 100 ) ,
"diffgutter" : false ,
"encoding" : "utf-8" ,
"eofnewline" : true ,
"fastdirty" : false ,
"fileformat" : defaultFileFormat ( ) ,
"filetype" : "unknown" ,
"hlsearch" : false ,
"hltaberrors" : false ,
"hltrailingws" : false ,
"incsearch" : true ,
"ignorecase" : true ,
"indentchar" : " " ,
"keepautoindent" : false ,
"matchbrace" : true ,
Add `matchbraceleft` option (#3432)
Add `matchbraceleft` option to allow disabling the default behavior
matching not just the brace under cursor but also the brace to the left
of it (which is arguably convenient, but also ambiguous and
non-intuitive). With `matchbraceleft` disabled, micro will only match
the brace character that is precisely under the cursor, and also when
jumping to the matching brace, will always move cursor precisely to the
matching brace character, not to the character next to it.
Nota bene: historical journey:
- There was already a `matchbraceleft` option introduced in commit
ea6a87d41a9f, when this feature (matching brace to the left) was
introduced first time. That time it was matching _only_ the brace
to the left, _instead_ of the brace under the cursor, and was
disabled by default.
- Later this feature was removed during the big refactoring of micro.
- Then this feature was reintroduced again in commit d1e713ce08ba, in
its present form (i.e. combined brace matching both under the cursor
and to the left, simulating I-beam cursor behavior), and it was
introduced unconditionally, without an option to disable it.
- Since then, multiple users complained about this feature and asked
for an option to disable it, so now we are reintroducing it as an
option again (this time enabled by default though).
2024-08-18 19:08:05 +00:00
"matchbraceleft" : true ,
2024-03-15 21:20:39 +00:00
"matchbracestyle" : "underline" ,
"mkparents" : false ,
"permbackup" : false ,
"readonly" : false ,
"reload" : "prompt" ,
"rmtrailingws" : false ,
"ruler" : true ,
"relativeruler" : false ,
"savecursor" : false ,
"saveundo" : false ,
"scrollbar" : false ,
"scrollmargin" : float64 ( 3 ) ,
"scrollspeed" : float64 ( 2 ) ,
"smartpaste" : true ,
"softwrap" : false ,
"splitbottom" : true ,
"splitright" : true ,
"statusformatl" : "$(filename) $(modified)($(line),$(col)) $(status.paste)| ft:$(opt:filetype) | $(opt:fileformat) | $(opt:encoding)" ,
"statusformatr" : "$(bind:ToggleKeyMenu): bindings, $(bind:ToggleHelp): help" ,
"statusline" : true ,
"syntax" : true ,
"tabmovement" : false ,
"tabsize" : float64 ( 4 ) ,
"tabstospaces" : false ,
"useprimary" : true ,
"wordwrap" : false ,
}
// a list of settings that should only be globally modified and their
// default values
var DefaultGlobalOnlySettings = map [ string ] interface { } {
"autosave" : float64 ( 0 ) ,
"clipboard" : "external" ,
"colorscheme" : "default" ,
"divchars" : "|-" ,
"divreverse" : true ,
"fakecursor" : false ,
"infobar" : true ,
"keymenu" : false ,
"mouse" : true ,
"multiopen" : "tab" ,
"parsecursor" : false ,
"paste" : false ,
"pluginchannels" : [ ] string { "https://raw.githubusercontent.com/micro-editor/plugin-channel/master/channel.json" } ,
"pluginrepos" : [ ] string { } ,
"savehistory" : true ,
"scrollbarchar" : "|" ,
"sucmd" : "sudo" ,
"tabhighlight" : false ,
"tabreverse" : true ,
"xterm" : false ,
}
// a list of settings that should never be globally modified
var LocalSettings = [ ] string {
"filetype" ,
"readonly" ,
}
2019-01-14 02:06:58 +00:00
var (
ErrInvalidOption = errors . New ( "Invalid option" )
ErrInvalidValue = errors . New ( "Invalid value" )
2016-03-29 01:10:10 +00:00
2019-01-14 02:06:58 +00:00
// The options that the user can set
GlobalSettings map [ string ] interface { }
// This is the raw parsed json
2020-06-23 21:29:20 +00:00
parsedSettings map [ string ] interface { }
settingsParseError bool
2020-06-09 02:19:15 +00: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
2024-03-14 03:43:40 +00:00
// VolatileSettings is a map of settings which should not be written to disk
// because they have been temporarily set for this session only
VolatileSettings map [ string ] bool
2019-01-14 02:06:58 +00:00
)
2017-08-06 16:02:17 +00:00
2019-08-18 22:13:43 +00:00
func init ( ) {
2020-06-09 02:19:15 +00:00
ModifiedSettings = make ( map [ string ] bool )
2024-03-14 03:43:40 +00:00
VolatileSettings = make ( map [ string ] bool )
2019-08-18 22:13:43 +00:00
parsedSettings = make ( map [ string ] interface { } )
}
2018-08-26 03:06:44 +00:00
func ReadSettings ( ) error {
2020-02-02 20:30:06 +00:00
filename := filepath . Join ( ConfigDir , "settings.json" )
2016-03-29 01:10:10 +00:00
if _ , e := os . Stat ( filename ) ; e == nil {
input , err := ioutil . ReadFile ( filename )
2018-08-26 03:06:44 +00:00
if err != nil {
2020-06-23 21:29:20 +00:00
settingsParseError = true
2018-08-26 03:06:44 +00:00
return errors . New ( "Error reading settings.json file: " + err . Error ( ) )
}
2016-08-30 15:19:51 +00:00
if ! strings . HasPrefix ( string ( input ) , "null" ) {
2018-08-26 03:06:44 +00:00
// Unmarshal the input into the parsed map
err = json5 . Unmarshal ( input , & parsedSettings )
2016-08-30 15:19:51 +00:00
if err != nil {
2020-06-23 21:29:20 +00:00
settingsParseError = true
2018-08-26 03:06:44 +00:00
return errors . New ( "Error reading settings.json: " + err . Error ( ) )
2016-08-30 15:19:51 +00:00
}
2020-02-08 21:53:08 +00: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 13:01:01 +00:00
}
2016-04-30 19:02:33 +00:00
}
2018-08-26 03:06:44 +00:00
return nil
}
2016-04-30 19:02:33 +00:00
2020-06-07 21:31:16 +00: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 03:06:44 +00:00
// InitGlobalSettings initializes the options map and sets all options to their default values
// Must be called after ReadSettings
2020-06-07 21:31:16 +00:00
func InitGlobalSettings ( ) error {
var err error
2018-08-27 19:53:10 +00:00
GlobalSettings = DefaultGlobalSettings ( )
2018-08-26 03:06:44 +00:00
for k , v := range parsedSettings {
2016-08-26 00:15:58 +00:00
if ! strings . HasPrefix ( reflect . TypeOf ( v ) . String ( ) , "map" ) {
2020-06-07 21:31:16 +00:00
if _ , ok := GlobalSettings [ k ] ; ok && ! verifySetting ( k , reflect . TypeOf ( v ) , reflect . TypeOf ( GlobalSettings [ k ] ) ) {
2020-09-16 04:08:01 +00: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-07 21:31:16 +00:00
continue
}
2018-08-27 19:53:10 +00:00
GlobalSettings [ k ] = v
2016-08-26 00:15:58 +00:00
}
2016-04-30 19:02:33 +00:00
}
2020-06-07 21:31:16 +00:00
return err
2016-08-26 00:15:58 +00:00
}
// InitLocalSettings scans the json in settings.json and sets the options locally based
2018-08-27 19:53:10 +00:00
// on whether the filetype or path matches ft or glob local settings
2018-08-26 03:06:44 +00:00
// Must be called after ReadSettings
2018-08-27 19:53:10 +00:00
func InitLocalSettings ( settings map [ string ] interface { } , path string ) error {
2018-08-26 03:06:44 +00:00
var parseError error
for k , v := range parsedSettings {
2016-08-26 00:15:58 +00:00
if strings . HasPrefix ( reflect . TypeOf ( v ) . String ( ) , "map" ) {
2017-12-04 04:38:09 +00:00
if strings . HasPrefix ( k , "ft:" ) {
2018-08-27 19:53:10 +00:00
if settings [ "filetype" ] . ( string ) == k [ 3 : ] {
2017-12-04 04:38:09 +00:00
for k1 , v1 := range v . ( map [ string ] interface { } ) {
2020-06-07 21:31:16 +00:00
if _ , ok := settings [ k1 ] ; ok && ! verifySetting ( k1 , reflect . TypeOf ( v1 ) , reflect . TypeOf ( settings [ k1 ] ) ) {
2020-09-16 04:08:01 +00: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-07 21:31:16 +00:00
continue
}
2018-08-27 19:53:10 +00:00
settings [ k1 ] = v1
2017-12-04 04:38:09 +00:00
}
}
} else {
g , err := glob . Compile ( k )
if err != nil {
2018-08-26 03:06:44 +00:00
parseError = errors . New ( "Error with glob setting " + k + ": " + err . Error ( ) )
2017-12-04 04:38:09 +00:00
continue
}
2016-08-26 00:15:58 +00:00
2018-08-27 19:53:10 +00:00
if g . MatchString ( path ) {
2017-12-04 04:38:09 +00:00
for k1 , v1 := range v . ( map [ string ] interface { } ) {
2020-06-07 21:31:16 +00:00
if _ , ok := settings [ k1 ] ; ok && ! verifySetting ( k1 , reflect . TypeOf ( v1 ) , reflect . TypeOf ( settings [ k1 ] ) ) {
2020-09-16 04:08:01 +00: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-07 21:31:16 +00:00
continue
}
2018-08-27 19:53:10 +00:00
settings [ k1 ] = v1
2017-12-04 04:38:09 +00:00
}
2016-08-26 00:15:58 +00:00
}
}
}
2016-03-29 01:10:10 +00:00
}
2018-08-26 03:06:44 +00:00
return parseError
2016-03-29 01:10:10 +00:00
}
// WriteSettings writes the settings to the specified filename as JSON
func WriteSettings ( filename string ) error {
2020-06-23 21:29:20 +00: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 01:49:36 +00:00
var err error
2018-08-27 19:53:10 +00:00
if _ , e := os . Stat ( ConfigDir ) ; e == nil {
2020-06-08 19:33:38 +00: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 ]
2024-03-14 03:43:40 +00:00
_ , vol := VolatileSettings [ k ]
if def , ok := defaults [ k ] ; ok && okcur && ! vol && reflect . DeepEqual ( cur , def ) {
2020-06-08 19:33:38 +00:00
delete ( parsedSettings , k )
}
}
}
// add any options to parsedSettings that have since been marked as non-default
2018-08-27 19:53:10 +00:00
for k , v := range GlobalSettings {
2020-06-08 19:33:38 +00:00
if def , ok := defaults [ k ] ; ! ok || ! reflect . DeepEqual ( v , def ) {
2020-06-09 02:19:15 +00:00
if _ , wr := ModifiedSettings [ k ] ; wr {
parsedSettings [ k ] = v
}
2020-06-08 19:33:38 +00:00
}
2016-08-26 00:15:58 +00:00
}
2018-08-26 03:06:44 +00:00
txt , _ := json . MarshalIndent ( parsedSettings , "" , " " )
2016-09-04 14:10:57 +00:00
err = ioutil . WriteFile ( filename , append ( txt , '\n' ) , 0644 )
2016-04-18 01:49:36 +00:00
}
2016-03-29 01:10:10 +00:00
return err
}
2020-06-08 19:33:38 +00:00
// OverwriteSettings writes the current settings to settings.json and
// resets any user configuration of local settings present in settings.json
2020-02-02 20:30:06 +00:00
func OverwriteSettings ( filename string ) error {
2020-06-08 19:33:38 +00:00
settings := make ( map [ string ] interface { } )
2020-02-02 20:30:06 +00:00
var err error
if _ , e := os . Stat ( ConfigDir ) ; e == nil {
2020-06-08 19:33:38 +00:00
defaults := DefaultGlobalSettings ( )
for k , v := range GlobalSettings {
if def , ok := defaults [ k ] ; ! ok || ! reflect . DeepEqual ( v , def ) {
2020-06-09 02:19:15 +00:00
if _ , wr := ModifiedSettings [ k ] ; wr {
settings [ k ] = v
}
2020-06-08 19:33:38 +00:00
}
}
txt , _ := json . MarshalIndent ( settings , "" , " " )
2020-02-02 20:30:06 +00:00
err = ioutil . WriteFile ( filename , append ( txt , '\n' ) , 0644 )
}
return err
}
2020-02-02 19:35:30 +00: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 {
2023-11-02 23:58:30 +00:00
return RegisterCommonOption ( pl + "." + name , defaultvalue )
2019-08-03 22:19:28 +00:00
}
2020-02-02 19:35:30 +00: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 )
}
2023-06-06 00:38:33 +00:00
// RegisterCommonOption creates a new option
func RegisterCommonOption ( name string , defaultvalue interface { } ) error {
2023-11-02 23:51:30 +00:00
if _ , ok := GlobalSettings [ name ] ; ! ok {
2023-06-06 00:38:33 +00:00
GlobalSettings [ name ] = defaultvalue
}
2023-11-03 00:07:29 +00:00
defaultCommonSettings [ name ] = defaultvalue
2023-06-06 00:38:33 +00:00
return nil
}
2020-02-02 19:35:30 +00:00
// RegisterGlobalOption creates a new global-only option
2019-08-03 22:19:28 +00:00
func RegisterGlobalOption ( name string , defaultvalue interface { } ) error {
2023-11-02 23:51:30 +00:00
if _ , ok := GlobalSettings [ name ] ; ! ok {
2019-08-03 22:19:28 +00:00
GlobalSettings [ name ] = defaultvalue
2016-04-30 16:13:21 +00:00
}
2023-11-03 00:07:29 +00:00
DefaultGlobalOnlySettings [ name ] = defaultvalue
2018-08-26 03:06:44 +00:00
return nil
2016-04-30 16:13:21 +00:00
}
2016-08-25 21:24:13 +00:00
// GetGlobalOption returns the global value of the given option
2016-08-24 23:55:44 +00:00
func GetGlobalOption ( name string ) interface { } {
2018-08-27 19:53:10 +00:00
return GlobalSettings [ name ]
2016-08-24 23:55:44 +00:00
}
2024-03-04 21:20:02 +00:00
func defaultFileFormat ( ) string {
if runtime . GOOS == "windows" {
return "dos"
}
return "unix"
2016-08-25 19:03:37 +00:00
}
2019-01-15 03:16:44 +00:00
func GetInfoBarOffset ( ) int {
2019-01-16 23:37:45 +00:00
offset := 0
2019-01-15 03:16:44 +00:00
if GetGlobalOption ( "infobar" ) . ( bool ) {
2019-01-16 23:37:45 +00:00
offset ++
2019-01-15 03:16:44 +00:00
}
2019-01-16 23:37:45 +00:00
if GetGlobalOption ( "keymenu" ) . ( bool ) {
offset += 2
}
return offset
2019-01-15 03:16:44 +00:00
}
2019-08-04 00:12:23 +00: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
}
2018-08-26 03:06:44 +00:00
// DefaultGlobalSettings returns the default global settings for micro
// Note that colorscheme is a global only option
func DefaultGlobalSettings ( ) map [ string ] interface { } {
2019-08-03 22:19:28 +00:00
globalsettings := make ( map [ string ] interface { } )
for k , v := range defaultCommonSettings {
globalsettings [ k ] = v
}
2020-02-12 18:18:59 +00:00
for k , v := range DefaultGlobalOnlySettings {
2019-08-03 22:19:28 +00:00
globalsettings [ k ] = v
}
return globalsettings
2018-08-26 03:06:44 +00:00
}
2019-08-03 22:19:28 +00:00
// DefaultAllSettings returns a map of all settings and their
2019-08-04 00:12:23 +00:00
// default values (both common and global settings)
2019-06-17 21:45:38 +00:00
func DefaultAllSettings ( ) map [ string ] interface { } {
2019-08-03 22:19:28 +00:00
allsettings := make ( map [ string ] interface { } )
for k , v := range defaultCommonSettings {
allsettings [ k ] = v
}
2020-02-12 18:18:59 +00:00
for k , v := range DefaultGlobalOnlySettings {
2019-08-03 22:19:28 +00:00
allsettings [ k ] = v
}
return allsettings
2019-06-17 21:45:38 +00:00
}
2020-01-21 03:03:32 +00:00
// GetNativeValue parses and validates a value for a given option
2019-01-14 02:06:58 +00: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 03:06:44 +00:00
2019-01-14 02:06:58 +00:00
if err := OptionIsValid ( option , native ) ; err != nil {
return nil , err
}
return native , nil
}
2016-09-14 19:05:05 +00:00
2019-01-14 02:06:58 +00:00
// OptionIsValid checks if a value is valid for a certain option
func OptionIsValid ( option string , value interface { } ) error {
2016-09-14 19:05:05 +00: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
}
2024-03-15 21:20:39 +00:00
func validateChoice ( option string , value interface { } ) error {
if choices , ok := OptionChoices [ option ] ; ok {
val , ok := value . ( string )
if ! ok {
return errors . New ( "Expected string type for " + option )
}
2020-07-05 00:00:39 +00:00
2024-03-15 21:20:39 +00:00
for _ , v := range choices {
if val == v {
return nil
}
}
2020-07-05 00:00:39 +00:00
2024-03-15 21:20:39 +00:00
choicesStr := strings . Join ( choices , ", " )
return errors . New ( option + " must be one of: " + choicesStr )
2020-07-05 00:00:39 +00:00
}
2024-03-15 21:20:39 +00:00
return errors . New ( "Option has no pre-defined choices" )
2020-07-05 00:00:39 +00:00
}
2024-03-15 21:20:39 +00:00
func validateColorscheme ( option string , value interface { } ) error {
colorscheme , ok := value . ( string )
2017-08-24 17:13:14 +00:00
if ! ok {
2024-03-15 21:20:39 +00:00
return errors . New ( "Expected string type for colorscheme" )
2017-08-24 17:13:14 +00:00
}
2024-03-15 21:20:39 +00:00
if ! ColorschemeExists ( colorscheme ) {
return errors . New ( colorscheme + " is not a valid colorscheme" )
2017-08-24 17:13:14 +00:00
}
return nil
}
2019-01-24 00:06:20 +00:00
func validateEncoding ( option string , value interface { } ) error {
_ , err := htmlindex . Get ( value . ( string ) )
return err
}