]> git.lizzy.rs Git - micro.git/blob - internal/config/settings.go
Merge
[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         "tabsize":      validatePositiveValue,
47         "scrollmargin": validateNonNegativeValue,
48         "scrollspeed":  validateNonNegativeValue,
49         "colorscheme":  validateColorscheme,
50         "colorcolumn":  validateNonNegativeValue,
51         "fileformat":   validateLineEnding,
52         "encoding":     validateEncoding,
53 }
54
55 func ReadSettings() error {
56         filename := filepath.Join(ConfigDir, "settings.json")
57         if _, e := os.Stat(filename); e == nil {
58                 input, err := ioutil.ReadFile(filename)
59                 if err != nil {
60                         settingsParseError = true
61                         return errors.New("Error reading settings.json file: " + err.Error())
62                 }
63                 if !strings.HasPrefix(string(input), "null") {
64                         // Unmarshal the input into the parsed map
65                         err = json5.Unmarshal(input, &parsedSettings)
66                         if err != nil {
67                                 settingsParseError = true
68                                 return errors.New("Error reading settings.json: " + err.Error())
69                         }
70
71                         // check if autosave is a boolean and convert it to float if so
72                         if v, ok := parsedSettings["autosave"]; ok {
73                                 s, ok := v.(bool)
74                                 if ok {
75                                         if s {
76                                                 parsedSettings["autosave"] = 8.0
77                                         } else {
78                                                 parsedSettings["autosave"] = 0.0
79                                         }
80                                 }
81                         }
82                 }
83         }
84         return nil
85 }
86
87 func verifySetting(option string, value reflect.Type, def reflect.Type) bool {
88         var interfaceArr []interface{}
89         switch option {
90         case "pluginrepos", "pluginchannels":
91                 return value.AssignableTo(reflect.TypeOf(interfaceArr))
92         default:
93                 return def.AssignableTo(value)
94         }
95 }
96
97 // InitGlobalSettings initializes the options map and sets all options to their default values
98 // Must be called after ReadSettings
99 func InitGlobalSettings() error {
100         var err error
101         GlobalSettings = DefaultGlobalSettings()
102
103         for k, v := range parsedSettings {
104                 if !strings.HasPrefix(reflect.TypeOf(v).String(), "map") {
105                         if _, ok := GlobalSettings[k]; ok && !verifySetting(k, reflect.TypeOf(v), reflect.TypeOf(GlobalSettings[k])) {
106                                 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                                 continue
108                         }
109
110                         GlobalSettings[k] = v
111                 }
112         }
113         return err
114 }
115
116 // InitLocalSettings scans the json in settings.json and sets the options locally based
117 // on whether the filetype or path matches ft or glob local settings
118 // Must be called after ReadSettings
119 func InitLocalSettings(settings map[string]interface{}, path string) error {
120         var parseError error
121         for k, v := range parsedSettings {
122                 if strings.HasPrefix(reflect.TypeOf(v).String(), "map") {
123                         if strings.HasPrefix(k, "ft:") {
124                                 if settings["filetype"].(string) == k[3:] {
125                                         for k1, v1 := range v.(map[string]interface{}) {
126                                                 if _, ok := settings[k1]; ok && !verifySetting(k1, reflect.TypeOf(v1), reflect.TypeOf(settings[k1])) {
127                                                         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])))
128                                                         continue
129                                                 }
130                                                 settings[k1] = v1
131                                         }
132                                 }
133                         } else {
134                                 g, err := glob.Compile(k)
135                                 if err != nil {
136                                         parseError = errors.New("Error with glob setting " + k + ": " + err.Error())
137                                         continue
138                                 }
139
140                                 if g.MatchString(path) {
141                                         for k1, v1 := range v.(map[string]interface{}) {
142                                                 if _, ok := settings[k1]; ok && !verifySetting(k1, reflect.TypeOf(v1), reflect.TypeOf(settings[k1])) {
143                                                         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])))
144                                                         continue
145                                                 }
146                                                 settings[k1] = v1
147                                         }
148                                 }
149                         }
150                 }
151         }
152         return parseError
153 }
154
155 // WriteSettings writes the settings to the specified filename as JSON
156 func WriteSettings(filename string) error {
157         if settingsParseError {
158                 // Don't write settings if there was a parse error
159                 // because this will delete the settings.json if it
160                 // is invalid. Instead we should allow the user to fix
161                 // it manually.
162                 return nil
163         }
164
165         var err error
166         if _, e := os.Stat(ConfigDir); e == nil {
167                 defaults := DefaultGlobalSettings()
168
169                 // remove any options froms parsedSettings that have since been marked as default
170                 for k, v := range parsedSettings {
171                         if !strings.HasPrefix(reflect.TypeOf(v).String(), "map") {
172                                 cur, okcur := GlobalSettings[k]
173                                 if def, ok := defaults[k]; ok && okcur && reflect.DeepEqual(cur, def) {
174                                         delete(parsedSettings, k)
175                                 }
176                         }
177                 }
178
179                 // add any options to parsedSettings that have since been marked as non-default
180                 for k, v := range GlobalSettings {
181                         if def, ok := defaults[k]; !ok || !reflect.DeepEqual(v, def) {
182                                 if _, wr := ModifiedSettings[k]; wr {
183                                         parsedSettings[k] = v
184                                 }
185                         }
186                 }
187
188                 txt, _ := json.MarshalIndent(parsedSettings, "", "    ")
189                 err = ioutil.WriteFile(filename, append(txt, '\n'), 0644)
190         }
191         return err
192 }
193
194 // OverwriteSettings writes the current settings to settings.json and
195 // resets any user configuration of local settings present in settings.json
196 func OverwriteSettings(filename string) error {
197         settings := make(map[string]interface{})
198
199         var err error
200         if _, e := os.Stat(ConfigDir); e == nil {
201                 defaults := DefaultGlobalSettings()
202                 for k, v := range GlobalSettings {
203                         if def, ok := defaults[k]; !ok || !reflect.DeepEqual(v, def) {
204                                 if _, wr := ModifiedSettings[k]; wr {
205                                         settings[k] = v
206                                 }
207                         }
208                 }
209
210                 txt, _ := json.MarshalIndent(settings, "", "    ")
211                 err = ioutil.WriteFile(filename, append(txt, '\n'), 0644)
212         }
213         return err
214 }
215
216 // RegisterCommonOptionPlug creates a new option (called pl.name). This is meant to be called by plugins to add options.
217 func RegisterCommonOptionPlug(pl string, name string, defaultvalue interface{}) error {
218         name = pl + "." + name
219         if _, ok := GlobalSettings[name]; !ok {
220                 defaultCommonSettings[name] = defaultvalue
221                 GlobalSettings[name] = defaultvalue
222                 err := WriteSettings(filepath.Join(ConfigDir, "settings.json"))
223                 if err != nil {
224                         return errors.New("Error writing settings.json file: " + err.Error())
225                 }
226         } else {
227                 defaultCommonSettings[name] = defaultvalue
228         }
229         return nil
230 }
231
232 // RegisterGlobalOptionPlug creates a new global-only option (named pl.name)
233 func RegisterGlobalOptionPlug(pl string, name string, defaultvalue interface{}) error {
234         return RegisterGlobalOption(pl+"."+name, defaultvalue)
235 }
236
237 // RegisterGlobalOption creates a new global-only option
238 func RegisterGlobalOption(name string, defaultvalue interface{}) error {
239         if v, ok := GlobalSettings[name]; !ok {
240                 DefaultGlobalOnlySettings[name] = defaultvalue
241                 GlobalSettings[name] = defaultvalue
242                 err := WriteSettings(filepath.Join(ConfigDir, "settings.json"))
243                 if err != nil {
244                         return errors.New("Error writing settings.json file: " + err.Error())
245                 }
246         } else {
247                 DefaultGlobalOnlySettings[name] = v
248         }
249         return nil
250 }
251
252 // GetGlobalOption returns the global value of the given option
253 func GetGlobalOption(name string) interface{} {
254         return GlobalSettings[name]
255 }
256
257 var defaultCommonSettings = map[string]interface{}{
258         "autoindent":     true,
259         "autosu":         false,
260         "backup":         true,
261         "backupdir":      "",
262         "basename":       false,
263         "colorcolumn":    float64(0),
264         "cursorline":     true,
265         "diffgutter":     false,
266         "encoding":       "utf-8",
267         "eofnewline":     true,
268         "fastdirty":      false,
269         "fileformat":     "unix",
270         "filetype":       "unknown",
271         "ignorecase":     false,
272         "indentchar":     " ",
273         "keepautoindent": false,
274         "matchbrace":     true,
275         "mkparents":      false,
276         "permbackup":     false,
277         "readonly":       false,
278         "rmtrailingws":   false,
279         "ruler":          true,
280         "relativeruler":  false,
281         "savecursor":     false,
282         "saveundo":       false,
283         "scrollbar":      false,
284         "scrollmargin":   float64(3),
285         "scrollspeed":    float64(2),
286         "smartpaste":     true,
287         "softwrap":       false,
288         "splitbottom":    true,
289         "splitright":     true,
290         "statusformatl":  "$(filename) $(modified)($(line),$(col)) $(status.paste)| ft:$(opt:filetype) | $(opt:fileformat) | $(opt:encoding)",
291         "statusformatr":  "$(bind:ToggleKeyMenu): bindings, $(bind:ToggleHelp): help",
292         "statusline":     true,
293         "syntax":         true,
294         "tabmovement":    false,
295         "tabsize":        float64(4),
296         "tabstospaces":   false,
297         "useprimary":     true,
298 }
299
300 func GetInfoBarOffset() int {
301         offset := 0
302         if GetGlobalOption("infobar").(bool) {
303                 offset++
304         }
305         if GetGlobalOption("keymenu").(bool) {
306                 offset += 2
307         }
308         return offset
309 }
310
311 // DefaultCommonSettings returns the default global settings for micro
312 // Note that colorscheme is a global only option
313 func DefaultCommonSettings() map[string]interface{} {
314         commonsettings := make(map[string]interface{})
315         for k, v := range defaultCommonSettings {
316                 commonsettings[k] = v
317         }
318         return commonsettings
319 }
320
321 // a list of settings that should only be globally modified and their
322 // default values
323 var DefaultGlobalOnlySettings = map[string]interface{}{
324         "autosave":       float64(0),
325         "colorscheme":    "default",
326         "divchars":       "|-",
327         "divreverse":     true,
328         "infobar":        true,
329         "keymenu":        false,
330         "mouse":          true,
331         "parsecursor":    false,
332         "paste":          false,
333         "savehistory":    true,
334         "sucmd":          "sudo",
335         "pluginchannels": []string{"https://raw.githubusercontent.com/micro-editor/plugin-channel/master/channel.json"},
336         "pluginrepos":    []string{},
337         "xterm":          false,
338 }
339
340 // a list of settings that should never be globally modified
341 var LocalSettings = []string{
342         "filetype",
343         "readonly",
344 }
345
346 // DefaultGlobalSettings returns the default global settings for micro
347 // Note that colorscheme is a global only option
348 func DefaultGlobalSettings() map[string]interface{} {
349         globalsettings := make(map[string]interface{})
350         for k, v := range defaultCommonSettings {
351                 globalsettings[k] = v
352         }
353         for k, v := range DefaultGlobalOnlySettings {
354                 globalsettings[k] = v
355         }
356         return globalsettings
357 }
358
359 // DefaultAllSettings returns a map of all settings and their
360 // default values (both common and global settings)
361 func DefaultAllSettings() map[string]interface{} {
362         allsettings := make(map[string]interface{})
363         for k, v := range defaultCommonSettings {
364                 allsettings[k] = v
365         }
366         for k, v := range DefaultGlobalOnlySettings {
367                 allsettings[k] = v
368         }
369         return allsettings
370 }
371
372 // GetNativeValue parses and validates a value for a given option
373 func GetNativeValue(option string, realValue interface{}, value string) (interface{}, error) {
374         var native interface{}
375         kind := reflect.TypeOf(realValue).Kind()
376         if kind == reflect.Bool {
377                 b, err := util.ParseBool(value)
378                 if err != nil {
379                         return nil, ErrInvalidValue
380                 }
381                 native = b
382         } else if kind == reflect.String {
383                 native = value
384         } else if kind == reflect.Float64 {
385                 i, err := strconv.Atoi(value)
386                 if err != nil {
387                         return nil, ErrInvalidValue
388                 }
389                 native = float64(i)
390         } else {
391                 return nil, ErrInvalidValue
392         }
393
394         if err := OptionIsValid(option, native); err != nil {
395                 return nil, err
396         }
397         return native, nil
398 }
399
400 // OptionIsValid checks if a value is valid for a certain option
401 func OptionIsValid(option string, value interface{}) error {
402         if validator, ok := optionValidators[option]; ok {
403                 return validator(option, value)
404         }
405
406         return nil
407 }
408
409 // Option validators
410
411 func validatePositiveValue(option string, value interface{}) error {
412         tabsize, ok := value.(float64)
413
414         if !ok {
415                 return errors.New("Expected numeric type for " + option)
416         }
417
418         if tabsize < 1 {
419                 return errors.New(option + " must be greater than 0")
420         }
421
422         return nil
423 }
424
425 func validateNonNegativeValue(option string, value interface{}) error {
426         nativeValue, ok := value.(float64)
427
428         if !ok {
429                 return errors.New("Expected numeric type for " + option)
430         }
431
432         if nativeValue < 0 {
433                 return errors.New(option + " must be non-negative")
434         }
435
436         return nil
437 }
438
439 func validateColorscheme(option string, value interface{}) error {
440         colorscheme, ok := value.(string)
441
442         if !ok {
443                 return errors.New("Expected string type for colorscheme")
444         }
445
446         if !ColorschemeExists(colorscheme) {
447                 return errors.New(colorscheme + " is not a valid colorscheme")
448         }
449
450         return nil
451 }
452
453 func validateLineEnding(option string, value interface{}) error {
454         endingType, ok := value.(string)
455
456         if !ok {
457                 return errors.New("Expected string type for file format")
458         }
459
460         if endingType != "unix" && endingType != "dos" {
461                 return errors.New("File format must be either 'unix' or 'dos'")
462         }
463
464         return nil
465 }
466
467 func validateEncoding(option string, value interface{}) error {
468         _, err := htmlindex.Get(value.(string))
469         return err
470 }