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