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