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
|
#
# * Sync.set() and Sync.clear() propagate local changes to chrome.storage.sync.
# * Sync.handleStorageUpdate() listens for changes to chrome.storage.sync and propagates those
# changes to localStorage and into vimium's internal state.
# * Sync.fetchAsync() polls chrome.storage.sync at startup, similarly propagating
# changes to localStorage and into vimium's internal state.
#
# The effect is best-effort synchronization of vimium options/settings between
# chrome/vimium instances.
#
# NOTE:
# Values handled within this module are ALWAYS already JSON.stringifed, so
# they're always non-empty strings.
#
root = exports ? window
Sync =
storage: chrome.storage.sync
doNotSync: ["settingsVersion", "previousVersion"]
# This is called in main.coffee.
init: ->
chrome.storage.onChanged.addListener (changes, area) -> Sync.handleStorageUpdate changes, area
@fetchAsync()
# Asynchronous fetch from synced storage, called only at startup.
fetchAsync: ->
@storage.get null, (items) =>
unless chrome.runtime.lastError
for own key, value of items
Settings.storeAndPropagate key, value if @shouldSyncKey key
Settings.isLoaded = true
unless isPreloaded
listener() while listener = Settings.eventListeners.load?.pop()
# Asynchronous message from synced storage.
handleStorageUpdate: (changes, area) ->
for own key, change of changes
Settings.storeAndPropagate key, change?.newValue if @shouldSyncKey key
# Only called synchronously from within vimium, never on a callback.
# No need to propagate updates to the rest of vimium, that's already been done.
set: (key, value) ->
if @shouldSyncKey key
setting = {}; setting[key] = value
@storage.set setting
# Only called synchronously from within vimium, never on a callback.
clear: (key) ->
@storage.remove key if @shouldSyncKey key
# Should we synchronize this key?
shouldSyncKey: (key) -> key not in @doNotSync
#
# Used by all parts of Vimium to manipulate localStorage.
#
# Select the object to use as the cache for settings.
if Utils.isExtensionPage()
if Utils.isBackgroundPage()
settingsCache = localStorage
isPreloaded = true
else
settingsCache = extend {}, localStorage # Make a copy of the cached settings from localStorage
isPreloaded = true
else
settingsCache = {}
isPreloaded = false
root.Settings = Settings =
isLoaded: isPreloaded
cache: settingsCache
eventListeners: {}
init: ->
Sync.init()
if isPreloaded
listener() while listener = Settings.eventListeners.load?.pop()
get: (key) ->
console.log "WARNING: Settings have not loaded yet; using the default value for #{key}." unless @isLoaded
if (key of @cache) then JSON.parse(@cache[key]) else @defaults[key]
set: (key, value) ->
# Don't store the value if it is equal to the default, so we can change the defaults in the future
if (value == @defaults[key])
@clear(key)
else
jsonValue = JSON.stringify value
@cache[key] = jsonValue
Sync.set key, jsonValue
clear: (key) ->
if @has key
delete @cache[key]
Sync.clear key
has: (key) -> key of @cache
addEventListener: (eventName, callback) ->
(@eventListeners[eventName] ||= []).push callback
# For settings which require action when their value changes, add hooks to this object, to be called from
# options/options.coffee (when the options page is saved), and by Settings.storeAndPropagate (when an
# update propagates from chrome.storage.sync).
postUpdateHooks: {}
# postUpdateHooks convenience wrapper
performPostUpdateHook: (key, value) ->
@postUpdateHooks[key]? value
# Only ever called from asynchronous synced-storage callbacks (fetchAsync and handleStorageUpdate).
storeAndPropagate: (key, value) ->
return unless key of @defaults
return if value and key of @cache and @cache[key] is value
defaultValue = @defaults[key]
defaultValueJSON = JSON.stringify(defaultValue)
if value and value != defaultValueJSON
# Key/value has been changed to non-default value at remote instance.
@cache[key] = value
@performPostUpdateHook key, JSON.parse(value)
else
# Key has been reset to default value at remote instance.
if key of @cache
delete @cache[key]
@performPostUpdateHook key, defaultValue
# options.coffee and options.html only handle booleans and strings; therefore all defaults must be booleans
# or strings
defaults:
scrollStepSize: 60
smoothScroll: true
keyMappings: "# Insert your preferred key mappings here."
linkHintCharacters: "sadfjklewcmpgh"
linkHintNumbers: "0123456789"
filterLinkHints: false
hideHud: false
userDefinedLinkHintCss:
"""
div > .vimiumHintMarker {
/* linkhint boxes */
background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#FFF785),
color-stop(100%,#FFC542));
border: 1px solid #E3BE23;
}
div > .vimiumHintMarker span {
/* linkhint text */
color: black;
font-weight: bold;
font-size: 12px;
}
div > .vimiumHintMarker > .matchingCharacter {
}
"""
# Default exclusion rules.
exclusionRules:
[
# Disable Vimium on Gmail.
{ pattern: "http*://mail.google.com/*", passKeys: "" }
]
# NOTE: If a page contains both a single angle-bracket link and a double angle-bracket link, then in
# most cases the single bracket link will be "prev/next page" and the double bracket link will be
# "first/last page", so we put the single bracket first in the pattern string so that it gets searched
# for first.
# "\bprev\b,\bprevious\b,\bback\b,<,←,«,≪,<<"
previousPatterns: "prev,previous,back,<,\u2190,\xab,\u226a,<<"
# "\bnext\b,\bmore\b,>,→,»,≫,>>"
nextPatterns: "next,more,>,\u2192,\xbb,\u226b,>>"
# default/fall back search engine
searchUrl: "https://www.google.com/search?q="
# put in an example search engine
searchEngines: [
"w: http://www.wikipedia.org/w/index.php?title=Special:Search&search=%s Wikipedia"
""
"# More examples."
"#"
"# (Vimium has built-in completion for these.)"
"#"
"# g: http://www.google.com/search?q=%s Google"
"# l: http://www.google.com/search?q=%s&btnI I'm feeling lucky..."
"# y: http://www.youtube.com/results?search_query=%s Youtube"
"# b: https://www.bing.com/search?q=%s Bing"
"# d: https://duckduckgo.com/?q=%s DuckDuckGo"
"# az: http://www.amazon.com/s/?field-keywords=%s Amazon"
"#"
"# Another example (for Vimium does not have completion)."
"#"
"# m: https://www.google.com/maps/search/%s Google Maps"
].join "\n"
newTabUrl: "chrome://newtab"
grabBackFocus: false
settingsVersion: Utils.getCurrentVersion()
# Export Sync via Settings for tests.
root.Settings.Sync = Sync
# Perform migration from old settings versions, if this is the background page.
if Utils.isBackgroundPage()
# We use settingsVersion to coordinate any necessary schema changes.
if Utils.compareVersions("1.42", Settings.get("settingsVersion")) != -1
Settings.set("scrollStepSize", parseFloat Settings.get("scrollStepSize"))
Settings.set("settingsVersion", Utils.getCurrentVersion())
# Migration (after 1.49, 2015/2/1).
# Legacy setting: findModeRawQuery (a string).
# New setting: findModeRawQueryList (a list of strings), now stored in chrome.storage.local (not localStorage).
chrome.storage.local.get "findModeRawQueryList", (items) ->
unless chrome.runtime.lastError or items.findModeRawQueryList
rawQuery = Settings.get "findModeRawQuery"
chrome.storage.local.set findModeRawQueryList: (if rawQuery then [ rawQuery ] else [])
|