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