]> git.lizzy.rs Git - micro.git/blob - cmd/micro/settings.go
23ede68e062ce5e514eeaf1cec080a7aeb36f1cb
[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/zyedidia/glob"
12         "github.com/zyedidia/json5/encoding/json5"
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":  "default",
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                 "pluginchannels": []string{
196                         "https://raw.githubusercontent.com/micro-editor/plugin-channel/master/channel.json",
197                 },
198                 "pluginrepos": []string{},
199         }
200 }
201
202 // DefaultLocalSettings returns the default local settings
203 // Note that filetype is a local only option
204 func DefaultLocalSettings() map[string]interface{} {
205         return map[string]interface{}{
206                 "autoindent":   true,
207                 "autosave":     false,
208                 "colorcolumn":  float64(0),
209                 "cursorline":   true,
210                 "filetype":     "Unknown",
211                 "ignorecase":   false,
212                 "indentchar":   " ",
213                 "ruler":        true,
214                 "savecursor":   false,
215                 "saveundo":     false,
216                 "scrollspeed":  float64(2),
217                 "scrollmargin": float64(3),
218                 "statusline":   true,
219                 "syntax":       true,
220                 "tabsize":      float64(4),
221                 "tabstospaces": false,
222         }
223 }
224
225 // SetOption attempts to set the given option to the value
226 // By default it will set the option as global, but if the option
227 // is local only it will set the local version
228 // Use setlocal to force an option to be set locally
229 func SetOption(option, value string) error {
230         if _, ok := globalSettings[option]; !ok {
231                 if _, ok := CurView().Buf.Settings[option]; !ok {
232                         return errors.New("Invalid option")
233                 }
234                 SetLocalOption(option, value, CurView())
235                 return nil
236         }
237
238         var nativeValue interface{}
239
240         kind := reflect.TypeOf(globalSettings[option]).Kind()
241         if kind == reflect.Bool {
242                 b, err := ParseBool(value)
243                 if err != nil {
244                         return errors.New("Invalid value")
245                 }
246                 nativeValue = b
247         } else if kind == reflect.String {
248                 nativeValue = value
249         } else if kind == reflect.Float64 {
250                 i, err := strconv.Atoi(value)
251                 if err != nil {
252                         return errors.New("Invalid value")
253                 }
254                 nativeValue = float64(i)
255         } else {
256                 return errors.New("Option has unsupported value type")
257         }
258
259         if err := optionIsValid(option, nativeValue); err != nil {
260                 return err
261         }
262
263         globalSettings[option] = nativeValue
264
265         if option == "colorscheme" {
266                 LoadSyntaxFiles()
267                 for _, tab := range tabs {
268                         for _, view := range tab.views {
269                                 view.Buf.UpdateRules()
270                                 if view.Buf.Settings["syntax"].(bool) {
271                                         view.matches = Match(view)
272                                 }
273                         }
274                 }
275         }
276
277         if option == "infobar" {
278                 for _, tab := range tabs {
279                         tab.Resize()
280                 }
281         }
282
283         if _, ok := CurView().Buf.Settings[option]; ok {
284                 for _, tab := range tabs {
285                         for _, view := range tab.views {
286                                 SetLocalOption(option, value, view)
287                         }
288                 }
289         }
290
291         return nil
292 }
293
294 // SetLocalOption sets the local version of this option
295 func SetLocalOption(option, value string, view *View) error {
296         buf := view.Buf
297         if _, ok := buf.Settings[option]; !ok {
298                 return errors.New("Invalid option")
299         }
300
301         var nativeValue interface{}
302
303         kind := reflect.TypeOf(buf.Settings[option]).Kind()
304         if kind == reflect.Bool {
305                 b, err := ParseBool(value)
306                 if err != nil {
307                         return errors.New("Invalid value")
308                 }
309                 nativeValue = b
310         } else if kind == reflect.String {
311                 nativeValue = value
312         } else if kind == reflect.Float64 {
313                 i, err := strconv.Atoi(value)
314                 if err != nil {
315                         return errors.New("Invalid value")
316                 }
317                 nativeValue = float64(i)
318         } else {
319                 return errors.New("Option has unsupported value type")
320         }
321
322         if err := optionIsValid(option, nativeValue); err != nil {
323                 return err
324         }
325
326         buf.Settings[option] = nativeValue
327
328         if option == "statusline" {
329                 view.ToggleStatusLine()
330                 if buf.Settings["syntax"].(bool) {
331                         view.matches = Match(view)
332                 }
333         }
334
335         if option == "filetype" {
336                 LoadSyntaxFiles()
337                 buf.UpdateRules()
338                 if buf.Settings["syntax"].(bool) {
339                         view.matches = Match(view)
340                 }
341         }
342
343         return nil
344 }
345
346 // SetOptionAndSettings sets the given option and saves the option setting to the settings config file
347 func SetOptionAndSettings(option, value string) {
348         filename := configDir + "/settings.json"
349
350         err := SetOption(option, value)
351
352         if err != nil {
353                 messenger.Error(err.Error())
354                 return
355         }
356
357         err = WriteSettings(filename)
358         if err != nil {
359                 messenger.Error("Error writing to settings.json: " + err.Error())
360                 return
361         }
362 }
363
364 func optionIsValid(option string, value interface{}) error {
365         if validator, ok := optionValidators[option]; ok {
366                 return validator(option, value)
367         }
368
369         return nil
370 }
371
372 // Option validators
373
374 func validatePositiveValue(option string, value interface{}) error {
375         tabsize, ok := value.(float64)
376
377         if !ok {
378                 return errors.New("Expected numeric type for " + option)
379         }
380
381         if tabsize < 1 {
382                 return errors.New(option + " must be greater than 0")
383         }
384
385         return nil
386 }
387
388 func validateNonNegativeValue(option string, value interface{}) error {
389         nativeValue, ok := value.(float64)
390
391         if !ok {
392                 return errors.New("Expected numeric type for " + option)
393         }
394
395         if nativeValue < 0 {
396                 return errors.New(option + " must be non-negative")
397         }
398
399         return nil
400 }
401
402 func validateColorscheme(option string, value interface{}) error {
403         colorscheme, ok := value.(string)
404
405         if !ok {
406                 return errors.New("Expected string type for colorscheme")
407         }
408
409         if !ColorschemeExists(colorscheme) {
410                 return errors.New(colorscheme + " is not a valid colorscheme")
411         }
412
413         return nil
414 }