14 "github.com/zyedidia/glob"
15 "github.com/zyedidia/json5"
16 "github.com/zyedidia/micro/v2/internal/util"
17 "golang.org/x/text/encoding/htmlindex"
20 type optionValidator func(string, interface{}) error
23 ErrInvalidOption = errors.New("Invalid option")
24 ErrInvalidValue = errors.New("Invalid value")
26 // The options that the user can set
27 GlobalSettings map[string]interface{}
29 // This is the raw parsed json
30 parsedSettings map[string]interface{}
32 // ModifiedSettings is a map of settings which should be written to disk
33 // because they have been modified by the user in this session
34 ModifiedSettings map[string]bool
38 ModifiedSettings = make(map[string]bool)
39 parsedSettings = make(map[string]interface{})
42 // Options with validators
43 var optionValidators = map[string]optionValidator{
44 "autosave": validateNonNegativeValue,
45 "tabsize": validatePositiveValue,
46 "scrollmargin": validateNonNegativeValue,
47 "scrollspeed": validateNonNegativeValue,
48 "colorscheme": validateColorscheme,
49 "colorcolumn": validateNonNegativeValue,
50 "fileformat": validateLineEnding,
51 "encoding": validateEncoding,
54 func ReadSettings() error {
55 filename := filepath.Join(ConfigDir, "settings.json")
56 if _, e := os.Stat(filename); e == nil {
57 input, err := ioutil.ReadFile(filename)
59 return errors.New("Error reading settings.json file: " + err.Error())
61 if !strings.HasPrefix(string(input), "null") {
62 // Unmarshal the input into the parsed map
63 err = json5.Unmarshal(input, &parsedSettings)
65 return errors.New("Error reading settings.json: " + err.Error())
68 // check if autosave is a boolean and convert it to float if so
69 if v, ok := parsedSettings["autosave"]; ok {
73 parsedSettings["autosave"] = 8.0
75 parsedSettings["autosave"] = 0.0
84 func verifySetting(option string, value reflect.Type, def reflect.Type) bool {
85 var interfaceArr []interface{}
87 case "pluginrepos", "pluginchannels":
88 return value.AssignableTo(reflect.TypeOf(interfaceArr))
90 return def.AssignableTo(value)
94 // InitGlobalSettings initializes the options map and sets all options to their default values
95 // Must be called after ReadSettings
96 func InitGlobalSettings() error {
98 GlobalSettings = DefaultGlobalSettings()
100 for k, v := range parsedSettings {
101 if !strings.HasPrefix(reflect.TypeOf(v).String(), "map") {
102 if _, ok := GlobalSettings[k]; ok && !verifySetting(k, reflect.TypeOf(v), reflect.TypeOf(GlobalSettings[k])) {
103 err = errors.New(fmt.Sprintf("Global Error: setting '%s' has incorrect type (%s), using default value: %v (%s)", k, reflect.TypeOf(v), GlobalSettings[k], reflect.TypeOf(GlobalSettings[k])))
107 GlobalSettings[k] = v
113 // InitLocalSettings scans the json in settings.json and sets the options locally based
114 // on whether the filetype or path matches ft or glob local settings
115 // Must be called after ReadSettings
116 func InitLocalSettings(settings map[string]interface{}, path string) error {
118 for k, v := range parsedSettings {
119 if strings.HasPrefix(reflect.TypeOf(v).String(), "map") {
120 if strings.HasPrefix(k, "ft:") {
121 if settings["filetype"].(string) == k[3:] {
122 for k1, v1 := range v.(map[string]interface{}) {
123 if _, ok := settings[k1]; ok && !verifySetting(k1, reflect.TypeOf(v1), reflect.TypeOf(settings[k1])) {
124 parseError = errors.New(fmt.Sprintf("Error: setting '%s' has incorrect type (%s), using default value: %v (%s)", k, reflect.TypeOf(v1), settings[k1], reflect.TypeOf(settings[k1])))
131 g, err := glob.Compile(k)
133 parseError = errors.New("Error with glob setting " + k + ": " + err.Error())
137 if g.MatchString(path) {
138 for k1, v1 := range v.(map[string]interface{}) {
139 if _, ok := settings[k1]; ok && !verifySetting(k1, reflect.TypeOf(v1), reflect.TypeOf(settings[k1])) {
140 parseError = errors.New(fmt.Sprintf("Error: setting '%s' has incorrect type (%s), using default value: %v (%s)", k, reflect.TypeOf(v1), settings[k1], reflect.TypeOf(settings[k1])))
152 // WriteSettings writes the settings to the specified filename as JSON
153 func WriteSettings(filename string) error {
155 if _, e := os.Stat(ConfigDir); e == nil {
156 defaults := DefaultGlobalSettings()
158 // remove any options froms parsedSettings that have since been marked as default
159 for k, v := range parsedSettings {
160 if !strings.HasPrefix(reflect.TypeOf(v).String(), "map") {
161 cur, okcur := GlobalSettings[k]
162 if def, ok := defaults[k]; ok && okcur && reflect.DeepEqual(cur, def) {
163 delete(parsedSettings, k)
168 // add any options to parsedSettings that have since been marked as non-default
169 for k, v := range GlobalSettings {
170 if def, ok := defaults[k]; !ok || !reflect.DeepEqual(v, def) {
171 if _, wr := ModifiedSettings[k]; wr {
172 parsedSettings[k] = v
177 txt, _ := json.MarshalIndent(parsedSettings, "", " ")
178 err = ioutil.WriteFile(filename, append(txt, '\n'), 0644)
183 // OverwriteSettings writes the current settings to settings.json and
184 // resets any user configuration of local settings present in settings.json
185 func OverwriteSettings(filename string) error {
186 settings := make(map[string]interface{})
189 if _, e := os.Stat(ConfigDir); e == nil {
190 defaults := DefaultGlobalSettings()
191 for k, v := range GlobalSettings {
192 if def, ok := defaults[k]; !ok || !reflect.DeepEqual(v, def) {
193 if _, wr := ModifiedSettings[k]; wr {
199 txt, _ := json.MarshalIndent(settings, "", " ")
200 err = ioutil.WriteFile(filename, append(txt, '\n'), 0644)
205 // RegisterCommonOptionPlug creates a new option (called pl.name). This is meant to be called by plugins to add options.
206 func RegisterCommonOptionPlug(pl string, name string, defaultvalue interface{}) error {
207 name = pl + "." + name
208 if _, ok := GlobalSettings[name]; !ok {
209 defaultCommonSettings[name] = defaultvalue
210 GlobalSettings[name] = defaultvalue
211 err := WriteSettings(filepath.Join(ConfigDir, "settings.json"))
213 return errors.New("Error writing settings.json file: " + err.Error())
216 defaultCommonSettings[name] = defaultvalue
221 // RegisterGlobalOptionPlug creates a new global-only option (named pl.name)
222 func RegisterGlobalOptionPlug(pl string, name string, defaultvalue interface{}) error {
223 return RegisterGlobalOption(pl+"."+name, defaultvalue)
226 // RegisterGlobalOption creates a new global-only option
227 func RegisterGlobalOption(name string, defaultvalue interface{}) error {
228 if v, ok := GlobalSettings[name]; !ok {
229 DefaultGlobalOnlySettings[name] = defaultvalue
230 GlobalSettings[name] = defaultvalue
231 err := WriteSettings(filepath.Join(ConfigDir, "settings.json"))
233 return errors.New("Error writing settings.json file: " + err.Error())
236 DefaultGlobalOnlySettings[name] = v
241 // GetGlobalOption returns the global value of the given option
242 func GetGlobalOption(name string) interface{} {
243 return GlobalSettings[name]
246 var defaultCommonSettings = map[string]interface{}{
251 "colorcolumn": float64(0),
257 "fileformat": "unix",
258 "filetype": "unknown",
261 "keepautoindent": false,
265 "rmtrailingws": false,
267 "relativeruler": false,
271 "scrollmargin": float64(3),
272 "scrollspeed": float64(2),
277 "statusformatl": "$(filename) $(modified)($(line),$(col)) $(status.paste)| ft:$(opt:filetype) | $(opt:fileformat) | $(opt:encoding)",
278 "statusformatr": "$(bind:ToggleKeyMenu): bindings, $(bind:ToggleHelp): help",
281 "tabmovement": false,
282 "tabsize": float64(4),
283 "tabstospaces": false,
287 func GetInfoBarOffset() int {
289 if GetGlobalOption("infobar").(bool) {
292 if GetGlobalOption("keymenu").(bool) {
298 // DefaultCommonSettings returns the default global settings for micro
299 // Note that colorscheme is a global only option
300 func DefaultCommonSettings() map[string]interface{} {
301 commonsettings := make(map[string]interface{})
302 for k, v := range defaultCommonSettings {
303 commonsettings[k] = v
305 return commonsettings
308 // a list of settings that should only be globally modified and their
310 var DefaultGlobalOnlySettings = map[string]interface{}{
311 "autosave": float64(0),
312 "colorscheme": "default",
318 "parsecursor": false,
322 "pluginchannels": []string{"https://raw.githubusercontent.com/micro-editor/plugin-channel/master/channel.json"},
323 "pluginrepos": []string{},
327 // a list of settings that should never be globally modified
328 var LocalSettings = []string{
333 // DefaultGlobalSettings returns the default global settings for micro
334 // Note that colorscheme is a global only option
335 func DefaultGlobalSettings() map[string]interface{} {
336 globalsettings := make(map[string]interface{})
337 for k, v := range defaultCommonSettings {
338 globalsettings[k] = v
340 for k, v := range DefaultGlobalOnlySettings {
341 globalsettings[k] = v
343 return globalsettings
346 // DefaultAllSettings returns a map of all settings and their
347 // default values (both common and global settings)
348 func DefaultAllSettings() map[string]interface{} {
349 allsettings := make(map[string]interface{})
350 for k, v := range defaultCommonSettings {
353 for k, v := range DefaultGlobalOnlySettings {
359 // GetNativeValue parses and validates a value for a given option
360 func GetNativeValue(option string, realValue interface{}, value string) (interface{}, error) {
361 var native interface{}
362 kind := reflect.TypeOf(realValue).Kind()
363 if kind == reflect.Bool {
364 b, err := util.ParseBool(value)
366 return nil, ErrInvalidValue
369 } else if kind == reflect.String {
371 } else if kind == reflect.Float64 {
372 i, err := strconv.Atoi(value)
374 return nil, ErrInvalidValue
378 return nil, ErrInvalidValue
381 if err := OptionIsValid(option, native); err != nil {
387 // OptionIsValid checks if a value is valid for a certain option
388 func OptionIsValid(option string, value interface{}) error {
389 if validator, ok := optionValidators[option]; ok {
390 return validator(option, value)
398 func validatePositiveValue(option string, value interface{}) error {
399 tabsize, ok := value.(float64)
402 return errors.New("Expected numeric type for " + option)
406 return errors.New(option + " must be greater than 0")
412 func validateNonNegativeValue(option string, value interface{}) error {
413 nativeValue, ok := value.(float64)
416 return errors.New("Expected numeric type for " + option)
420 return errors.New(option + " must be non-negative")
426 func validateColorscheme(option string, value interface{}) error {
427 colorscheme, ok := value.(string)
430 return errors.New("Expected string type for colorscheme")
433 if !ColorschemeExists(colorscheme) {
434 return errors.New(colorscheme + " is not a valid colorscheme")
440 func validateLineEnding(option string, value interface{}) error {
441 endingType, ok := value.(string)
444 return errors.New("Expected string type for file format")
447 if endingType != "unix" && endingType != "dos" {
448 return errors.New("File format must be either 'unix' or 'dos'")
454 func validateEncoding(option string, value interface{}) error {
455 _, err := htmlindex.Get(value.(string))