15 "github.com/flynn/json5"
16 "github.com/zyedidia/glob"
19 type optionValidator func(string, interface{}) error
21 // The options that the user can set
22 var globalSettings map[string]interface{}
24 var invalidSettings bool
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,
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{}
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") {
48 TermMessage("Error reading settings.json file: " + err.Error())
49 invalidSettings = true
53 err = json5.Unmarshal(input, &parsed)
55 TermMessage("Error reading settings.json:", err.Error())
56 invalidSettings = true
63 globalSettings = make(map[string]interface{})
64 for k, v := range defaults {
67 for k, v := range parsed {
68 if !strings.HasPrefix(reflect.TypeOf(v).String(), "map") {
73 if _, err := os.Stat(filename); os.IsNotExist(err) || writeSettings {
74 err := WriteSettings(filename)
76 TermMessage("Error writing settings.json file: " + err.Error())
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{}
87 filename := configDir + "/settings.json"
88 if _, e := os.Stat(filename); e == nil {
89 input, err := ioutil.ReadFile(filename)
91 TermMessage("Error reading settings.json file: " + err.Error())
92 invalidSettings = true
96 err = json5.Unmarshal(input, &parsed)
98 TermMessage("Error reading settings.json:", err.Error())
99 invalidSettings = true
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
112 g, err := glob.Compile(k)
114 TermMessage("Error with glob setting ", k, ": ", err)
118 if g.MatchString(buf.Path) {
119 for k1, v1 := range v.(map[string]interface{}) {
120 buf.Settings[k1] = v1
128 // WriteSettings writes the settings to the specified filename as JSON
129 func WriteSettings(filename string) error {
131 // Do not write the settings if there was an error when reading them
136 if _, e := os.Stat(configDir); e == nil {
137 parsed := make(map[string]interface{})
139 filename := configDir + "/settings.json"
140 for k, v := range globalSettings {
143 if _, e := os.Stat(filename); e == nil {
144 input, err := ioutil.ReadFile(filename)
145 if string(input) != "null" {
150 err = json5.Unmarshal(input, &parsed)
152 TermMessage("Error reading settings.json:", err.Error())
153 invalidSettings = true
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]
166 txt, _ := json.MarshalIndent(parsed, "", " ")
167 err = ioutil.WriteFile(filename, append(txt, '\n'), 0644)
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")
177 TermMessage("Error writing settings.json file: " + err.Error())
181 // GetGlobalOption returns the global value of the given option
182 func GetGlobalOption(name string) interface{} {
183 return globalSettings[name]
186 // GetLocalOption returns the local value of the given option
187 func GetLocalOption(name string, buf *Buffer) interface{} {
188 return buf.Settings[name]
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)
198 return GetGlobalOption(name)
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{}{
208 "colorcolumn": float64(0),
209 "colorscheme": "default",
213 "fileformat": "unix",
218 "keepautoindent": false,
221 "matchbraceleft": false,
223 "pluginchannels": []string{"https://raw.githubusercontent.com/micro-editor/plugin-channel/master/channel.json"},
224 "pluginrepos": []string{},
225 "rmtrailingws": false,
231 "scrollmargin": float64(3),
232 "scrollspeed": float64(2),
240 "tabmovement": false,
241 "tabsize": float64(4),
242 "tabstospaces": false,
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{}{
255 "colorcolumn": float64(0),
259 "fileformat": "unix",
260 "filetype": "Unknown",
264 "keepautoindent": false,
266 "matchbraceleft": false,
267 "rmtrailingws": false,
272 "scrollmargin": float64(3),
273 "scrollspeed": float64(2),
280 "tabmovement": false,
281 "tabsize": float64(4),
282 "tabstospaces": false,
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")
296 SetLocalOption(option, value, CurView())
300 var nativeValue interface{}
302 kind := reflect.TypeOf(globalSettings[option]).Kind()
303 if kind == reflect.Bool {
304 b, err := ParseBool(value)
306 return errors.New("Invalid value")
309 } else if kind == reflect.String {
311 } else if kind == reflect.Float64 {
312 i, err := strconv.Atoi(value)
314 return errors.New("Invalid value")
316 nativeValue = float64(i)
318 return errors.New("Option has unsupported value type")
321 if err := optionIsValid(option, nativeValue); err != nil {
325 globalSettings[option] = nativeValue
327 if option == "colorscheme" {
330 for _, tab := range tabs {
331 for _, view := range tab.Views {
332 view.Buf.UpdateRules()
337 if option == "infobar" || option == "keymenu" {
338 for _, tab := range tabs {
343 if option == "mouse" {
344 if !nativeValue.(bool) {
345 screen.DisableMouse()
352 if _, ok := CurView().Buf.Settings[option]; ok {
353 for _, tab := range tabs {
354 for _, view := range tab.Views {
355 SetLocalOption(option, value, view)
364 // SetLocalOption sets the local version of this option
365 func SetLocalOption(option, value string, view *View) error {
367 if _, ok := buf.Settings[option]; !ok {
368 return errors.New("Invalid option")
371 var nativeValue interface{}
373 kind := reflect.TypeOf(buf.Settings[option]).Kind()
374 if kind == reflect.Bool {
375 b, err := ParseBool(value)
377 return errors.New("Invalid value")
380 } else if kind == reflect.String {
382 } else if kind == reflect.Float64 {
383 i, err := strconv.Atoi(value)
385 return errors.New("Invalid value")
387 nativeValue = float64(i)
389 return errors.New("Option has unsupported value type")
392 if err := optionIsValid(option, nativeValue); err != nil {
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
401 for _, tab := range tabs {
402 for _, v := range tab.Views {
403 if !nativeValue.(bool) {
404 if v.Buf.origHash == empty {
407 go func(b *Buffer) { // calculate md5 hash of the file
410 if file, e := os.Open(b.AbsPath); e == nil {
415 if _, e = io.Copy(h, file); e == nil {
416 h.Sum(b.origHash[:0])
422 v.Buf.IsModified = v.Buf.Modified()
430 buf.Settings[option] = nativeValue
432 if option == "statusline" {
433 view.ToggleStatusLine()
436 if option == "filetype" {
442 if option == "fileformat" {
443 buf.IsModified = true
446 if option == "syntax" {
447 if !nativeValue.(bool) {
450 if buf.highlighter != nil {
451 buf.highlighter.HighlightStates(buf)
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"
463 err := SetOption(option, value)
466 messenger.Error(err.Error())
470 err = WriteSettings(filename)
472 messenger.Error("Error writing to settings.json: " + err.Error())
477 func optionIsValid(option string, value interface{}) error {
478 if validator, ok := optionValidators[option]; ok {
479 return validator(option, value)
487 func validatePositiveValue(option string, value interface{}) error {
488 tabsize, ok := value.(float64)
491 return errors.New("Expected numeric type for " + option)
495 return errors.New(option + " must be greater than 0")
501 func validateNonNegativeValue(option string, value interface{}) error {
502 nativeValue, ok := value.(float64)
505 return errors.New("Expected numeric type for " + option)
509 return errors.New(option + " must be non-negative")
515 func validateColorscheme(option string, value interface{}) error {
516 colorscheme, ok := value.(string)
519 return errors.New("Expected string type for colorscheme")
522 if !ColorschemeExists(colorscheme) {
523 return errors.New(colorscheme + " is not a valid colorscheme")
529 func validateLineEnding(option string, value interface{}) error {
530 endingType, ok := value.(string)
533 return errors.New("Expected string type for file format")
536 if endingType != "unix" && endingType != "dos" {
537 return errors.New("File format must be either 'unix' or 'dos'")