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{}
31 settingsParseError bool
33 // ModifiedSettings is a map of settings which should be written to disk
34 // because they have been modified by the user in this session
35 ModifiedSettings map[string]bool
39 ModifiedSettings = make(map[string]bool)
40 parsedSettings = make(map[string]interface{})
43 // Options with validators
44 var optionValidators = map[string]optionValidator{
45 "autosave": validateNonNegativeValue,
46 "clipboard": validateClipboard,
47 "tabsize": validatePositiveValue,
48 "scrollmargin": validateNonNegativeValue,
49 "scrollspeed": validateNonNegativeValue,
50 "colorscheme": validateColorscheme,
51 "colorcolumn": validateNonNegativeValue,
52 "fileformat": validateLineEnding,
53 "encoding": validateEncoding,
56 func ReadSettings() error {
57 filename := filepath.Join(ConfigDir, "settings.json")
58 if _, e := os.Stat(filename); e == nil {
59 input, err := ioutil.ReadFile(filename)
61 settingsParseError = true
62 return errors.New("Error reading settings.json file: " + err.Error())
64 if !strings.HasPrefix(string(input), "null") {
65 // Unmarshal the input into the parsed map
66 err = json5.Unmarshal(input, &parsedSettings)
68 settingsParseError = true
69 return errors.New("Error reading settings.json: " + err.Error())
72 // check if autosave is a boolean and convert it to float if so
73 if v, ok := parsedSettings["autosave"]; ok {
77 parsedSettings["autosave"] = 8.0
79 parsedSettings["autosave"] = 0.0
88 func verifySetting(option string, value reflect.Type, def reflect.Type) bool {
89 var interfaceArr []interface{}
91 case "pluginrepos", "pluginchannels":
92 return value.AssignableTo(reflect.TypeOf(interfaceArr))
94 return def.AssignableTo(value)
98 // InitGlobalSettings initializes the options map and sets all options to their default values
99 // Must be called after ReadSettings
100 func InitGlobalSettings() error {
102 GlobalSettings = DefaultGlobalSettings()
104 for k, v := range parsedSettings {
105 if !strings.HasPrefix(reflect.TypeOf(v).String(), "map") {
106 if _, ok := GlobalSettings[k]; ok && !verifySetting(k, reflect.TypeOf(v), reflect.TypeOf(GlobalSettings[k])) {
107 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]))
111 GlobalSettings[k] = v
117 // InitLocalSettings scans the json in settings.json and sets the options locally based
118 // on whether the filetype or path matches ft or glob local settings
119 // Must be called after ReadSettings
120 func InitLocalSettings(settings map[string]interface{}, path string) error {
122 for k, v := range parsedSettings {
123 if strings.HasPrefix(reflect.TypeOf(v).String(), "map") {
124 if strings.HasPrefix(k, "ft:") {
125 if settings["filetype"].(string) == k[3:] {
126 for k1, v1 := range v.(map[string]interface{}) {
127 if _, ok := settings[k1]; ok && !verifySetting(k1, reflect.TypeOf(v1), reflect.TypeOf(settings[k1])) {
128 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]))
135 g, err := glob.Compile(k)
137 parseError = errors.New("Error with glob setting " + k + ": " + err.Error())
141 if g.MatchString(path) {
142 for k1, v1 := range v.(map[string]interface{}) {
143 if _, ok := settings[k1]; ok && !verifySetting(k1, reflect.TypeOf(v1), reflect.TypeOf(settings[k1])) {
144 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]))
156 // WriteSettings writes the settings to the specified filename as JSON
157 func WriteSettings(filename string) error {
158 if settingsParseError {
159 // Don't write settings if there was a parse error
160 // because this will delete the settings.json if it
161 // is invalid. Instead we should allow the user to fix
167 if _, e := os.Stat(ConfigDir); e == nil {
168 defaults := DefaultGlobalSettings()
170 // remove any options froms parsedSettings that have since been marked as default
171 for k, v := range parsedSettings {
172 if !strings.HasPrefix(reflect.TypeOf(v).String(), "map") {
173 cur, okcur := GlobalSettings[k]
174 if def, ok := defaults[k]; ok && okcur && reflect.DeepEqual(cur, def) {
175 delete(parsedSettings, k)
180 // add any options to parsedSettings that have since been marked as non-default
181 for k, v := range GlobalSettings {
182 if def, ok := defaults[k]; !ok || !reflect.DeepEqual(v, def) {
183 if _, wr := ModifiedSettings[k]; wr {
184 parsedSettings[k] = v
189 txt, _ := json.MarshalIndent(parsedSettings, "", " ")
190 err = ioutil.WriteFile(filename, append(txt, '\n'), 0644)
195 // OverwriteSettings writes the current settings to settings.json and
196 // resets any user configuration of local settings present in settings.json
197 func OverwriteSettings(filename string) error {
198 settings := make(map[string]interface{})
201 if _, e := os.Stat(ConfigDir); e == nil {
202 defaults := DefaultGlobalSettings()
203 for k, v := range GlobalSettings {
204 if def, ok := defaults[k]; !ok || !reflect.DeepEqual(v, def) {
205 if _, wr := ModifiedSettings[k]; wr {
211 txt, _ := json.MarshalIndent(settings, "", " ")
212 err = ioutil.WriteFile(filename, append(txt, '\n'), 0644)
217 // RegisterCommonOptionPlug creates a new option (called pl.name). This is meant to be called by plugins to add options.
218 func RegisterCommonOptionPlug(pl string, name string, defaultvalue interface{}) error {
219 name = pl + "." + name
220 if _, ok := GlobalSettings[name]; !ok {
221 defaultCommonSettings[name] = defaultvalue
222 GlobalSettings[name] = defaultvalue
223 err := WriteSettings(filepath.Join(ConfigDir, "settings.json"))
225 return errors.New("Error writing settings.json file: " + err.Error())
228 defaultCommonSettings[name] = defaultvalue
233 // RegisterGlobalOptionPlug creates a new global-only option (named pl.name)
234 func RegisterGlobalOptionPlug(pl string, name string, defaultvalue interface{}) error {
235 return RegisterGlobalOption(pl+"."+name, defaultvalue)
238 // RegisterGlobalOption creates a new global-only option
239 func RegisterGlobalOption(name string, defaultvalue interface{}) error {
240 if v, ok := GlobalSettings[name]; !ok {
241 DefaultGlobalOnlySettings[name] = defaultvalue
242 GlobalSettings[name] = defaultvalue
243 err := WriteSettings(filepath.Join(ConfigDir, "settings.json"))
245 return errors.New("Error writing settings.json file: " + err.Error())
248 DefaultGlobalOnlySettings[name] = v
253 // GetGlobalOption returns the global value of the given option
254 func GetGlobalOption(name string) interface{} {
255 return GlobalSettings[name]
258 var defaultCommonSettings = map[string]interface{}{
265 "colorcolumn": float64(0),
271 "fileformat": "unix",
272 "filetype": "unknown",
277 "keepautoindent": false,
282 "rmtrailingws": false,
284 "relativeruler": false,
288 "scrollmargin": float64(3),
289 "scrollspeed": float64(2),
294 "statusformatl": "$(filename) $(modified)($(line),$(col)) $(status.paste)| ft:$(opt:filetype) | $(opt:fileformat) | $(opt:encoding)",
295 "statusformatr": "$(bind:ToggleKeyMenu): bindings, $(bind:ToggleHelp): help",
298 "tabmovement": false,
299 "tabsize": float64(4),
300 "tabstospaces": false,
305 func GetInfoBarOffset() int {
307 if GetGlobalOption("infobar").(bool) {
310 if GetGlobalOption("keymenu").(bool) {
316 // DefaultCommonSettings returns the default global settings for micro
317 // Note that colorscheme is a global only option
318 func DefaultCommonSettings() map[string]interface{} {
319 commonsettings := make(map[string]interface{})
320 for k, v := range defaultCommonSettings {
321 commonsettings[k] = v
323 return commonsettings
326 // a list of settings that should only be globally modified and their
328 var DefaultGlobalOnlySettings = map[string]interface{}{
329 "autosave": float64(0),
330 "clipboard": "external",
331 "colorscheme": "default",
337 "parsecursor": false,
339 "pluginchannels": []string{"https://raw.githubusercontent.com/micro-editor/plugin-channel/master/channel.json"},
340 "pluginrepos": []string{},
343 "tabhighlight": false,
348 // a list of settings that should never be globally modified
349 var LocalSettings = []string{
354 // DefaultGlobalSettings returns the default global settings for micro
355 // Note that colorscheme is a global only option
356 func DefaultGlobalSettings() map[string]interface{} {
357 globalsettings := make(map[string]interface{})
358 for k, v := range defaultCommonSettings {
359 globalsettings[k] = v
361 for k, v := range DefaultGlobalOnlySettings {
362 globalsettings[k] = v
364 return globalsettings
367 // DefaultAllSettings returns a map of all settings and their
368 // default values (both common and global settings)
369 func DefaultAllSettings() map[string]interface{} {
370 allsettings := make(map[string]interface{})
371 for k, v := range defaultCommonSettings {
374 for k, v := range DefaultGlobalOnlySettings {
380 // GetNativeValue parses and validates a value for a given option
381 func GetNativeValue(option string, realValue interface{}, value string) (interface{}, error) {
382 var native interface{}
383 kind := reflect.TypeOf(realValue).Kind()
384 if kind == reflect.Bool {
385 b, err := util.ParseBool(value)
387 return nil, ErrInvalidValue
390 } else if kind == reflect.String {
392 } else if kind == reflect.Float64 {
393 i, err := strconv.Atoi(value)
395 return nil, ErrInvalidValue
399 return nil, ErrInvalidValue
402 if err := OptionIsValid(option, native); err != nil {
408 // OptionIsValid checks if a value is valid for a certain option
409 func OptionIsValid(option string, value interface{}) error {
410 if validator, ok := optionValidators[option]; ok {
411 return validator(option, value)
419 func validatePositiveValue(option string, value interface{}) error {
420 tabsize, ok := value.(float64)
423 return errors.New("Expected numeric type for " + option)
427 return errors.New(option + " must be greater than 0")
433 func validateNonNegativeValue(option string, value interface{}) error {
434 nativeValue, ok := value.(float64)
437 return errors.New("Expected numeric type for " + option)
441 return errors.New(option + " must be non-negative")
447 func validateColorscheme(option string, value interface{}) error {
448 colorscheme, ok := value.(string)
451 return errors.New("Expected string type for colorscheme")
454 if !ColorschemeExists(colorscheme) {
455 return errors.New(colorscheme + " is not a valid colorscheme")
461 func validateClipboard(option string, value interface{}) error {
462 val, ok := value.(string)
465 return errors.New("Expected string type for clipboard")
469 case "internal", "external", "terminal":
471 return errors.New(option + " must be 'internal', 'external', or 'terminal'")
477 func validateLineEnding(option string, value interface{}) error {
478 endingType, ok := value.(string)
481 return errors.New("Expected string type for file format")
484 if endingType != "unix" && endingType != "dos" {
485 return errors.New("File format must be either 'unix' or 'dos'")
491 func validateEncoding(option string, value interface{}) error {
492 _, err := htmlindex.Get(value.(string))