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