]> git.lizzy.rs Git - micro.git/blob - internal/config/settings.go
Persist plugin options correctly
[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
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
35 )
36
37 func init() {
38         ModifiedSettings = make(map[string]bool)
39         parsedSettings = make(map[string]interface{})
40 }
41
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,
52 }
53
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)
58                 if err != nil {
59                         return errors.New("Error reading settings.json file: " + err.Error())
60                 }
61                 if !strings.HasPrefix(string(input), "null") {
62                         // Unmarshal the input into the parsed map
63                         err = json5.Unmarshal(input, &parsedSettings)
64                         if err != nil {
65                                 return errors.New("Error reading settings.json: " + err.Error())
66                         }
67
68                         // check if autosave is a boolean and convert it to float if so
69                         if v, ok := parsedSettings["autosave"]; ok {
70                                 s, ok := v.(bool)
71                                 if ok {
72                                         if s {
73                                                 parsedSettings["autosave"] = 8.0
74                                         } else {
75                                                 parsedSettings["autosave"] = 0.0
76                                         }
77                                 }
78                         }
79                 }
80         }
81         return nil
82 }
83
84 func verifySetting(option string, value reflect.Type, def reflect.Type) bool {
85         var interfaceArr []interface{}
86         switch option {
87         case "pluginrepos", "pluginchannels":
88                 return value.AssignableTo(reflect.TypeOf(interfaceArr))
89         default:
90                 return def.AssignableTo(value)
91         }
92 }
93
94 // InitGlobalSettings initializes the options map and sets all options to their default values
95 // Must be called after ReadSettings
96 func InitGlobalSettings() error {
97         var err error
98         GlobalSettings = DefaultGlobalSettings()
99
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])))
104                                 continue
105                         }
106
107                         GlobalSettings[k] = v
108                 }
109         }
110         return err
111 }
112
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 {
117         var parseError 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])))
125                                                         continue
126                                                 }
127                                                 settings[k1] = v1
128                                         }
129                                 }
130                         } else {
131                                 g, err := glob.Compile(k)
132                                 if err != nil {
133                                         parseError = errors.New("Error with glob setting " + k + ": " + err.Error())
134                                         continue
135                                 }
136
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])))
141                                                         continue
142                                                 }
143                                                 settings[k1] = v1
144                                         }
145                                 }
146                         }
147                 }
148         }
149         return parseError
150 }
151
152 // WriteSettings writes the settings to the specified filename as JSON
153 func WriteSettings(filename string) error {
154         var err error
155         if _, e := os.Stat(ConfigDir); e == nil {
156                 defaults := DefaultGlobalSettings()
157
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)
164                                 }
165                         }
166                 }
167
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
173                                 }
174                         }
175                 }
176
177                 txt, _ := json.MarshalIndent(parsedSettings, "", "    ")
178                 err = ioutil.WriteFile(filename, append(txt, '\n'), 0644)
179         }
180         return err
181 }
182
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{})
187
188         var err error
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 {
194                                         settings[k] = v
195                                 }
196                         }
197                 }
198
199                 txt, _ := json.MarshalIndent(settings, "", "    ")
200                 err = ioutil.WriteFile(filename, append(txt, '\n'), 0644)
201         }
202         return err
203 }
204
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"))
212                 if err != nil {
213                         return errors.New("Error writing settings.json file: " + err.Error())
214                 }
215         } else {
216                 defaultCommonSettings[name] = defaultvalue
217         }
218         return nil
219 }
220
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)
224 }
225
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"))
232                 if err != nil {
233                         return errors.New("Error writing settings.json file: " + err.Error())
234                 }
235         } else {
236                 DefaultGlobalOnlySettings[name] = v
237         }
238         return nil
239 }
240
241 // GetGlobalOption returns the global value of the given option
242 func GetGlobalOption(name string) interface{} {
243         return GlobalSettings[name]
244 }
245
246 var defaultCommonSettings = map[string]interface{}{
247         "autoindent":     true,
248         "autosu":         false,
249         "backup":         true,
250         "basename":       false,
251         "colorcolumn":    float64(0),
252         "cursorline":     true,
253         "diffgutter":     false,
254         "encoding":       "utf-8",
255         "eofnewline":     true,
256         "fastdirty":      false,
257         "fileformat":     "unix",
258         "filetype":       "unknown",
259         "ignorecase":     false,
260         "indentchar":     " ",
261         "keepautoindent": false,
262         "matchbrace":     true,
263         "mkparents":      false,
264         "readonly":       false,
265         "rmtrailingws":   false,
266         "ruler":          true,
267         "relativeruler":  false,
268         "savecursor":     false,
269         "saveundo":       false,
270         "scrollbar":      false,
271         "scrollmargin":   float64(3),
272         "scrollspeed":    float64(2),
273         "smartpaste":     true,
274         "softwrap":       false,
275         "splitbottom":    true,
276         "splitright":     true,
277         "statusformatl":  "$(filename) $(modified)($(line),$(col)) $(status.paste)| ft:$(opt:filetype) | $(opt:fileformat) | $(opt:encoding)",
278         "statusformatr":  "$(bind:ToggleKeyMenu): bindings, $(bind:ToggleHelp): help",
279         "statusline":     true,
280         "syntax":         true,
281         "tabmovement":    false,
282         "tabsize":        float64(4),
283         "tabstospaces":   false,
284         "useprimary":     true,
285 }
286
287 func GetInfoBarOffset() int {
288         offset := 0
289         if GetGlobalOption("infobar").(bool) {
290                 offset++
291         }
292         if GetGlobalOption("keymenu").(bool) {
293                 offset += 2
294         }
295         return offset
296 }
297
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
304         }
305         return commonsettings
306 }
307
308 // a list of settings that should only be globally modified and their
309 // default values
310 var DefaultGlobalOnlySettings = map[string]interface{}{
311         "autosave":       float64(0),
312         "colorscheme":    "default",
313         "divchars":       "|-",
314         "divreverse":     true,
315         "infobar":        true,
316         "keymenu":        false,
317         "mouse":          true,
318         "parsecursor":    false,
319         "paste":          false,
320         "savehistory":    true,
321         "sucmd":          "sudo",
322         "pluginchannels": []string{"https://raw.githubusercontent.com/micro-editor/plugin-channel/master/channel.json"},
323         "pluginrepos":    []string{},
324         "xterm":          false,
325 }
326
327 // a list of settings that should never be globally modified
328 var LocalSettings = []string{
329         "filetype",
330         "readonly",
331 }
332
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
339         }
340         for k, v := range DefaultGlobalOnlySettings {
341                 globalsettings[k] = v
342         }
343         return globalsettings
344 }
345
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 {
351                 allsettings[k] = v
352         }
353         for k, v := range DefaultGlobalOnlySettings {
354                 allsettings[k] = v
355         }
356         return allsettings
357 }
358
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)
365                 if err != nil {
366                         return nil, ErrInvalidValue
367                 }
368                 native = b
369         } else if kind == reflect.String {
370                 native = value
371         } else if kind == reflect.Float64 {
372                 i, err := strconv.Atoi(value)
373                 if err != nil {
374                         return nil, ErrInvalidValue
375                 }
376                 native = float64(i)
377         } else {
378                 return nil, ErrInvalidValue
379         }
380
381         if err := OptionIsValid(option, native); err != nil {
382                 return nil, err
383         }
384         return native, nil
385 }
386
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)
391         }
392
393         return nil
394 }
395
396 // Option validators
397
398 func validatePositiveValue(option string, value interface{}) error {
399         tabsize, ok := value.(float64)
400
401         if !ok {
402                 return errors.New("Expected numeric type for " + option)
403         }
404
405         if tabsize < 1 {
406                 return errors.New(option + " must be greater than 0")
407         }
408
409         return nil
410 }
411
412 func validateNonNegativeValue(option string, value interface{}) error {
413         nativeValue, ok := value.(float64)
414
415         if !ok {
416                 return errors.New("Expected numeric type for " + option)
417         }
418
419         if nativeValue < 0 {
420                 return errors.New(option + " must be non-negative")
421         }
422
423         return nil
424 }
425
426 func validateColorscheme(option string, value interface{}) error {
427         colorscheme, ok := value.(string)
428
429         if !ok {
430                 return errors.New("Expected string type for colorscheme")
431         }
432
433         if !ColorschemeExists(colorscheme) {
434                 return errors.New(colorscheme + " is not a valid colorscheme")
435         }
436
437         return nil
438 }
439
440 func validateLineEnding(option string, value interface{}) error {
441         endingType, ok := value.(string)
442
443         if !ok {
444                 return errors.New("Expected string type for file format")
445         }
446
447         if endingType != "unix" && endingType != "dos" {
448                 return errors.New("File format must be either 'unix' or 'dos'")
449         }
450
451         return nil
452 }
453
454 func validateEncoding(option string, value interface{}) error {
455         _, err := htmlindex.Get(value.(string))
456         return err
457 }