]> git.lizzy.rs Git - micro.git/blob - internal/config/settings.go
Use filepath.Join more
[micro.git] / internal / config / settings.go
1 package config
2
3 import (
4         "encoding/json"
5         "errors"
6         "io/ioutil"
7         "os"
8         "path/filepath"
9         "reflect"
10         "strconv"
11         "strings"
12
13         "github.com/zyedidia/glob"
14         "github.com/zyedidia/json5"
15         "github.com/zyedidia/micro/internal/util"
16         "golang.org/x/text/encoding/htmlindex"
17 )
18
19 type optionValidator func(string, interface{}) error
20
21 var (
22         ErrInvalidOption = errors.New("Invalid option")
23         ErrInvalidValue  = errors.New("Invalid value")
24
25         // The options that the user can set
26         GlobalSettings map[string]interface{}
27
28         // This is the raw parsed json
29         parsedSettings map[string]interface{}
30 )
31
32 func init() {
33         parsedSettings = make(map[string]interface{})
34 }
35
36 // Options with validators
37 var optionValidators = map[string]optionValidator{
38         "autosave":     validateNonNegativeValue,
39         "tabsize":      validatePositiveValue,
40         "scrollmargin": validateNonNegativeValue,
41         "scrollspeed":  validateNonNegativeValue,
42         "colorscheme":  validateColorscheme,
43         "colorcolumn":  validateNonNegativeValue,
44         "fileformat":   validateLineEnding,
45         "encoding":     validateEncoding,
46 }
47
48 func ReadSettings() error {
49         filename := filepath.Join(ConfigDir, "settings.json")
50         if _, e := os.Stat(filename); e == nil {
51                 input, err := ioutil.ReadFile(filename)
52                 if err != nil {
53                         return errors.New("Error reading settings.json file: " + err.Error())
54                 }
55                 if !strings.HasPrefix(string(input), "null") {
56                         // Unmarshal the input into the parsed map
57                         err = json5.Unmarshal(input, &parsedSettings)
58                         if err != nil {
59                                 return errors.New("Error reading settings.json: " + err.Error())
60                         }
61
62                         // check if autosave is a boolean and convert it to float if so
63                         if v, ok := parsedSettings["autosave"]; ok {
64                                 s, ok := v.(bool)
65                                 if ok {
66                                         if s {
67                                                 parsedSettings["autosave"] = 8.0
68                                         } else {
69                                                 parsedSettings["autosave"] = 0.0
70                                         }
71                                 }
72                         }
73                 }
74         }
75         return nil
76 }
77
78 // InitGlobalSettings initializes the options map and sets all options to their default values
79 // Must be called after ReadSettings
80 func InitGlobalSettings() {
81         GlobalSettings = DefaultGlobalSettings()
82
83         for k, v := range parsedSettings {
84                 if !strings.HasPrefix(reflect.TypeOf(v).String(), "map") {
85                         GlobalSettings[k] = v
86                 }
87         }
88 }
89
90 // InitLocalSettings scans the json in settings.json and sets the options locally based
91 // on whether the filetype or path matches ft or glob local settings
92 // Must be called after ReadSettings
93 func InitLocalSettings(settings map[string]interface{}, path string) error {
94         var parseError error
95         for k, v := range parsedSettings {
96                 if strings.HasPrefix(reflect.TypeOf(v).String(), "map") {
97                         if strings.HasPrefix(k, "ft:") {
98                                 if settings["filetype"].(string) == k[3:] {
99                                         for k1, v1 := range v.(map[string]interface{}) {
100                                                 settings[k1] = v1
101                                         }
102                                 }
103                         } else {
104                                 g, err := glob.Compile(k)
105                                 if err != nil {
106                                         parseError = errors.New("Error with glob setting " + k + ": " + err.Error())
107                                         continue
108                                 }
109
110                                 if g.MatchString(path) {
111                                         for k1, v1 := range v.(map[string]interface{}) {
112                                                 settings[k1] = v1
113                                         }
114                                 }
115                         }
116                 }
117         }
118         return parseError
119 }
120
121 // WriteSettings writes the settings to the specified filename as JSON
122 func WriteSettings(filename string) error {
123         var err error
124         if _, e := os.Stat(ConfigDir); e == nil {
125                 for k, v := range GlobalSettings {
126                         parsedSettings[k] = v
127                 }
128
129                 txt, _ := json.MarshalIndent(parsedSettings, "", "    ")
130                 err = ioutil.WriteFile(filename, append(txt, '\n'), 0644)
131         }
132         return err
133 }
134
135 func OverwriteSettings(filename string) error {
136         var err error
137         if _, e := os.Stat(ConfigDir); e == nil {
138                 txt, _ := json.MarshalIndent(GlobalSettings, "", "    ")
139                 err = ioutil.WriteFile(filename, append(txt, '\n'), 0644)
140         }
141         return err
142 }
143
144 // RegisterCommonOptionPlug creates a new option (called pl.name). This is meant to be called by plugins to add options.
145 func RegisterCommonOptionPlug(pl string, name string, defaultvalue interface{}) error {
146         name = pl + "." + name
147         if v, ok := GlobalSettings[name]; !ok {
148                 defaultCommonSettings[name] = defaultvalue
149                 GlobalSettings[name] = defaultvalue
150                 err := WriteSettings(filepath.Join(ConfigDir, "settings.json"))
151                 if err != nil {
152                         return errors.New("Error writing settings.json file: " + err.Error())
153                 }
154         } else {
155                 defaultCommonSettings[name] = v
156         }
157         return nil
158 }
159
160 // RegisterGlobalOptionPlug creates a new global-only option (named pl.name)
161 func RegisterGlobalOptionPlug(pl string, name string, defaultvalue interface{}) error {
162         return RegisterGlobalOption(pl+"."+name, defaultvalue)
163 }
164
165 // RegisterGlobalOption creates a new global-only option
166 func RegisterGlobalOption(name string, defaultvalue interface{}) error {
167         if v, ok := GlobalSettings[name]; !ok {
168                 defaultGlobalSettings[name] = defaultvalue
169                 GlobalSettings[name] = defaultvalue
170                 err := WriteSettings(filepath.Join(ConfigDir, "settings.json"))
171                 if err != nil {
172                         return errors.New("Error writing settings.json file: " + err.Error())
173                 }
174         } else {
175                 defaultGlobalSettings[name] = v
176         }
177         return nil
178 }
179
180 // GetGlobalOption returns the global value of the given option
181 func GetGlobalOption(name string) interface{} {
182         return GlobalSettings[name]
183 }
184
185 var defaultCommonSettings = map[string]interface{}{
186         "autoindent":     true,
187         "backup":         true,
188         "basename":       false,
189         "colorcolumn":    float64(0),
190         "cursorline":     true,
191         "diffgutter":     false,
192         "encoding":       "utf-8",
193         "eofnewline":     false,
194         "fastdirty":      true,
195         "fileformat":     "unix",
196         "filetype":       "unknown",
197         "ignorecase":     false,
198         "indentchar":     " ",
199         "keepautoindent": false,
200         "matchbrace":     true,
201         "mkparents":      false,
202         "readonly":       false,
203         "rmtrailingws":   false,
204         "ruler":          true,
205         "savecursor":     false,
206         "saveundo":       false,
207         "scrollbar":      false,
208         "scrollmargin":   float64(3),
209         "scrollspeed":    float64(2),
210         "smartpaste":     true,
211         "softwrap":       false,
212         "splitbottom":    true,
213         "splitright":     true,
214         "statusformatl":  "$(filename) $(modified)($(line),$(col)) $(status.paste)| ft:$(opt:filetype) | $(opt:fileformat) | $(opt:encoding)",
215         "statusformatr":  "$(bind:ToggleKeyMenu): bindings, $(bind:ToggleHelp): help",
216         "statusline":     true,
217         "syntax":         true,
218         "tabmovement":    false,
219         "tabsize":        float64(4),
220         "tabstospaces":   false,
221         "useprimary":     true,
222 }
223
224 func GetInfoBarOffset() int {
225         offset := 0
226         if GetGlobalOption("infobar").(bool) {
227                 offset++
228         }
229         if GetGlobalOption("keymenu").(bool) {
230                 offset += 2
231         }
232         return offset
233 }
234
235 // DefaultCommonSettings returns the default global settings for micro
236 // Note that colorscheme is a global only option
237 func DefaultCommonSettings() map[string]interface{} {
238         commonsettings := make(map[string]interface{})
239         for k, v := range defaultCommonSettings {
240                 commonsettings[k] = v
241         }
242         return commonsettings
243 }
244
245 // a list of settings that should only be globally modified and their
246 // default values
247 var defaultGlobalSettings = map[string]interface{}{
248         "autosave":       float64(0),
249         "colorscheme":    "default",
250         "infobar":        true,
251         "keymenu":        false,
252         "mouse":          true,
253         "paste":          false,
254         "savehistory":    true,
255         "sucmd":          "sudo",
256         "pluginchannels": []string{"https://raw.githubusercontent.com/micro-editor/plugin-channel/master/channel.json"},
257         "pluginrepos":    []string{},
258 }
259
260 // a list of settings that should never be globally modified
261 var LocalSettings = []string{
262         "filetype",
263         "readonly",
264 }
265
266 // DefaultGlobalSettings returns the default global settings for micro
267 // Note that colorscheme is a global only option
268 func DefaultGlobalSettings() map[string]interface{} {
269         globalsettings := make(map[string]interface{})
270         for k, v := range defaultCommonSettings {
271                 globalsettings[k] = v
272         }
273         for k, v := range defaultGlobalSettings {
274                 globalsettings[k] = v
275         }
276         return globalsettings
277 }
278
279 // DefaultAllSettings returns a map of all settings and their
280 // default values (both common and global settings)
281 func DefaultAllSettings() map[string]interface{} {
282         allsettings := make(map[string]interface{})
283         for k, v := range defaultCommonSettings {
284                 allsettings[k] = v
285         }
286         for k, v := range defaultGlobalSettings {
287                 allsettings[k] = v
288         }
289         return allsettings
290 }
291
292 // GetNativeValue parses and validates a value for a given option
293 func GetNativeValue(option string, realValue interface{}, value string) (interface{}, error) {
294         var native interface{}
295         kind := reflect.TypeOf(realValue).Kind()
296         if kind == reflect.Bool {
297                 b, err := util.ParseBool(value)
298                 if err != nil {
299                         return nil, ErrInvalidValue
300                 }
301                 native = b
302         } else if kind == reflect.String {
303                 native = value
304         } else if kind == reflect.Float64 {
305                 i, err := strconv.Atoi(value)
306                 if err != nil {
307                         return nil, ErrInvalidValue
308                 }
309                 native = float64(i)
310         } else {
311                 return nil, ErrInvalidValue
312         }
313
314         if err := OptionIsValid(option, native); err != nil {
315                 return nil, err
316         }
317         return native, nil
318 }
319
320 // OptionIsValid checks if a value is valid for a certain option
321 func OptionIsValid(option string, value interface{}) error {
322         if validator, ok := optionValidators[option]; ok {
323                 return validator(option, value)
324         }
325
326         return nil
327 }
328
329 // Option validators
330
331 func validatePositiveValue(option string, value interface{}) error {
332         tabsize, ok := value.(float64)
333
334         if !ok {
335                 return errors.New("Expected numeric type for " + option)
336         }
337
338         if tabsize < 1 {
339                 return errors.New(option + " must be greater than 0")
340         }
341
342         return nil
343 }
344
345 func validateNonNegativeValue(option string, value interface{}) error {
346         nativeValue, ok := value.(float64)
347
348         if !ok {
349                 return errors.New("Expected numeric type for " + option)
350         }
351
352         if nativeValue < 0 {
353                 return errors.New(option + " must be non-negative")
354         }
355
356         return nil
357 }
358
359 func validateColorscheme(option string, value interface{}) error {
360         colorscheme, ok := value.(string)
361
362         if !ok {
363                 return errors.New("Expected string type for colorscheme")
364         }
365
366         if !ColorschemeExists(colorscheme) {
367                 return errors.New(colorscheme + " is not a valid colorscheme")
368         }
369
370         return nil
371 }
372
373 func validateLineEnding(option string, value interface{}) error {
374         endingType, ok := value.(string)
375
376         if !ok {
377                 return errors.New("Expected string type for file format")
378         }
379
380         if endingType != "unix" && endingType != "dos" {
381                 return errors.New("File format must be either 'unix' or 'dos'")
382         }
383
384         return nil
385 }
386
387 func validateEncoding(option string, value interface{}) error {
388         _, err := htmlindex.Get(value.(string))
389         return err
390 }