]> git.lizzy.rs Git - micro.git/blob - internal/config/settings.go
e8f37e012cb55ef6aa06464e34aa425eebd02aa0
[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         "encoding":       "utf-8",
192         "eofnewline":     false,
193         "fastdirty":      true,
194         "fileformat":     "unix",
195         "filetype":       "unknown",
196         "ignorecase":     false,
197         "indentchar":     " ",
198         "keepautoindent": false,
199         "matchbrace":     true,
200         "mkparents":      false,
201         "readonly":       false,
202         "rmtrailingws":   false,
203         "ruler":          true,
204         "savecursor":     false,
205         "saveundo":       false,
206         "scrollbar":      false,
207         "scrollmargin":   float64(3),
208         "scrollspeed":    float64(2),
209         "smartpaste":     true,
210         "softwrap":       false,
211         "splitbottom":    true,
212         "splitright":     true,
213         "statusformatl":  "$(filename) $(modified)($(line),$(col)) $(status.paste)| ft:$(opt:filetype) | $(opt:fileformat) | $(opt:encoding)",
214         "statusformatr":  "$(bind:ToggleKeyMenu): bindings, $(bind:ToggleHelp): help",
215         "statusline":     true,
216         "syntax":         true,
217         "tabmovement":    false,
218         "tabsize":        float64(4),
219         "tabstospaces":   false,
220         "useprimary":     true,
221 }
222
223 func GetInfoBarOffset() int {
224         offset := 0
225         if GetGlobalOption("infobar").(bool) {
226                 offset++
227         }
228         if GetGlobalOption("keymenu").(bool) {
229                 offset += 2
230         }
231         return offset
232 }
233
234 // DefaultCommonSettings returns the default global settings for micro
235 // Note that colorscheme is a global only option
236 func DefaultCommonSettings() map[string]interface{} {
237         commonsettings := make(map[string]interface{})
238         for k, v := range defaultCommonSettings {
239                 commonsettings[k] = v
240         }
241         return commonsettings
242 }
243
244 // a list of settings that should only be globally modified and their
245 // default values
246 var defaultGlobalSettings = map[string]interface{}{
247         "autosave":       float64(0),
248         "colorscheme":    "default",
249         "infobar":        true,
250         "keymenu":        false,
251         "mouse":          true,
252         "paste":          false,
253         "savehistory":    true,
254         "sucmd":          "sudo",
255         "pluginchannels": []string{"https://raw.githubusercontent.com/micro-editor/plugin-channel/master/channel.json"},
256         "pluginrepos":    []string{},
257 }
258
259 // a list of settings that should never be globally modified
260 var LocalSettings = []string{
261         "filetype",
262         "readonly",
263 }
264
265 // DefaultGlobalSettings returns the default global settings for micro
266 // Note that colorscheme is a global only option
267 func DefaultGlobalSettings() map[string]interface{} {
268         globalsettings := make(map[string]interface{})
269         for k, v := range defaultCommonSettings {
270                 globalsettings[k] = v
271         }
272         for k, v := range defaultGlobalSettings {
273                 globalsettings[k] = v
274         }
275         return globalsettings
276 }
277
278 // DefaultAllSettings returns a map of all settings and their
279 // default values (both common and global settings)
280 func DefaultAllSettings() map[string]interface{} {
281         allsettings := make(map[string]interface{})
282         for k, v := range defaultCommonSettings {
283                 allsettings[k] = v
284         }
285         for k, v := range defaultGlobalSettings {
286                 allsettings[k] = v
287         }
288         return allsettings
289 }
290
291 // GetNativeValue parses and validates a value for a given option
292 func GetNativeValue(option string, realValue interface{}, value string) (interface{}, error) {
293         var native interface{}
294         kind := reflect.TypeOf(realValue).Kind()
295         if kind == reflect.Bool {
296                 b, err := util.ParseBool(value)
297                 if err != nil {
298                         return nil, ErrInvalidValue
299                 }
300                 native = b
301         } else if kind == reflect.String {
302                 native = value
303         } else if kind == reflect.Float64 {
304                 i, err := strconv.Atoi(value)
305                 if err != nil {
306                         return nil, ErrInvalidValue
307                 }
308                 native = float64(i)
309         } else {
310                 return nil, ErrInvalidValue
311         }
312
313         if err := OptionIsValid(option, native); err != nil {
314                 return nil, err
315         }
316         return native, nil
317 }
318
319 // OptionIsValid checks if a value is valid for a certain option
320 func OptionIsValid(option string, value interface{}) error {
321         if validator, ok := optionValidators[option]; ok {
322                 return validator(option, value)
323         }
324
325         return nil
326 }
327
328 // Option validators
329
330 func validatePositiveValue(option string, value interface{}) error {
331         tabsize, ok := value.(float64)
332
333         if !ok {
334                 return errors.New("Expected numeric type for " + option)
335         }
336
337         if tabsize < 1 {
338                 return errors.New(option + " must be greater than 0")
339         }
340
341         return nil
342 }
343
344 func validateNonNegativeValue(option string, value interface{}) error {
345         nativeValue, ok := value.(float64)
346
347         if !ok {
348                 return errors.New("Expected numeric type for " + option)
349         }
350
351         if nativeValue < 0 {
352                 return errors.New(option + " must be non-negative")
353         }
354
355         return nil
356 }
357
358 func validateColorscheme(option string, value interface{}) error {
359         colorscheme, ok := value.(string)
360
361         if !ok {
362                 return errors.New("Expected string type for colorscheme")
363         }
364
365         if !ColorschemeExists(colorscheme) {
366                 return errors.New(colorscheme + " is not a valid colorscheme")
367         }
368
369         return nil
370 }
371
372 func validateLineEnding(option string, value interface{}) error {
373         endingType, ok := value.(string)
374
375         if !ok {
376                 return errors.New("Expected string type for file format")
377         }
378
379         if endingType != "unix" && endingType != "dos" {
380                 return errors.New("File format must be either 'unix' or 'dos'")
381         }
382
383         return nil
384 }
385
386 func validateEncoding(option string, value interface{}) error {
387         _, err := htmlindex.Get(value.(string))
388         return err
389 }