]> git.lizzy.rs Git - micro.git/blob - internal/config/settings.go
2c23f39b433f970d50654557fa7ac1bea5cea77b
[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         "backup":         true,
262         "backupdir":      "",
263         "basename":       false,
264         "colorcolumn":    float64(0),
265         "cursorline":     true,
266         "diffgutter":     false,
267         "encoding":       "utf-8",
268         "eofnewline":     true,
269         "fastdirty":      false,
270         "fileformat":     "unix",
271         "filetype":       "unknown",
272         "hlsearch":       false,
273         "incsearch":      true,
274         "ignorecase":     true,
275         "indentchar":     " ",
276         "keepautoindent": false,
277         "matchbrace":     true,
278         "mkparents":      false,
279         "permbackup":     false,
280         "readonly":       false,
281         "rmtrailingws":   false,
282         "ruler":          true,
283         "relativeruler":  false,
284         "savecursor":     false,
285         "saveundo":       false,
286         "scrollbar":      false,
287         "scrollmargin":   float64(3),
288         "scrollspeed":    float64(2),
289         "smartpaste":     true,
290         "softwrap":       false,
291         "splitbottom":    true,
292         "splitright":     true,
293         "statusformatl":  "$(filename) $(modified)($(line),$(col)) $(status.paste)| ft:$(opt:filetype) | $(opt:fileformat) | $(opt:encoding)",
294         "statusformatr":  "$(bind:ToggleKeyMenu): bindings, $(bind:ToggleHelp): help",
295         "statusline":     true,
296         "syntax":         true,
297         "tabmovement":    false,
298         "tabsize":        float64(4),
299         "tabstospaces":   false,
300         "useprimary":     true,
301         "wordwrap":       false,
302 }
303
304 func GetInfoBarOffset() int {
305         offset := 0
306         if GetGlobalOption("infobar").(bool) {
307                 offset++
308         }
309         if GetGlobalOption("keymenu").(bool) {
310                 offset += 2
311         }
312         return offset
313 }
314
315 // DefaultCommonSettings returns the default global settings for micro
316 // Note that colorscheme is a global only option
317 func DefaultCommonSettings() map[string]interface{} {
318         commonsettings := make(map[string]interface{})
319         for k, v := range defaultCommonSettings {
320                 commonsettings[k] = v
321         }
322         return commonsettings
323 }
324
325 // a list of settings that should only be globally modified and their
326 // default values
327 var DefaultGlobalOnlySettings = map[string]interface{}{
328         "autosave":       float64(0),
329         "clipboard":      "external",
330         "colorscheme":    "default",
331         "divchars":       "|-",
332         "divreverse":     true,
333         "infobar":        true,
334         "keymenu":        false,
335         "mouse":          true,
336         "parsecursor":    false,
337         "paste":          false,
338         "pluginchannels": []string{"https://raw.githubusercontent.com/micro-editor/plugin-channel/master/channel.json"},
339         "pluginrepos":    []string{},
340         "savehistory":    true,
341         "sucmd":          "sudo",
342         "tabhighlight":   false,
343         "tabreverse":     true,
344         "xterm":          false,
345 }
346
347 // a list of settings that should never be globally modified
348 var LocalSettings = []string{
349         "filetype",
350         "readonly",
351 }
352
353 // DefaultGlobalSettings returns the default global settings for micro
354 // Note that colorscheme is a global only option
355 func DefaultGlobalSettings() map[string]interface{} {
356         globalsettings := make(map[string]interface{})
357         for k, v := range defaultCommonSettings {
358                 globalsettings[k] = v
359         }
360         for k, v := range DefaultGlobalOnlySettings {
361                 globalsettings[k] = v
362         }
363         return globalsettings
364 }
365
366 // DefaultAllSettings returns a map of all settings and their
367 // default values (both common and global settings)
368 func DefaultAllSettings() map[string]interface{} {
369         allsettings := make(map[string]interface{})
370         for k, v := range defaultCommonSettings {
371                 allsettings[k] = v
372         }
373         for k, v := range DefaultGlobalOnlySettings {
374                 allsettings[k] = v
375         }
376         return allsettings
377 }
378
379 // GetNativeValue parses and validates a value for a given option
380 func GetNativeValue(option string, realValue interface{}, value string) (interface{}, error) {
381         var native interface{}
382         kind := reflect.TypeOf(realValue).Kind()
383         if kind == reflect.Bool {
384                 b, err := util.ParseBool(value)
385                 if err != nil {
386                         return nil, ErrInvalidValue
387                 }
388                 native = b
389         } else if kind == reflect.String {
390                 native = value
391         } else if kind == reflect.Float64 {
392                 i, err := strconv.Atoi(value)
393                 if err != nil {
394                         return nil, ErrInvalidValue
395                 }
396                 native = float64(i)
397         } else {
398                 return nil, ErrInvalidValue
399         }
400
401         if err := OptionIsValid(option, native); err != nil {
402                 return nil, err
403         }
404         return native, nil
405 }
406
407 // OptionIsValid checks if a value is valid for a certain option
408 func OptionIsValid(option string, value interface{}) error {
409         if validator, ok := optionValidators[option]; ok {
410                 return validator(option, value)
411         }
412
413         return nil
414 }
415
416 // Option validators
417
418 func validatePositiveValue(option string, value interface{}) error {
419         tabsize, ok := value.(float64)
420
421         if !ok {
422                 return errors.New("Expected numeric type for " + option)
423         }
424
425         if tabsize < 1 {
426                 return errors.New(option + " must be greater than 0")
427         }
428
429         return nil
430 }
431
432 func validateNonNegativeValue(option string, value interface{}) error {
433         nativeValue, ok := value.(float64)
434
435         if !ok {
436                 return errors.New("Expected numeric type for " + option)
437         }
438
439         if nativeValue < 0 {
440                 return errors.New(option + " must be non-negative")
441         }
442
443         return nil
444 }
445
446 func validateColorscheme(option string, value interface{}) error {
447         colorscheme, ok := value.(string)
448
449         if !ok {
450                 return errors.New("Expected string type for colorscheme")
451         }
452
453         if !ColorschemeExists(colorscheme) {
454                 return errors.New(colorscheme + " is not a valid colorscheme")
455         }
456
457         return nil
458 }
459
460 func validateClipboard(option string, value interface{}) error {
461         val, ok := value.(string)
462
463         if !ok {
464                 return errors.New("Expected string type for clipboard")
465         }
466
467         switch val {
468         case "internal", "external", "terminal":
469         default:
470                 return errors.New(option + " must be 'internal', 'external', or 'terminal'")
471         }
472
473         return nil
474 }
475
476 func validateLineEnding(option string, value interface{}) error {
477         endingType, ok := value.(string)
478
479         if !ok {
480                 return errors.New("Expected string type for file format")
481         }
482
483         if endingType != "unix" && endingType != "dos" {
484                 return errors.New("File format must be either 'unix' or 'dos'")
485         }
486
487         return nil
488 }
489
490 func validateEncoding(option string, value interface{}) error {
491         _, err := htmlindex.Get(value.(string))
492         return err
493 }