]> git.lizzy.rs Git - micro.git/blob - internal/config/settings.go
Add autoretab
[micro.git] / internal / config / settings.go
1 package config
2
3 import (
4         "encoding/json"
5         "errors"
6         "fmt"
7         "io/ioutil"
8         "os"
9         "path/filepath"
10         "reflect"
11         "strconv"
12         "strings"
13
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"
18 )
19
20 type optionValidator func(string, interface{}) error
21
22 var (
23         ErrInvalidOption = errors.New("Invalid option")
24         ErrInvalidValue  = errors.New("Invalid value")
25
26         // The options that the user can set
27         GlobalSettings map[string]interface{}
28
29         // This is the raw parsed json
30         parsedSettings     map[string]interface{}
31         settingsParseError bool
32
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
36 )
37
38 func init() {
39         ModifiedSettings = make(map[string]bool)
40         parsedSettings = make(map[string]interface{})
41 }
42
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,
54 }
55
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)
60                 if err != nil {
61                         settingsParseError = true
62                         return errors.New("Error reading settings.json file: " + err.Error())
63                 }
64                 if !strings.HasPrefix(string(input), "null") {
65                         // Unmarshal the input into the parsed map
66                         err = json5.Unmarshal(input, &parsedSettings)
67                         if err != nil {
68                                 settingsParseError = true
69                                 return errors.New("Error reading settings.json: " + err.Error())
70                         }
71
72                         // check if autosave is a boolean and convert it to float if so
73                         if v, ok := parsedSettings["autosave"]; ok {
74                                 s, ok := v.(bool)
75                                 if ok {
76                                         if s {
77                                                 parsedSettings["autosave"] = 8.0
78                                         } else {
79                                                 parsedSettings["autosave"] = 0.0
80                                         }
81                                 }
82                         }
83                 }
84         }
85         return nil
86 }
87
88 func verifySetting(option string, value reflect.Type, def reflect.Type) bool {
89         var interfaceArr []interface{}
90         switch option {
91         case "pluginrepos", "pluginchannels":
92                 return value.AssignableTo(reflect.TypeOf(interfaceArr))
93         default:
94                 return def.AssignableTo(value)
95         }
96 }
97
98 // InitGlobalSettings initializes the options map and sets all options to their default values
99 // Must be called after ReadSettings
100 func InitGlobalSettings() error {
101         var err error
102         GlobalSettings = DefaultGlobalSettings()
103
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]))
108                                 continue
109                         }
110
111                         GlobalSettings[k] = v
112                 }
113         }
114         return err
115 }
116
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 {
121         var parseError 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]))
129                                                         continue
130                                                 }
131                                                 settings[k1] = v1
132                                         }
133                                 }
134                         } else {
135                                 g, err := glob.Compile(k)
136                                 if err != nil {
137                                         parseError = errors.New("Error with glob setting " + k + ": " + err.Error())
138                                         continue
139                                 }
140
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]))
145                                                         continue
146                                                 }
147                                                 settings[k1] = v1
148                                         }
149                                 }
150                         }
151                 }
152         }
153         return parseError
154 }
155
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
162                 // it manually.
163                 return nil
164         }
165
166         var err error
167         if _, e := os.Stat(ConfigDir); e == nil {
168                 defaults := DefaultGlobalSettings()
169
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)
176                                 }
177                         }
178                 }
179
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
185                                 }
186                         }
187                 }
188
189                 txt, _ := json.MarshalIndent(parsedSettings, "", "    ")
190                 err = ioutil.WriteFile(filename, append(txt, '\n'), 0644)
191         }
192         return err
193 }
194
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{})
199
200         var err error
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 {
206                                         settings[k] = v
207                                 }
208                         }
209                 }
210
211                 txt, _ := json.MarshalIndent(settings, "", "    ")
212                 err = ioutil.WriteFile(filename, append(txt, '\n'), 0644)
213         }
214         return err
215 }
216
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"))
224                 if err != nil {
225                         return errors.New("Error writing settings.json file: " + err.Error())
226                 }
227         } else {
228                 defaultCommonSettings[name] = defaultvalue
229         }
230         return nil
231 }
232
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)
236 }
237
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"))
244                 if err != nil {
245                         return errors.New("Error writing settings.json file: " + err.Error())
246                 }
247         } else {
248                 DefaultGlobalOnlySettings[name] = v
249         }
250         return nil
251 }
252
253 // GetGlobalOption returns the global value of the given option
254 func GetGlobalOption(name string) interface{} {
255         return GlobalSettings[name]
256 }
257
258 var defaultCommonSettings = map[string]interface{}{
259         "autoindent":     true,
260         "autosu":         false,
261         "autoretab":      false,
262         "backup":         true,
263         "backupdir":      "",
264         "basename":       false,
265         "colorcolumn":    float64(0),
266         "cursorline":     true,
267         "diffgutter":     false,
268         "encoding":       "utf-8",
269         "eofnewline":     true,
270         "fastdirty":      false,
271         "fileformat":     "unix",
272         "filetype":       "unknown",
273         "hlsearch":       false,
274         "incsearch":      true,
275         "ignorecase":     true,
276         "indentchar":     " ",
277         "keepautoindent": false,
278         "matchbrace":     true,
279         "mkparents":      false,
280         "permbackup":     false,
281         "readonly":       false,
282         "rmtrailingws":   false,
283         "ruler":          true,
284         "relativeruler":  false,
285         "savecursor":     false,
286         "saveundo":       false,
287         "scrollbar":      false,
288         "scrollmargin":   float64(3),
289         "scrollspeed":    float64(2),
290         "smartpaste":     true,
291         "softwrap":       false,
292         "splitbottom":    true,
293         "splitright":     true,
294         "statusformatl":  "$(filename) $(modified)($(line),$(col)) $(status.paste)| ft:$(opt:filetype) | $(opt:fileformat) | $(opt:encoding)",
295         "statusformatr":  "$(bind:ToggleKeyMenu): bindings, $(bind:ToggleHelp): help",
296         "statusline":     true,
297         "syntax":         true,
298         "tabmovement":    false,
299         "tabsize":        float64(4),
300         "tabstospaces":   false,
301         "useprimary":     true,
302         "wordwrap":       false,
303 }
304
305 func GetInfoBarOffset() int {
306         offset := 0
307         if GetGlobalOption("infobar").(bool) {
308                 offset++
309         }
310         if GetGlobalOption("keymenu").(bool) {
311                 offset += 2
312         }
313         return offset
314 }
315
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
322         }
323         return commonsettings
324 }
325
326 // a list of settings that should only be globally modified and their
327 // default values
328 var DefaultGlobalOnlySettings = map[string]interface{}{
329         "autosave":       float64(0),
330         "clipboard":      "external",
331         "colorscheme":    "default",
332         "divchars":       "|-",
333         "divreverse":     true,
334         "infobar":        true,
335         "keymenu":        false,
336         "mouse":          true,
337         "parsecursor":    false,
338         "paste":          false,
339         "pluginchannels": []string{"https://raw.githubusercontent.com/micro-editor/plugin-channel/master/channel.json"},
340         "pluginrepos":    []string{},
341         "savehistory":    true,
342         "sucmd":          "sudo",
343         "tabhighlight":   false,
344         "tabreverse":     true,
345         "xterm":          false,
346 }
347
348 // a list of settings that should never be globally modified
349 var LocalSettings = []string{
350         "filetype",
351         "readonly",
352 }
353
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
360         }
361         for k, v := range DefaultGlobalOnlySettings {
362                 globalsettings[k] = v
363         }
364         return globalsettings
365 }
366
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 {
372                 allsettings[k] = v
373         }
374         for k, v := range DefaultGlobalOnlySettings {
375                 allsettings[k] = v
376         }
377         return allsettings
378 }
379
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)
386                 if err != nil {
387                         return nil, ErrInvalidValue
388                 }
389                 native = b
390         } else if kind == reflect.String {
391                 native = value
392         } else if kind == reflect.Float64 {
393                 i, err := strconv.Atoi(value)
394                 if err != nil {
395                         return nil, ErrInvalidValue
396                 }
397                 native = float64(i)
398         } else {
399                 return nil, ErrInvalidValue
400         }
401
402         if err := OptionIsValid(option, native); err != nil {
403                 return nil, err
404         }
405         return native, nil
406 }
407
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)
412         }
413
414         return nil
415 }
416
417 // Option validators
418
419 func validatePositiveValue(option string, value interface{}) error {
420         tabsize, ok := value.(float64)
421
422         if !ok {
423                 return errors.New("Expected numeric type for " + option)
424         }
425
426         if tabsize < 1 {
427                 return errors.New(option + " must be greater than 0")
428         }
429
430         return nil
431 }
432
433 func validateNonNegativeValue(option string, value interface{}) error {
434         nativeValue, ok := value.(float64)
435
436         if !ok {
437                 return errors.New("Expected numeric type for " + option)
438         }
439
440         if nativeValue < 0 {
441                 return errors.New(option + " must be non-negative")
442         }
443
444         return nil
445 }
446
447 func validateColorscheme(option string, value interface{}) error {
448         colorscheme, ok := value.(string)
449
450         if !ok {
451                 return errors.New("Expected string type for colorscheme")
452         }
453
454         if !ColorschemeExists(colorscheme) {
455                 return errors.New(colorscheme + " is not a valid colorscheme")
456         }
457
458         return nil
459 }
460
461 func validateClipboard(option string, value interface{}) error {
462         val, ok := value.(string)
463
464         if !ok {
465                 return errors.New("Expected string type for clipboard")
466         }
467
468         switch val {
469         case "internal", "external", "terminal":
470         default:
471                 return errors.New(option + " must be 'internal', 'external', or 'terminal'")
472         }
473
474         return nil
475 }
476
477 func validateLineEnding(option string, value interface{}) error {
478         endingType, ok := value.(string)
479
480         if !ok {
481                 return errors.New("Expected string type for file format")
482         }
483
484         if endingType != "unix" && endingType != "dos" {
485                 return errors.New("File format must be either 'unix' or 'dos'")
486         }
487
488         return nil
489 }
490
491 func validateEncoding(option string, value interface{}) error {
492         _, err := htmlindex.Get(value.(string))
493         return err
494 }