]> git.lizzy.rs Git - micro.git/blob - cmd/micro/settings.go
Start refactor
[micro.git] / cmd / micro / settings.go
1 package main
2
3 import (
4         "encoding/json"
5         "errors"
6         "io/ioutil"
7         "os"
8         "reflect"
9         "strings"
10
11         "github.com/flynn/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 // This is the raw parsed json
21 var parsedSettings map[string]interface{}
22
23 // Options with validators
24 var optionValidators = map[string]optionValidator{
25         "tabsize":      validatePositiveValue,
26         "scrollmargin": validateNonNegativeValue,
27         "scrollspeed":  validateNonNegativeValue,
28         "colorscheme":  validateColorscheme,
29         "colorcolumn":  validateNonNegativeValue,
30         "fileformat":   validateLineEnding,
31 }
32
33 func ReadSettings() error {
34         filename := configDir + "/settings.json"
35         if _, e := os.Stat(filename); e == nil {
36                 input, err := ioutil.ReadFile(filename)
37                 if err != nil {
38                         return errors.New("Error reading settings.json file: " + err.Error())
39                 }
40                 if !strings.HasPrefix(string(input), "null") {
41                         // Unmarshal the input into the parsed map
42                         err = json5.Unmarshal(input, &parsedSettings)
43                         if err != nil {
44                                 return errors.New("Error reading settings.json: " + err.Error())
45                         }
46                 }
47         }
48         return nil
49 }
50
51 // InitGlobalSettings initializes the options map and sets all options to their default values
52 // Must be called after ReadSettings
53 func InitGlobalSettings() {
54         globalSettings = DefaultGlobalSettings()
55
56         for k, v := range parsedSettings {
57                 if !strings.HasPrefix(reflect.TypeOf(v).String(), "map") {
58                         globalSettings[k] = v
59                 }
60         }
61 }
62
63 // InitLocalSettings scans the json in settings.json and sets the options locally based
64 // on whether the buffer matches the glob
65 // Must be called after ReadSettings
66 func InitLocalSettings(buf *Buffer) error {
67         var parseError error
68         for k, v := range parsedSettings {
69                 if strings.HasPrefix(reflect.TypeOf(v).String(), "map") {
70                         if strings.HasPrefix(k, "ft:") {
71                                 if buf.Settings["filetype"].(string) == k[3:] {
72                                         for k1, v1 := range v.(map[string]interface{}) {
73                                                 buf.Settings[k1] = v1
74                                         }
75                                 }
76                         } else {
77                                 g, err := glob.Compile(k)
78                                 if err != nil {
79                                         parseError = errors.New("Error with glob setting " + k + ": " + err.Error())
80                                         continue
81                                 }
82
83                                 if g.MatchString(buf.Path) {
84                                         for k1, v1 := range v.(map[string]interface{}) {
85                                                 buf.Settings[k1] = v1
86                                         }
87                                 }
88                         }
89                 }
90         }
91         return parseError
92 }
93
94 // WriteSettings writes the settings to the specified filename as JSON
95 func WriteSettings(filename string) error {
96         var err error
97         if _, e := os.Stat(configDir); e == nil {
98                 for k, v := range globalSettings {
99                         parsedSettings[k] = v
100                 }
101
102                 txt, _ := json.MarshalIndent(parsedSettings, "", "    ")
103                 err = ioutil.WriteFile(filename, append(txt, '\n'), 0644)
104         }
105         return err
106 }
107
108 // AddOption creates a new option. This is meant to be called by plugins to add options.
109 func AddOption(name string, value interface{}) error {
110         globalSettings[name] = value
111         err := WriteSettings(configDir + "/settings.json")
112         if err != nil {
113                 return errors.New("Error writing settings.json file: " + err.Error())
114         }
115         return nil
116 }
117
118 // GetGlobalOption returns the global value of the given option
119 func GetGlobalOption(name string) interface{} {
120         return globalSettings[name]
121 }
122
123 // GetLocalOption returns the local value of the given option
124 func GetLocalOption(name string, buf *Buffer) interface{} {
125         return buf.Settings[name]
126 }
127
128 // TODO: get option for current buffer
129 // GetOption returns the value of the given option
130 // If there is a local version of the option, it returns that
131 // otherwise it will return the global version
132 // func GetOption(name string) interface{} {
133 //      if GetLocalOption(name, CurView().Buf) != nil {
134 //              return GetLocalOption(name, CurView().Buf)
135 //      }
136 //      return GetGlobalOption(name)
137 // }
138
139 func DefaultCommonSettings() map[string]interface{} {
140         return map[string]interface{}{
141                 "autoindent":     true,
142                 "autosave":       false,
143                 "basename":       false,
144                 "colorcolumn":    float64(0),
145                 "cursorline":     true,
146                 "eofnewline":     false,
147                 "fastdirty":      true,
148                 "fileformat":     "unix",
149                 "hidehelp":       false,
150                 "ignorecase":     false,
151                 "indentchar":     " ",
152                 "keepautoindent": false,
153                 "matchbrace":     false,
154                 "matchbraceleft": false,
155                 "rmtrailingws":   false,
156                 "ruler":          true,
157                 "savecursor":     false,
158                 "saveundo":       false,
159                 "scrollbar":      false,
160                 "scrollmargin":   float64(3),
161                 "scrollspeed":    float64(2),
162                 "softwrap":       false,
163                 "smartpaste":     true,
164                 "splitbottom":    true,
165                 "splitright":     true,
166                 "statusline":     true,
167                 "syntax":         true,
168                 "tabmovement":    false,
169                 "tabsize":        float64(4),
170                 "tabstospaces":   false,
171                 "useprimary":     true,
172         }
173 }
174
175 // DefaultGlobalSettings returns the default global settings for micro
176 // Note that colorscheme is a global only option
177 func DefaultGlobalSettings() map[string]interface{} {
178         common := DefaultCommonSettings()
179         common["colorscheme"] = "default"
180         common["infobar"] = true
181         common["keymenu"] = false
182         common["mouse"] = true
183         common["pluginchannels"] = []string{"https://raw.githubusercontent.com/micro-editor/plugin-channel/master/channel.json"}
184         common["pluginrepos"] = []string{}
185         common["savehistory"] = true
186         common["sucmd"] = "sudo"
187         common["termtitle"] = false
188         return common
189 }
190
191 // DefaultLocalSettings returns the default local settings
192 // Note that filetype is a local only option
193 func DefaultLocalSettings() map[string]interface{} {
194         common := DefaultCommonSettings()
195         common["filetype"] = "Unknown"
196         return common
197 }
198
199 // TODO: everything else
200
201 // SetOption attempts to set the given option to the value
202 // By default it will set the option as global, but if the option
203 // is local only it will set the local version
204 // Use setlocal to force an option to be set locally
205 // func SetOption(option, value string) error {
206 //      if _, ok := globalSettings[option]; !ok {
207 //              if _, ok := CurView().Buf.Settings[option]; !ok {
208 //                      return errors.New("Invalid option")
209 //              }
210 //              SetLocalOption(option, value, CurView())
211 //              return nil
212 //      }
213 //
214 //      var nativeValue interface{}
215 //
216 //      kind := reflect.TypeOf(globalSettings[option]).Kind()
217 //      if kind == reflect.Bool {
218 //              b, err := ParseBool(value)
219 //              if err != nil {
220 //                      return errors.New("Invalid value")
221 //              }
222 //              nativeValue = b
223 //      } else if kind == reflect.String {
224 //              nativeValue = value
225 //      } else if kind == reflect.Float64 {
226 //              i, err := strconv.Atoi(value)
227 //              if err != nil {
228 //                      return errors.New("Invalid value")
229 //              }
230 //              nativeValue = float64(i)
231 //      } else {
232 //              return errors.New("Option has unsupported value type")
233 //      }
234 //
235 //      if err := optionIsValid(option, nativeValue); err != nil {
236 //              return err
237 //      }
238 //
239 //      globalSettings[option] = nativeValue
240 //
241 //      if option == "colorscheme" {
242 //              // LoadSyntaxFiles()
243 //              InitColorscheme()
244 //              for _, tab := range tabs {
245 //                      for _, view := range tab.Views {
246 //                              view.Buf.UpdateRules()
247 //                      }
248 //              }
249 //      }
250 //
251 //      if option == "infobar" || option == "keymenu" {
252 //              for _, tab := range tabs {
253 //                      tab.Resize()
254 //              }
255 //      }
256 //
257 //      if option == "mouse" {
258 //              if !nativeValue.(bool) {
259 //                      screen.DisableMouse()
260 //              } else {
261 //                      screen.EnableMouse()
262 //              }
263 //      }
264 //
265 //      if len(tabs) != 0 {
266 //              if _, ok := CurView().Buf.Settings[option]; ok {
267 //                      for _, tab := range tabs {
268 //                              for _, view := range tab.Views {
269 //                                      SetLocalOption(option, value, view)
270 //                              }
271 //                      }
272 //              }
273 //      }
274 //
275 //      return nil
276 // }
277 //
278 // // SetLocalOption sets the local version of this option
279 // func SetLocalOption(option, value string, view *View) error {
280 //      buf := view.Buf
281 //      if _, ok := buf.Settings[option]; !ok {
282 //              return errors.New("Invalid option")
283 //      }
284 //
285 //      var nativeValue interface{}
286 //
287 //      kind := reflect.TypeOf(buf.Settings[option]).Kind()
288 //      if kind == reflect.Bool {
289 //              b, err := ParseBool(value)
290 //              if err != nil {
291 //                      return errors.New("Invalid value")
292 //              }
293 //              nativeValue = b
294 //      } else if kind == reflect.String {
295 //              nativeValue = value
296 //      } else if kind == reflect.Float64 {
297 //              i, err := strconv.Atoi(value)
298 //              if err != nil {
299 //                      return errors.New("Invalid value")
300 //              }
301 //              nativeValue = float64(i)
302 //      } else {
303 //              return errors.New("Option has unsupported value type")
304 //      }
305 //
306 //      if err := optionIsValid(option, nativeValue); err != nil {
307 //              return err
308 //      }
309 //
310 //      if option == "fastdirty" {
311 //              // If it is being turned off, we have to hash every open buffer
312 //              var empty [md5.Size]byte
313 //              var wg sync.WaitGroup
314 //
315 //              for _, tab := range tabs {
316 //                      for _, v := range tab.Views {
317 //                              if !nativeValue.(bool) {
318 //                                      if v.Buf.origHash == empty {
319 //                                              wg.Add(1)
320 //
321 //                                              go func(b *Buffer) { // calculate md5 hash of the file
322 //                                                      defer wg.Done()
323 //
324 //                                                      if file, e := os.Open(b.AbsPath); e == nil {
325 //                                                              defer file.Close()
326 //
327 //                                                              h := md5.New()
328 //
329 //                                                              if _, e = io.Copy(h, file); e == nil {
330 //                                                                      h.Sum(b.origHash[:0])
331 //                                                              }
332 //                                                      }
333 //                                              }(v.Buf)
334 //                                      }
335 //                              } else {
336 //                                      v.Buf.IsModified = v.Buf.Modified()
337 //                              }
338 //                      }
339 //              }
340 //
341 //              wg.Wait()
342 //      }
343 //
344 //      buf.Settings[option] = nativeValue
345 //
346 //      if option == "statusline" {
347 //              view.ToggleStatusLine()
348 //      }
349 //
350 //      if option == "filetype" {
351 //              // LoadSyntaxFiles()
352 //              InitColorscheme()
353 //              buf.UpdateRules()
354 //      }
355 //
356 //      if option == "fileformat" {
357 //              buf.IsModified = true
358 //      }
359 //
360 //      if option == "syntax" {
361 //              if !nativeValue.(bool) {
362 //                      buf.ClearMatches()
363 //              } else {
364 //                      if buf.highlighter != nil {
365 //                              buf.highlighter.HighlightStates(buf)
366 //                      }
367 //              }
368 //      }
369 //
370 //      return nil
371 // }
372 //
373 // // SetOptionAndSettings sets the given option and saves the option setting to the settings config file
374 // func SetOptionAndSettings(option, value string) {
375 //      filename := configDir + "/settings.json"
376 //
377 //      err := SetOption(option, value)
378 //
379 //      if err != nil {
380 //              messenger.Error(err.Error())
381 //              return
382 //      }
383 //
384 //      err = WriteSettings(filename)
385 //      if err != nil {
386 //              messenger.Error("Error writing to settings.json: " + err.Error())
387 //              return
388 //      }
389 // }
390
391 func optionIsValid(option string, value interface{}) error {
392         if validator, ok := optionValidators[option]; ok {
393                 return validator(option, value)
394         }
395
396         return nil
397 }
398
399 // Option validators
400
401 func validatePositiveValue(option string, value interface{}) error {
402         tabsize, ok := value.(float64)
403
404         if !ok {
405                 return errors.New("Expected numeric type for " + option)
406         }
407
408         if tabsize < 1 {
409                 return errors.New(option + " must be greater than 0")
410         }
411
412         return nil
413 }
414
415 func validateNonNegativeValue(option string, value interface{}) error {
416         nativeValue, ok := value.(float64)
417
418         if !ok {
419                 return errors.New("Expected numeric type for " + option)
420         }
421
422         if nativeValue < 0 {
423                 return errors.New(option + " must be non-negative")
424         }
425
426         return nil
427 }
428
429 func validateColorscheme(option string, value interface{}) error {
430         colorscheme, ok := value.(string)
431
432         if !ok {
433                 return errors.New("Expected string type for colorscheme")
434         }
435
436         if !ColorschemeExists(colorscheme) {
437                 return errors.New(colorscheme + " is not a valid colorscheme")
438         }
439
440         return nil
441 }
442
443 func validateLineEnding(option string, value interface{}) error {
444         endingType, ok := value.(string)
445
446         if !ok {
447                 return errors.New("Expected string type for file format")
448         }
449
450         if endingType != "unix" && endingType != "dos" {
451                 return errors.New("File format must be either 'unix' or 'dos'")
452         }
453
454         return nil
455 }