forked from atom/atom
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathconfig.coffee
271 lines (237 loc) · 8.57 KB
/
config.coffee
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
_ = require 'underscore-plus'
fs = require 'fs-plus'
{Emitter} = require 'emissary'
CSON = require 'season'
path = require 'path'
async = require 'async'
pathWatcher = require 'pathwatcher'
# Public: Used to access all of Atom's configuration details.
#
# An instance of this class is always available as the `atom.config` global.
#
# ## Best practices
#
# * Create your own root keypath using your package's name.
# * Don't depend on (or write to) configuration keys outside of your keypath.
#
# ## Examples
#
# ```coffee
# atom.config.set('my-package.key', 'value')
# atom.config.observe 'my-package.key', ->
# console.log 'My configuration changed:', atom.config.get('my-package.key')
# ```
module.exports =
class Config
Emitter.includeInto(this)
# Created during initialization, available as `atom.config`
constructor: ({@configDirPath, @resourcePath}={}) ->
@defaultSettings = {}
@settings = {}
@configFileHasErrors = false
@configFilePath = fs.resolve(@configDirPath, 'config', ['json', 'cson'])
@configFilePath ?= path.join(@configDirPath, 'config.cson')
initializeConfigDirectory: (done) ->
return if fs.existsSync(@configDirPath)
fs.makeTreeSync(@configDirPath)
queue = async.queue ({sourcePath, destinationPath}, callback) ->
fs.copy(sourcePath, destinationPath, callback)
queue.drain = done
templateConfigDirPath = fs.resolve(@resourcePath, 'dot-atom')
onConfigDirFile = (sourcePath) =>
relativePath = sourcePath.substring(templateConfigDirPath.length + 1)
destinationPath = path.join(@configDirPath, relativePath)
queue.push({sourcePath, destinationPath})
fs.traverseTree(templateConfigDirPath, onConfigDirFile, (path) -> true)
load: ->
@initializeConfigDirectory()
@loadUserConfig()
@observeUserConfig()
loadUserConfig: ->
unless fs.existsSync(@configFilePath)
fs.makeTreeSync(path.dirname(@configFilePath))
CSON.writeFileSync(@configFilePath, {})
try
userConfig = CSON.readFileSync(@configFilePath)
_.extend(@settings, userConfig)
@configFileHasErrors = false
@emit 'updated'
catch e
@configFileHasErrors = true
console.error "Failed to load user config '#{@configFilePath}'", e.message
console.error e.stack
observeUserConfig: ->
@watchSubscription ?= pathWatcher.watch @configFilePath, (eventType) =>
@loadUserConfig() if eventType is 'change' and @watchSubscription?
unobserveUserConfig: ->
@watchSubscription?.close()
@watchSubscription = null
setDefaults: (keyPath, defaults) ->
keys = keyPath.split('.')
hash = @defaultSettings
for key in keys
hash[key] ?= {}
hash = hash[key]
_.extend hash, defaults
@emit 'updated'
# Extended: Get the {String} path to the config file being used.
getUserConfigPath: ->
@configFilePath
# Extended: Returns a new {Object} containing all of settings and defaults.
getSettings: ->
_.deepExtend(@settings, @defaultSettings)
# Essential: Retrieves the setting for the given key.
#
# * `keyPath` The {String} name of the key to retrieve.
#
# Returns the value from Atom's default settings, the user's configuration
# file, or `null` if the key doesn't exist in either.
get: (keyPath) ->
value = _.valueForKeyPath(@settings, keyPath)
defaultValue = _.valueForKeyPath(@defaultSettings, keyPath)
if value?
value = _.deepClone(value)
valueIsObject = _.isObject(value) and not _.isArray(value)
defaultValueIsObject = _.isObject(defaultValue) and not _.isArray(defaultValue)
if valueIsObject and defaultValueIsObject
_.defaults(value, defaultValue)
else
value = _.deepClone(defaultValue)
value
# Extended: Retrieves the setting for the given key as an integer.
#
# * `keyPath` The {String} name of the key to retrieve
#
# Returns the value from Atom's default settings, the user's configuration
# file, or `NaN` if the key doesn't exist in either.
getInt: (keyPath) ->
parseInt(@get(keyPath))
# Extended: Retrieves the setting for the given key as a positive integer.
#
# * `keyPath` The {String} name of the key to retrieve
# * `defaultValue` The integer {Number} to fall back to if the value isn't
# positive, defaults to 0.
#
# Returns the value from Atom's default settings, the user's configuration
# file, or `defaultValue` if the key value isn't greater than zero.
getPositiveInt: (keyPath, defaultValue=0) ->
Math.max(@getInt(keyPath), 0) or defaultValue
# Essential: Sets the value for a configuration setting.
#
# This value is stored in Atom's internal configuration file.
#
# * `keyPath` The {String} name of the key.
# * `value` The value of the setting.
#
# Returns the `value`.
set: (keyPath, value) ->
if @get(keyPath) isnt value
defaultValue = _.valueForKeyPath(@defaultSettings, keyPath)
value = undefined if _.isEqual(defaultValue, value)
_.setValueForKeyPath(@settings, keyPath, value)
@update()
value
# Extended: Toggle the value at the key path.
#
# The new value will be `true` if the value is currently falsy and will be
# `false` if the value is currently truthy.
#
# * `keyPath` The {String} name of the key.
#
# Returns the new value.
toggle: (keyPath) ->
@set(keyPath, !@get(keyPath))
# Extended: Restore the key path to its default value.
#
# * `keyPath` The {String} name of the key.
#
# Returns the new value.
restoreDefault: (keyPath) ->
@set(keyPath, _.valueForKeyPath(@defaultSettings, keyPath))
# Extended: Get the default value of the key path.
#
# * `keyPath` The {String} name of the key.
#
# Returns the default value.
getDefault: (keyPath) ->
defaultValue = _.valueForKeyPath(@defaultSettings, keyPath)
_.deepClone(defaultValue)
# Extended: Is the key path value its default value?
#
# * `keyPath` The {String} name of the key.
#
# Returns a {Boolean}, `true` if the current value is the default, `false`
# otherwise.
isDefault: (keyPath) ->
not _.valueForKeyPath(@settings, keyPath)?
# Extended: Push the value to the array at the key path.
#
# * `keyPath` The {String} key path.
# * `value` The value to push to the array.
#
# Returns the new array length {Number} of the setting.
pushAtKeyPath: (keyPath, value) ->
arrayValue = @get(keyPath) ? []
result = arrayValue.push(value)
@set(keyPath, arrayValue)
result
# Extended: Add the value to the beginning of the array at the key path.
#
# * `keyPath` The {String} key path.
# * `value` The value to shift onto the array.
#
# Returns the new array length {Number} of the setting.
unshiftAtKeyPath: (keyPath, value) ->
arrayValue = @get(keyPath) ? []
result = arrayValue.unshift(value)
@set(keyPath, arrayValue)
result
# Public: Remove the value from the array at the key path.
#
# * `keyPath` The {String} key path.
# * `value` The value to remove from the array.
#
# Returns the new array value of the setting.
removeAtKeyPath: (keyPath, value) ->
arrayValue = @get(keyPath) ? []
result = _.remove(arrayValue, value)
@set(keyPath, arrayValue)
result
# Essential: Add a listener for changes to a given key path.
#
# * `keyPath` The {String} name of the key to observe
# * `options` An optional {Object} containing the `callNow` key.
# * `callback` The {Function} to call when the value of the key changes.
# The first argument will be the new value of the key and the
# second argument will be an {Object} with a `previous` property
# that is the prior value of the key.
#
# Returns an {Object} with the following keys:
# * `off` A {Function} that unobserves the `keyPath` when called.
observe: (keyPath, options={}, callback) ->
if _.isFunction(options)
callback = options
options = {}
value = @get(keyPath)
previousValue = _.clone(value)
updateCallback = =>
value = @get(keyPath)
unless _.isEqual(value, previousValue)
previous = previousValue
previousValue = _.clone(value)
callback(value, {previous})
eventName = "updated.#{keyPath.replace(/\./, '-')}"
subscription = @on eventName, updateCallback
callback(value) if options.callNow ? true
subscription
# Unobserve all callbacks on a given key.
#
# * `keyPath` The {String} name of the key to unobserve.
unobserve: (keyPath) ->
@off("updated.#{keyPath.replace(/\./, '-')}")
update: ->
return if @configFileHasErrors
@save()
@emit 'updated'
save: ->
CSON.writeFileSync(@configFilePath, @settings)