]> git.lizzy.rs Git - micro.git/blob - cmd/micro/settings.go
Add 'autosave' option
[micro.git] / cmd / micro / settings.go
1 package main
2
3 import (
4         "errors"
5         "io/ioutil"
6         "os"
7         "reflect"
8         "strconv"
9         "strings"
10
11         "github.com/yosuke-furukawa/json5/encoding/json5"
12         "github.com/zyedidia/glob"
13 )
14
15 type optionValidator func(string, interface{}) error
16
17 // The options that the user can set
18 var globalSettings map[string]interface{}
19
20 // Options with validators
21 var optionValidators = map[string]optionValidator{
22         "tabsize":      validatePositiveValue,
23         "scrollmargin": validateNonNegativeValue,
24         "scrollspeed":  validateNonNegativeValue,
25         "colorscheme":  validateColorscheme,
26         "colorcolumn":  validateNonNegativeValue,
27 }
28
29 // InitGlobalSettings initializes the options map and sets all options to their default values
30 func InitGlobalSettings() {
31         defaults := DefaultGlobalSettings()
32         var parsed map[string]interface{}
33
34         filename := configDir + "/settings.json"
35         writeSettings := false
36         if _, e := os.Stat(filename); e == nil {
37                 input, err := ioutil.ReadFile(filename)
38                 if !strings.HasPrefix(string(input), "null") {
39                         if err != nil {
40                                 TermMessage("Error reading settings.json file: " + err.Error())
41                                 return
42                         }
43
44                         err = json5.Unmarshal(input, &parsed)
45                         if err != nil {
46                                 TermMessage("Error reading settings.json:", err.Error())
47                         }
48                 } else {
49                         writeSettings = true
50                 }
51         }
52
53         globalSettings = make(map[string]interface{})
54         for k, v := range defaults {
55                 globalSettings[k] = v
56         }
57         for k, v := range parsed {
58                 if !strings.HasPrefix(reflect.TypeOf(v).String(), "map") {
59                         globalSettings[k] = v
60                 }
61         }
62
63         if _, err := os.Stat(filename); os.IsNotExist(err) || writeSettings {
64                 err := WriteSettings(filename)
65                 if err != nil {
66                         TermMessage("Error writing settings.json file: " + err.Error())
67                 }
68         }
69 }
70
71 // InitLocalSettings scans the json in settings.json and sets the options locally based
72 // on whether the buffer matches the glob
73 func InitLocalSettings(buf *Buffer) {
74         var parsed map[string]interface{}
75
76         filename := configDir + "/settings.json"
77         if _, e := os.Stat(filename); e == nil {
78                 input, err := ioutil.ReadFile(filename)
79                 if err != nil {
80                         TermMessage("Error reading settings.json file: " + err.Error())
81                         return
82                 }
83
84                 err = json5.Unmarshal(input, &parsed)
85                 if err != nil {
86                         TermMessage("Error reading settings.json:", err.Error())
87                 }
88         }
89
90         for k, v := range parsed {
91                 if strings.HasPrefix(reflect.TypeOf(v).String(), "map") {
92                         g, err := glob.Compile(k)
93                         if err != nil {
94                                 TermMessage("Error with glob setting ", k, ": ", err)
95                                 continue
96                         }
97
98                         if g.MatchString(buf.Path) {
99                                 for k1, v1 := range v.(map[string]interface{}) {
100                                         buf.Settings[k1] = v1
101                                 }
102                         }
103                 }
104         }
105 }
106
107 // WriteSettings writes the settings to the specified filename as JSON
108 func WriteSettings(filename string) error {
109         var err error
110         if _, e := os.Stat(configDir); e == nil {
111                 parsed := make(map[string]interface{})
112
113                 filename := configDir + "/settings.json"
114                 for k, v := range globalSettings {
115                         parsed[k] = v
116                 }
117                 if _, e := os.Stat(filename); e == nil {
118                         input, err := ioutil.ReadFile(filename)
119                         if string(input) != "null" {
120                                 if err != nil {
121                                         return err
122                                 }
123
124                                 err = json5.Unmarshal(input, &parsed)
125                                 if err != nil {
126                                         TermMessage("Error reading settings.json:", err.Error())
127                                 }
128
129                                 for k, v := range parsed {
130                                         if !strings.HasPrefix(reflect.TypeOf(v).String(), "map") {
131                                                 if _, ok := globalSettings[k]; ok {
132                                                         parsed[k] = globalSettings[k]
133                                                 }
134                                         }
135                                 }
136                         }
137                 }
138
139                 txt, _ := json5.MarshalIndent(parsed, "", "    ")
140                 err = ioutil.WriteFile(filename, append(txt, '\n'), 0644)
141         }
142         return err
143 }
144
145 // AddOption creates a new option. This is meant to be called by plugins to add options.
146 func AddOption(name string, value interface{}) {
147         globalSettings[name] = value
148         err := WriteSettings(configDir + "/settings.json")
149         if err != nil {
150                 TermMessage("Error writing settings.json file: " + err.Error())
151         }
152 }
153
154 // GetGlobalOption returns the global value of the given option
155 func GetGlobalOption(name string) interface{} {
156         return globalSettings[name]
157 }
158
159 // GetLocalOption returns the local value of the given option
160 func GetLocalOption(name string, buf *Buffer) interface{} {
161         return buf.Settings[name]
162 }
163
164 // GetOption returns the value of the given option
165 // If there is a local version of the option, it returns that
166 // otherwise it will return the global version
167 func GetOption(name string) interface{} {
168         if GetLocalOption(name, CurView().Buf) != nil {
169                 return GetLocalOption(name, CurView().Buf)
170         }
171         return GetGlobalOption(name)
172 }
173
174 // DefaultGlobalSettings returns the default global settings for micro
175 // Note that colorscheme is a global only option
176 func DefaultGlobalSettings() map[string]interface{} {
177         return map[string]interface{}{
178                 "autoindent":   true,
179                 "autosave":     false,
180                 "colorcolumn":  float64(0),
181                 "colorscheme":  "zenburn",
182                 "cursorline":   true,
183                 "ignorecase":   false,
184                 "indentchar":   " ",
185                 "infobar":      true,
186                 "ruler":        true,
187                 "savecursor":   false,
188                 "saveundo":     false,
189                 "scrollspeed":  float64(2),
190                 "scrollmargin": float64(3),
191                 "statusline":   true,
192                 "syntax":       true,
193                 "tabsize":      float64(4),
194                 "tabstospaces": false,
195         }
196 }
197
198 // DefaultLocalSettings returns the default local settings
199 // Note that filetype is a local only option
200 func DefaultLocalSettings() map[string]interface{} {
201         return map[string]interface{}{
202                 "autoindent":   true,
203                 "autosave":     false,
204                 "colorcolumn":  float64(0),
205                 "cursorline":   true,
206                 "filetype":     "Unknown",
207                 "ignorecase":   false,
208                 "indentchar":   " ",
209                 "ruler":        true,
210                 "savecursor":   false,
211                 "saveundo":     false,
212                 "scrollspeed":  float64(2),
213                 "scrollmargin": float64(3),
214                 "statusline":   true,
215                 "syntax":       true,
216                 "tabsize":      float64(4),
217                 "tabstospaces": false,
218         }
219 }
220
221 // SetOption attempts to set the given option to the value
222 // By default it will set the option as global, but if the option
223 // is local only it will set the local version
224 // Use setlocal to force an option to be set locally
225 func SetOption(option, value string) error {
226         if _, ok := globalSettings[option]; !ok {
227                 if _, ok := CurView().Buf.Settings[option]; !ok {
228                         return errors.New("Invalid option")
229                 }
230                 SetLocalOption(option, value, CurView())
231                 return nil
232         }
233
234         var nativeValue interface{}
235
236         kind := reflect.TypeOf(globalSettings[option]).Kind()
237         if kind == reflect.Bool {
238                 b, err := ParseBool(value)
239                 if err != nil {
240                         return errors.New("Invalid value")
241                 }
242                 nativeValue = b
243         } else if kind == reflect.String {
244                 nativeValue = value
245         } else if kind == reflect.Float64 {
246                 i, err := strconv.Atoi(value)
247                 if err != nil {
248                         return errors.New("Invalid value")
249                 }
250                 nativeValue = float64(i)
251         } else {
252                 return errors.New("Option has unsupported value type")
253         }
254
255         if err := optionIsValid(option, nativeValue); err != nil {
256                 return err
257         }
258
259         globalSettings[option] = nativeValue
260
261         if option == "colorscheme" {
262                 LoadSyntaxFiles()
263                 for _, tab := range tabs {
264                         for _, view := range tab.views {
265                                 view.Buf.UpdateRules()
266                                 if view.Buf.Settings["syntax"].(bool) {
267                                         view.matches = Match(view)
268                                 }
269                         }
270                 }
271         }
272
273         if option == "infobar" {
274                 for _, tab := range tabs {
275                         tab.Resize()
276                 }
277         }
278
279         if _, ok := CurView().Buf.Settings[option]; ok {
280                 for _, tab := range tabs {
281                         for _, view := range tab.views {
282                                 SetLocalOption(option, value, view)
283                         }
284                 }
285         }
286
287         return nil
288 }
289
290 // SetLocalOption sets the local version of this option
291 func SetLocalOption(option, value string, view *View) error {
292         buf := view.Buf
293         if _, ok := buf.Settings[option]; !ok {
294                 return errors.New("Invalid option")
295         }
296
297         var nativeValue interface{}
298
299         kind := reflect.TypeOf(buf.Settings[option]).Kind()
300         if kind == reflect.Bool {
301                 b, err := ParseBool(value)
302                 if err != nil {
303                         return errors.New("Invalid value")
304                 }
305                 nativeValue = b
306         } else if kind == reflect.String {
307                 nativeValue = value
308         } else if kind == reflect.Float64 {
309                 i, err := strconv.Atoi(value)
310                 if err != nil {
311                         return errors.New("Invalid value")
312                 }
313                 nativeValue = float64(i)
314         } else {
315                 return errors.New("Option has unsupported value type")
316         }
317
318         if err := optionIsValid(option, nativeValue); err != nil {
319                 return err
320         }
321
322         buf.Settings[option] = nativeValue
323
324         if option == "statusline" {
325                 view.ToggleStatusLine()
326                 if buf.Settings["syntax"].(bool) {
327                         view.matches = Match(view)
328                 }
329         }
330
331         if option == "filetype" {
332                 LoadSyntaxFiles()
333                 buf.UpdateRules()
334                 if buf.Settings["syntax"].(bool) {
335                         view.matches = Match(view)
336                 }
337         }
338
339         return nil
340 }
341
342 // SetOptionAndSettings sets the given option and saves the option setting to the settings config file
343 func SetOptionAndSettings(option, value string) {
344         filename := configDir + "/settings.json"
345
346         err := SetOption(option, value)
347
348         if err != nil {
349                 messenger.Error(err.Error())
350                 return
351         }
352
353         err = WriteSettings(filename)
354         if err != nil {
355                 messenger.Error("Error writing to settings.json: " + err.Error())
356                 return
357         }
358 }
359
360 func optionIsValid(option string, value interface{}) error {
361         if validator, ok := optionValidators[option]; ok {
362                 return validator(option, value)
363         }
364
365         return nil
366 }
367
368 // Option validators
369
370 func validatePositiveValue(option string, value interface{}) error {
371         tabsize, ok := value.(float64)
372
373         if !ok {
374                 return errors.New("Expected numeric type for " + option)
375         }
376
377         if tabsize < 1 {
378                 return errors.New(option + " must be greater than 0")
379         }
380
381         return nil
382 }
383
384 func validateNonNegativeValue(option string, value interface{}) error {
385         nativeValue, ok := value.(float64)
386
387         if !ok {
388                 return errors.New("Expected numeric type for " + option)
389         }
390
391         if nativeValue < 0 {
392                 return errors.New(option + " must be non-negative")
393         }
394
395         return nil
396 }
397
398 func validateColorscheme(option string, value interface{}) error {
399         colorscheme, ok := value.(string)
400
401         if !ok {
402                 return errors.New("Expected string type for colorscheme")
403         }
404
405         if !ColorschemeExists(colorscheme) {
406                 return errors.New(colorscheme + " is not a valid colorscheme")
407         }
408
409         return nil
410 }