| 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
 | 
# A "setting" is a stored key/value pair.  An "option" is setting which has a default value and whose value
# can be changed on the options page.
#
# Option values which have never been changed by the user are in Settings.defaults.
#
# Settings whose values have been changed are:
# 1. stored either in chrome.storage.sync or in chrome.storage.local (but never both), and
# 2. cached in Settings.cache; on extension pages, Settings.cache uses localStorage (so it persists).
#
# In all cases except Settings.defaults, values are stored as jsonified strings.
Settings =
  storage: chrome.storage.sync
  cache: {}
  isLoaded: false
  onLoadedCallbacks: []
  init: ->
    if Utils.isExtensionPage()
      # On extension pages, we use localStorage (or a copy of it) as the cache.
      @cache = if Utils.isBackgroundPage() then localStorage else extend {}, localStorage
      @onLoaded()
    chrome.storage.local.get null, (localItems) =>
      localItems = {} if chrome.runtime.lastError
      @storage.get null, (syncedItems) =>
        unless chrome.runtime.lastError
          @handleUpdateFromChromeStorage key, value for own key, value of extend localItems, syncedItems
        chrome.storage.onChanged.addListener (changes, area) =>
          @propagateChangesFromChromeStorage changes if area == "sync"
        @onLoaded()
  # Called after @cache has been initialized.  On extension pages, this will be called twice, but that does
  # not matter because it's idempotent.
  onLoaded: ->
    @isLoaded = true
    callback() while callback = @onLoadedCallbacks.pop()
  shouldSyncKey: (key) ->
    (key of @defaults) and key not in [ "settingsVersion", "previousVersion" ]
  propagateChangesFromChromeStorage: (changes) ->
    @handleUpdateFromChromeStorage key, change?.newValue for own key, change of changes
  handleUpdateFromChromeStorage: (key, value) ->
    # Note: value here is either null or a JSONified string.  Therefore, even falsy settings values (like
    # false, 0 or "") are truthy here.  Only null is falsy.
    if @shouldSyncKey key
      unless value and key of @cache and @cache[key] == value
        value ?= JSON.stringify @defaults[key]
        @set key, JSON.parse(value), false
  get: (key) ->
    console.log "WARNING: Settings have not loaded yet; using the default value for #{key}." unless @isLoaded
    if key of @cache and @cache[key]? then JSON.parse @cache[key] else @defaults[key]
  set: (key, value, shouldSetInSyncedStorage = true) ->
    @cache[key] = JSON.stringify value
    if @shouldSyncKey key
      if shouldSetInSyncedStorage
        setting = {}; setting[key] = @cache[key]
        @storage.set setting
      # Remove settings installed by the "copyNonDefaultsToChromeStorage-20150717" migration; see below.
      chrome.storage.local.remove key if Utils.isBackgroundPage()
    @performPostUpdateHook key, value
  clear: (key) ->
    @set key, @defaults[key]
  has: (key) -> key of @cache
  use: (key, callback) ->
    invokeCallback = => callback @get key
    if @isLoaded then invokeCallback() else @onLoadedCallbacks.push invokeCallback
  # For settings which require action when their value changes, add hooks to this object.
  postUpdateHooks: {}
  performPostUpdateHook: (key, value) -> @postUpdateHooks[key]? value
  # Default values for all settings.
  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: "https?://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 supports search completion Wikipedia, as
      # above, and 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
      # gm: https://www.google.com/maps?q=%s Google maps
      # 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
      """
    newTabUrl: "chrome://newtab"
    grabBackFocus: false
    regexFindMode: false
    settingsVersion: Utils.getCurrentVersion()
    helpDialog_showAdvancedCommands: false
Settings.init()
# 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 [])
  # Migration (after 1.51, 2015/6/17).
  # Copy options with non-default values (and which are not in synced storage) to chrome.storage.local;
  # thereby making these settings accessible within content scripts.
  do (migrationKey = "copyNonDefaultsToChromeStorage-20150717") ->
    unless localStorage[migrationKey]
      chrome.storage.sync.get null, (items) ->
        unless chrome.runtime.lastError
          updates = {}
          for own key of localStorage
            if Settings.shouldSyncKey(key) and not items[key]
              updates[key] = localStorage[key]
          chrome.storage.local.set updates, ->
            localStorage[migrationKey] = not chrome.runtime.lastError
root = exports ? window
root.Settings = Settings
 |