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