diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7a3dd1a..fcbe2ef 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,10 @@
Changelog
-------------
+### 2.0.0 (14/7/2018)
+* vimari now exists as a Safari App Extension, making it compatible with Safari
+ version 12
+
### 1.13.0 (4/12/2017)
* New fresh icon
* Removed shift as default modifier key
diff --git a/README.md b/README.md
index ebadbc6..c114003 100644
--- a/README.md
+++ b/README.md
@@ -1,38 +1,3 @@
-:rotating_light: **Attention!** We are currently blocked from releasing new
-version of this extension, and we have a proposal solving this problem
-:rotating_light:
-
-**Funding Progress**
-
-
-
-**For full context, read [the full proposal](/docs/crowdfunding.md).**
-
-## Proposal
-
-I propose we use [Ko-fi](https://ko-fi.com) as a crowdfunding platform in order
-to collect $99 for a ADP membership to be registered in my name (Simon
-Egersand).
-
-## If we reach our goal
-
-If we reach the goal and successfully raise all the money we need, what
-happens? First of all, I will register my account for a ADP membership and then
-make all the changes required to publish the extension to the Safari Extension
-Gallery (some changes are required, see
-[#100](/~https://github.com/guyht/vimari/issues/100)). But what happens after
-that?
-
-I commit to being active in maintaining this extension for the next year. That
-means I will work on fixing bugs, developing new features, increasing test
-coverage and generally improving the code. And I welcome everyone to join me!
-
----
-
-**NOTE: If you have a pre 1.2 version of Vimari, you need to manually
-update to the latest version as there is a bug in the auto-update code.
-Versions post 1.2 will automatically update to the lastest version.**
-
Vimari - Keyboard Shortcuts extension for Safari
================================================
@@ -44,6 +9,7 @@ Vimari attempts to provide a lightweight port of vimium to Safari,
taking the best components of vimium and adapting them to Safari.
### Releases
+ - [Version 2.0.0](docs/safari_12.md) (Safari v12)
- [Version 1.12](/~https://github.com/guyht/vimari/releases/tag/v1.12)
- [Version 1.11](/~https://github.com/guyht/vimari/releases/tag/v1.11)
- [Version 1.10](/~https://github.com/guyht/vimari/releases/tag/v1.10)
diff --git a/app_extension/vimari/.gitignore b/app_extension/vimari/.gitignore
new file mode 100644
index 0000000..7b86622
--- /dev/null
+++ b/app_extension/vimari/.gitignore
@@ -0,0 +1,79 @@
+
+# Created by https://www.gitignore.io/api/swift
+
+### Swift ###
+# Xcode
+#
+# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
+
+## Build generated
+build/
+DerivedData/
+
+## Various settings
+*.pbxuser
+!default.pbxuser
+*.mode1v3
+!default.mode1v3
+*.mode2v3
+!default.mode2v3
+*.perspectivev3
+!default.perspectivev3
+xcuserdata/
+
+## Other
+*.moved-aside
+*.xccheckout
+*.xcscmblueprint
+
+## Obj-C/Swift specific
+*.hmap
+*.ipa
+*.dSYM.zip
+*.dSYM
+
+## Playgrounds
+timeline.xctimeline
+playground.xcworkspace
+
+# Swift Package Manager
+#
+# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
+# Packages/
+# Package.pins
+# Package.resolved
+.build/
+
+# CocoaPods
+#
+# We recommend against adding the Pods directory to your .gitignore. However
+# you should judge for yourself, the pros and cons are mentioned at:
+# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
+#
+# Pods/
+#
+# Add this line if you want to avoid checking in source code from the Xcode workspace
+# *.xcworkspace
+
+# Carthage
+#
+# Add this line if you want to avoid checking in source code from Carthage dependencies.
+# Carthage/Checkouts
+
+Carthage/Build
+
+# fastlane
+#
+# It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the
+# screenshots whenever they are needed.
+# For more information about the recommended setup visit:
+# https://docs.fastlane.tools/best-practices/source-control/#source-control
+
+fastlane/report.xml
+fastlane/Preview.html
+fastlane/screenshots/**/*.png
+fastlane/test_output
+
+
+# End of https://www.gitignore.io/api/swift
+
diff --git a/app_extension/vimari/extension/Base.lproj/SafariExtensionViewController.xib b/app_extension/vimari/extension/Base.lproj/SafariExtensionViewController.xib
new file mode 100644
index 0000000..6527f19
--- /dev/null
+++ b/app_extension/vimari/extension/Base.lproj/SafariExtensionViewController.xib
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app_extension/vimari/extension/Info.plist b/app_extension/vimari/extension/Info.plist
new file mode 100644
index 0000000..81d49d8
--- /dev/null
+++ b/app_extension/vimari/extension/Info.plist
@@ -0,0 +1,76 @@
+
+
+
+
+ CFBundleDevelopmentRegion
+ $(DEVELOPMENT_LANGUAGE)
+ CFBundleDisplayName
+ vimari
+ CFBundleExecutable
+ $(EXECUTABLE_NAME)
+ CFBundleIdentifier
+ $(PRODUCT_BUNDLE_IDENTIFIER)
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleName
+ $(PRODUCT_NAME)
+ CFBundlePackageType
+ XPC!
+ CFBundleShortVersionString
+ 2.0.0
+ CFBundleVersion
+ 2
+ LSMinimumSystemVersion
+ $(MACOSX_DEPLOYMENT_TARGET)
+ NSExtension
+
+ SFSafariStyleSheet
+
+
+ Style Sheet
+ injected.css
+
+
+ NSExtensionPointIdentifier
+ com.apple.Safari.extension
+ NSExtensionPrincipalClass
+ $(PRODUCT_MODULE_NAME).SafariExtensionHandler
+ SFSafariContentScript
+
+
+ Script
+ settings.js
+
+
+ Script
+ keyboard-utils.js
+
+
+ Script
+ vimium-scripts.js
+
+
+ Script
+ link-hints.js
+
+
+ Script
+ mousetrap.js
+
+
+ Script
+ injected.js
+
+
+ SFSafariWebsiteAccess
+
+ Level
+ All
+
+
+ NSHumanReadableCopyright
+ MIT license
+ NSHumanReadableDescription
+ Safari port of vimium
+
+
diff --git a/app_extension/vimari/extension/SafariExtensionHandler.swift b/app_extension/vimari/extension/SafariExtensionHandler.swift
new file mode 100644
index 0000000..3e126dd
--- /dev/null
+++ b/app_extension/vimari/extension/SafariExtensionHandler.swift
@@ -0,0 +1,34 @@
+//
+// SafariExtensionHandler.swift
+// extension
+//
+// Created by Simon on 2018-07-13.
+// Copyright © 2018 vimari. All rights reserved.
+//
+
+import SafariServices
+
+class SafariExtensionHandler: SFSafariExtensionHandler {
+
+ override func messageReceived(withName messageName: String, from page: SFSafariPage, userInfo: [String : Any]?) {
+ // This method will be called when a content script provided by your extension calls safari.extension.dispatchMessage("message").
+ page.getPropertiesWithCompletionHandler { properties in
+ NSLog("The extension received a message (\(messageName)) from a script injected into (\(String(describing: properties?.url))) with userInfo (\(userInfo ?? [:]))")
+ }
+ }
+
+ override func toolbarItemClicked(in window: SFSafariWindow) {
+ // This method will be called when your toolbar item is clicked.
+ NSLog("The extension's toolbar item was clicked")
+ }
+
+ override func validateToolbarItem(in window: SFSafariWindow, validationHandler: @escaping ((Bool, String) -> Void)) {
+ // This is called when Safari's state changed in some way that would require the extension's toolbar item to be validated again.
+ validationHandler(true, "")
+ }
+
+ override func popoverViewController() -> SFSafariExtensionViewController {
+ return SafariExtensionViewController.shared
+ }
+
+}
diff --git a/app_extension/vimari/extension/SafariExtensionViewController.swift b/app_extension/vimari/extension/SafariExtensionViewController.swift
new file mode 100644
index 0000000..6ff9f9a
--- /dev/null
+++ b/app_extension/vimari/extension/SafariExtensionViewController.swift
@@ -0,0 +1,15 @@
+//
+// SafariExtensionViewController.swift
+// extension
+//
+// Created by Simon on 2018-07-13.
+// Copyright © 2018 vimari. All rights reserved.
+//
+
+import SafariServices
+
+class SafariExtensionViewController: SFSafariExtensionViewController {
+
+ static let shared = SafariExtensionViewController()
+
+}
diff --git a/app_extension/vimari/extension/css/injected.css b/app_extension/vimari/extension/css/injected.css
new file mode 100644
index 0000000..fbfe2f1
--- /dev/null
+++ b/app_extension/vimari/extension/css/injected.css
@@ -0,0 +1,114 @@
+.vimiumReset {
+ background: none;
+ border: none;
+ bottom: auto;
+ box-shadow: none;
+ color: black;
+ cursor: auto;
+ display: inline;
+ float: none;
+ font-family : "Helvetica Neue", "Helvetica", "Arial", sans-serif;
+ font-size: inherit;
+ font-style: normal;
+ font-variant: normal;
+ font-weight: normal;
+ height: auto;
+ left: auto;
+ letter-spacing: 0;
+ line-height: 100%;
+ margin: 0;
+ max-height: none;
+ max-width: none;
+ min-height: 0;
+ min-width: 0;
+ opacity: 1;
+ padding: 0;
+ position: static;
+ right: auto;
+ text-align: left;
+ text-decoration: none;
+ text-indent: 0;
+ text-shadow: none;
+ text-transform: none;
+ top: auto;
+ vertical-align: baseline;
+ white-space: normal;
+ width: auto;
+ z-index: 2147483647; /* Maximum value in Safari */
+}
+
+div.internalVimiumHintMarker {
+ position: absolute !important;
+ display: block !important;
+ top: -1px;
+ left: -1px;
+ white-space: nowrap !important;
+ overflow: hidden !important;
+ font-size: 11px !important;
+ padding: 2px 3px !important;
+ background-color: #feda31 !important;
+ border: 0 !important;
+ border-radius: 2px !important;
+ box-shadow: inset 0 -2px 0 #b39922 !important;
+}
+
+div.internalVimiumHintMarker span {
+ color: #4a400e;
+ font-family: Helvetica, Arial, sans-serif;
+ font-weight: bold;
+}
+
+div.internalVimiumHintMarker > .matchingCharacter {
+ color: #dcbc2a;
+}
+
+.vimiumHUD, .vimiumHUD * {
+ line-height: 100%;
+ font-size: 11px;
+ font-weight: normal;
+}
+
+.vimiumHUD {
+ position: fixed;
+ bottom: 0px;
+ left: 40px;
+ color: black;
+ max-width: 400px;
+ min-width: 150px;
+ text-align: center;
+ background-color: #ebebeb;
+ padding: 3px 3px 5px 3px;
+ border: 1px solid #b3b3b3;
+ border-bottom: none;
+ border-radius: 4px 4px 0 0;
+ font-family: Lucida Grande, Arial, Sans;
+ /* One less than vimium's hint markers, so link hints can be shown e.g. for the panel's close button. */
+ z-index: 99999998;
+ text-shadow: 0px 1px 2px #FFF;
+ line-height: 1.0;
+ opacity: 0;
+}
+
+.vimiumHUD a, .vimiumHUD a:hover {
+ background: transparent;
+ color: blue;
+ text-decoration: underline;
+}
+
+.vimiumHUD a.close-button {
+ float:right;
+ font-family:courier new;
+ font-weight:bold;
+ color:#9C9A9A;
+ text-decoration:none;
+ padding-left:10px;
+ margin-top:-1px;
+ font-size:14px;
+}
+
+.vimiumHUD a.close-button:hover {
+ color:#333333;
+ cursor:default;
+ -webkit-user-select:none;
+}
+
diff --git a/app_extension/vimari/extension/extension.entitlements b/app_extension/vimari/extension/extension.entitlements
new file mode 100644
index 0000000..f2ef3ae
--- /dev/null
+++ b/app_extension/vimari/extension/extension.entitlements
@@ -0,0 +1,10 @@
+
+
+
+
+ com.apple.security.app-sandbox
+
+ com.apple.security.files.user-selected.read-only
+
+
+
diff --git a/app_extension/vimari/extension/js/global.js b/app_extension/vimari/extension/js/global.js
new file mode 100644
index 0000000..695c50f
--- /dev/null
+++ b/app_extension/vimari/extension/js/global.js
@@ -0,0 +1,155 @@
+// Function to handle messages... all messages are sent to this function
+function handleMessage(msg) {
+ // Attempt to call a function with the same name as the message name
+ switch (msg.name) {
+ case 'getSettings' :
+ getSettings(msg);
+ break;
+ case 'openTab' :
+ openTab();
+ break;
+ case 'closeTab':
+ closeTab(msg.message);
+ break;
+ case 'changeTab' :
+ changeTab(msg.message);
+ break;
+ }
+}
+
+// Dispatch a message to a tab's page or reader view
+function dispatchMessage(target, name, message) {
+ if (target) {
+ // Do some checks on the target to make sure we aren't trying to send a
+ // message to inaccessible tabs (e.g. Top Sites)
+ if (target.page && typeof target.page.dispatchMessage === "function") {
+ target.page.dispatchMessage(name, message);
+ } else if (typeof target.dispatchMessage === "function") {
+ target.dispatchMessage(name, message);
+ }
+ }
+}
+
+// Pass the settings on to the injected script
+function getSettings(event) {
+ var settings = {
+ 'linkHintCharacters': safari.extension.settings.linkHintCharacters,
+ 'hintToggle': safari.extension.settings.hintToggle,
+ 'newTabHintToggle': safari.extension.settings.newTabHintToggle,
+ 'tabForward': safari.extension.settings.tabForward,
+ 'tabBack': safari.extension.settings.tabBack,
+ 'scrollDown': safari.extension.settings.scrollDown,
+ 'scrollUp': safari.extension.settings.scrollUp,
+ 'scrollLeft': safari.extension.settings.scrollLeft,
+ 'scrollRight': safari.extension.settings.scrollRight,
+ 'goBack': safari.extension.settings.goBack,
+ 'goForward': safari.extension.settings.goForward,
+ 'reload': safari.extension.settings.reload,
+ 'scrollDownHalfPage': safari.extension.settings.scrollDownHalfPage,
+ 'scrollUpHalfPage': safari.extension.settings.scrollUpHalfPage,
+ 'goToPageBottom': safari.extension.settings.goToPageBottom,
+ 'goToPageTop': safari.extension.settings.goToPageTop,
+ 'closeTab': safari.extension.settings.closeTab,
+ 'closeTabReverse': safari.extension.settings.closeTabReverse,
+ 'openTab': safari.extension.settings.openTab,
+ 'modifier': safari.extension.settings.modifier,
+ 'scrollSize': safari.extension.settings.scrollSize,
+ 'excludedUrls': safari.extension.settings.excludedUrls,
+ 'detectByCursorStyle': safari.extension.settings.detectByCursorStyle
+ };
+
+ dispatchMessage(event.target, 'setSettings', settings);
+}
+
+/*
+ * Changes to the next avail tab
+ *
+ * dir - 1 forwards, 0 backwards
+ */
+function changeTab(dir) {
+ var tabs = safari.application.activeBrowserWindow.tabs,
+ i;
+
+ for (i = 0; i < tabs.length; i++) {
+ if (tabs[i] === safari.application.activeBrowserWindow.activeTab) {
+ if (dir === 1) {
+ if ((i + 1) === tabs.length) {
+ tabs[0].activate();
+ } else {
+ tabs[i + 1].activate();
+ }
+ } else {
+ if (i === 0) {
+ tabs[tabs.length - 1].activate();
+ } else {
+ tabs[i - 1].activate();
+ }
+ }
+ return;
+ }
+ }
+}
+
+/*
+ * Closes to current tab
+ *
+ * dir - 1 forwards, 0 backwards
+ */
+function closeTab(dir) {
+ var tab = safari.application.activeBrowserWindow.activeTab;
+ changeTab(dir);
+ tab.close();
+}
+
+/*
+ * Opens a new tab
+ */
+function openTab() {
+ var win = safari.application.activeBrowserWindow;
+ win.openTab();
+}
+
+/*
+ * Get the active tab
+ *
+ */
+function getActiveTab() {
+ var tabs = safari.application.activeBrowserWindow.tabs,
+ i;
+
+ for (i = 0; i < tabs.length; i++) {
+ if (tabs[i] === safari.application.activeBrowserWindow.activeTab) {
+ return i;
+ }
+ }
+}
+
+/*
+ * Disable extension on non active tabs,
+ * enable on active tab
+ *
+ * Need to do it in 2 seperate loops to make sure all tabs are disabled first
+ */
+function activateTab() {
+ var tabs = safari.application.activeBrowserWindow.tabs,
+ i;
+
+ for (i = 0; i < tabs.length; i++) {
+ dispatchMessage(safari.application.activeBrowserWindow.tabs[i], 'setActive', false);
+ }
+
+ for (i = 0; i < tabs.length; i++) {
+ if (tabs[i] === safari.application.activeBrowserWindow.activeTab) {
+ dispatchMessage(safari.application.activeBrowserWindow.tabs[i], 'setActive', true);
+ }
+ }
+
+}
+
+safari.application.addEventListener('message', handleMessage, false);
+
+// Need to detect if a new tab becomes active and if so, reload the extension
+safari.application.addEventListener('activate', function (event) {
+ activateTab();
+ getSettings(event);
+}, true);
diff --git a/app_extension/vimari/extension/js/injected.js b/app_extension/vimari/extension/js/injected.js
new file mode 100644
index 0000000..9ec06e4
--- /dev/null
+++ b/app_extension/vimari/extension/js/injected.js
@@ -0,0 +1,286 @@
+/*
+ * Vimari injected script.
+ *
+ * This script is called before the requested page is loaded. This allows us
+ * to intercept events before they are passed to the requested pages code and
+ * therefore we can stop certain pages (google) stealing the focus.
+ */
+
+
+/*
+ * Global vars
+ *
+ * topWindow - true if top window, false if iframe
+ * settings - stores user settings
+ * currentZoomLevel - required for vimium scripts to run correctly
+ * linkHintCss - required from vimium scripts
+ * extensionActive - is the extension currently enabled (should only be true when tab is active)
+ * shiftKeyToggle - is shift key currently toggled
+ */
+
+var topWindow = (window.top === window),
+ settings = {},
+ currentZoomLevel = 100,
+ linkHintCss = {},
+ extensionActive = true,
+ insertMode = false,
+ shiftKeyToggle = false,
+ hudDuration = 5000;
+
+var actionMap = {
+ 'hintToggle' : function() {
+ HUD.showForDuration('Open link in current tab', hudDuration);
+ activateLinkHintsMode(false, false); },
+
+ 'newTabHintToggle' : function() {
+ HUD.showForDuration('Open link in new tab', hudDuration);
+ activateLinkHintsMode(true, false); },
+
+ 'tabForward': function() {
+ safari.self.tab.dispatchMessage('changeTab', 1); },
+
+ 'tabBack': function() {
+ safari.self.tab.dispatchMessage('changeTab', 0); },
+
+ 'scrollDown':
+ function() { window.scrollBy(0, settings.scrollSize); },
+
+ 'scrollUp':
+ function() { window.scrollBy(0, -settings.scrollSize); },
+
+ 'scrollLeft':
+ function() { window.scrollBy(-settings.scrollSize, 0); },
+
+ 'scrollRight':
+ function() { window.scrollBy(settings.scrollSize, 0); },
+
+ 'goBack':
+ function() { window.history.back(); },
+
+ 'goForward':
+ function() { window.history.forward(); },
+
+ 'reload':
+ function() { window.location.reload(); },
+
+ 'openTab':
+ function() { safari.self.tab.dispatchMessage('openTab'); },
+
+ 'closeTab':
+ function() { safari.self.tab.dispatchMessage('closeTab', 0); },
+
+ 'closeTabReverse':
+ function() { safari.self.tab.dispatchMessage('closeTab', 1); },
+
+ 'scrollDownHalfPage':
+ function() { window.scrollBy(0, window.innerHeight / 2); },
+
+ 'scrollUpHalfPage':
+ function() { window.scrollBy(0, window.innerHeight / -2); },
+
+ 'goToPageBottom':
+ function() { window.scrollBy(0, document.body.scrollHeight); },
+
+ 'goToPageTop':
+ function() { window.scrollBy(0, -document.body.scrollHeight); }
+};
+
+// Meant to be overridden, but still has to be copy/pasted from the original...
+Mousetrap.stopCallback = function(e, element, combo) {
+ // Escape key is special, no need to stop. Vimari-specific.
+ if (combo === 'esc' || combo === 'ctrl+[') { return false; }
+
+ // Preserve the behavior of allowing ex. ctrl-j in an input
+ if (settings.modifier) { return false; }
+
+ // if the element has the class "mousetrap" then no need to stop
+ if ((' ' + element.className + ' ').indexOf(' mousetrap ') > -1) {
+ return false;
+ }
+
+ var tagName = element.tagName;
+ var contentIsEditable = (element.contentEditable && element.contentEditable === 'true');
+
+ // stop for input, select, and textarea
+ return tagName === 'INPUT' || tagName === 'SELECT' || tagName === 'TEXTAREA' || contentIsEditable;
+};
+
+// Set up key codes to event handlers
+function bindKeyCodesToActions() {
+ // Only add if topWindow... not iframe
+ if (topWindow && !isExcludedUrl(settings.excludedUrls, document.URL)) {
+ Mousetrap.reset();
+ Mousetrap.bind('esc', enterNormalMode);
+ Mousetrap.bind('ctrl+[', enterNormalMode);
+ Mousetrap.bind('i', enterInsertMode);
+ for (var actionName in actionMap) {
+ if (actionMap.hasOwnProperty(actionName)) {
+ var keyCode = getKeyCode(actionName);
+ Mousetrap.bind(keyCode, executeAction(actionName), 'keydown');
+ }
+ }
+ }
+}
+
+function enterNormalMode() {
+ // Clear input focus
+ document.activeElement.blur();
+
+ // Clear link hints (if any)
+ deactivateLinkHintsMode();
+
+ // Re-enable if in insert mode
+ insertMode = false;
+ Mousetrap.bind('i', enterInsertMode);
+}
+
+// Calling it 'insert mode', but it's really just a user-triggered
+// off switch for the actions.
+function enterInsertMode() {
+ insertMode = true;
+ Mousetrap.unbind('i');
+}
+
+function executeAction(actionName) {
+ return function() {
+ // don't do anything if we're not supposed to
+ if (linkHintsModeActivated || !extensionActive || insertMode)
+ return;
+
+ //Call the action function
+ actionMap[actionName]();
+
+ // Tell mousetrap to stop propagation
+ return false;
+ }
+}
+
+function unbindKeyCodes() {
+ Mousetrap.reset();
+}
+
+// Adds an optional modifier to the configured key code for the action
+function getKeyCode(actionName) {
+ var keyCode = '';
+ if(settings.modifier) {
+ keyCode += settings.modifier + '+';
+ }
+ return keyCode + settings[actionName];
+}
+
+
+/*
+ * Adds the given CSS to the page.
+ * This function is required by vimium but depracated for vimari as the
+ * css is pre loaded into the page.
+ */
+function addCssToPage(css) {
+ return;
+}
+
+
+/*
+ * Input or text elements are considered focusable and able to receive their own keyboard events,
+ * and will enter enter mode if focused. Also note that the "contentEditable" attribute can be set on
+ * any element which makes it a rich text editor, like the notes on jjot.com.
+ * Note: we used to discriminate for text-only inputs, but this is not accurate since all input fields
+ * can be controlled via the keyboard, particularly SELECT combo boxes.
+ */
+function isEditable(target) {
+ if (target.getAttribute("contentEditable") === "true")
+ return true;
+ var focusableInputs = ["input", "textarea", "select", "button"];
+ return focusableInputs.indexOf(target.tagName.toLowerCase()) >= 0;
+}
+
+
+/*
+ * Embedded elements like Flash and quicktime players can obtain focus but cannot be programmatically
+ * unfocused.
+ */
+function isEmbed(element) { return ["EMBED", "OBJECT"].indexOf(element.tagName) > 0; }
+
+
+// ==========================
+// Message handling functions
+// ==========================
+
+/*
+ * All messages are handled by this function
+ */
+function handleMessage(msg) {
+ // Attempt to call a function with the same name as the message name
+ switch(msg.name) {
+ case 'setSettings':
+ setSettings(msg.message);
+ break;
+ case 'setActive':
+ setActive(msg.message);
+ break;
+ }
+}
+
+/*
+ * Callback to pass settings to injected script
+ */
+function setSettings(msg) {
+ settings = msg;
+ bindKeyCodesToActions();
+}
+
+/*
+ * Enable or disable the extension on this tab
+ */
+function setActive(msg) {
+ extensionActive = msg;
+ if(msg) {
+ bindKeyCodesToActions();
+ } else {
+ unbindKeyCodes();
+ }
+}
+
+function isExcludedUrl(storedExcludedUrls, currentUrl) {
+ if (!storedExcludedUrls.length) {
+ return false;
+ }
+
+ var excludedUrls, regexp, url, formattedUrl, _i, _len;
+ excludedUrls = storedExcludedUrls.split(",");
+ for (_i = 0, _len = excludedUrls.length; _i < _len; _i++) {
+ url = excludedUrls[_i];
+ formattedUrl = stripProtocolAndWww(url);
+ formattedUrl = formattedUrl.toLowerCase();
+ regexp = new RegExp('((.*)?(' + formattedUrl + ')+(.*))');
+ if (currentUrl.toLowerCase().match(regexp)) {
+ return true;
+ }
+ }
+ return false;
+}
+
+// These formations removes the protocol and www so that
+// the regexp can catch less AND more specific excluded
+// domains than the current URL.
+function stripProtocolAndWww(url) {
+ url = url.replace('http://', '');
+ url = url.replace('https://', '');
+ if (url.startsWith('www.')) {
+ url = url.slice(4);
+ }
+
+ return url;
+}
+
+// Bootstrap extension
+setSettings(window.getSettings());
+
+// TODO: Implement settings management in Swift
+// Add event listener
+// safari.self.addEventListener("message", handleMessage, false);
+// Retrieve settings
+// safari.self.tab.dispatchMessage('getSettings', '');
+
+// Export to make it testable
+window.isExcludedUrl = isExcludedUrl;
+window.stripProtocolAndWww = stripProtocolAndWww;
diff --git a/app_extension/vimari/extension/js/keyboard-utils.js b/app_extension/vimari/extension/js/keyboard-utils.js
new file mode 100644
index 0000000..20887d4
--- /dev/null
+++ b/app_extension/vimari/extension/js/keyboard-utils.js
@@ -0,0 +1,63 @@
+var keyCodes = { ESC: 27, backspace: 8, deleteKey: 46, enter: 13, space: 32, shiftKey: 16, f1: 112, f12: 123};
+var keyNames = { 37: "left", 38: "up", 39: "right", 40: "down" };
+
+// This is a mapping of the incorrect keyIdentifiers generated by Webkit on Windows during keydown events to
+// the correct identifiers, which are correctly generated on Mac. We require this mapping to properly handle
+// these keys on Windows. See https://bugs.webkit.org/show_bug.cgi?id=19906 for more details.
+var keyIdentifierCorrectionMap = {
+ "U+00C0": ["U+0060", "U+007E"], // `~
+ "U+00BD": ["U+002D", "U+005F"], // -_
+ "U+00BB": ["U+003D", "U+002B"], // =+
+ "U+00DB": ["U+005B", "U+007B"], // [{
+ "U+00DD": ["U+005D", "U+007D"], // ]}
+ "U+00DC": ["U+005C", "U+007C"], // \|
+ "U+00BA": ["U+003B", "U+003A"], // ;:
+ "U+00DE": ["U+0027", "U+0022"], // '"
+ "U+00BC": ["U+002C", "U+003C"], // ,<
+ "U+00BE": ["U+002E", "U+003E"], // .>
+ "U+00BF": ["U+002F", "U+003F"] // /?
+};
+
+var platform;
+if (navigator.userAgent.indexOf("Mac") !== -1)
+ platform = "Mac";
+else if (navigator.userAgent.indexOf("Linux") !== -1)
+ platform = "Linux";
+else
+ platform = "Windows";
+
+function getKeyChar(event) {
+ // Not a letter
+ if (event.keyIdentifier.slice(0, 2) !== "U+") {
+ // Named key
+ if (keyNames[event.keyCode]) {
+ return keyNames[event.keyCode];
+ }
+ // F-key
+ if (event.keyCode >= keyCodes.f1 && event.keyCode <= keyCodes.f12) {
+ return "f" + (1 + event.keyCode - keyCodes.f1);
+ }
+ return "";
+ }
+ var keyIdentifier = event.keyIdentifier;
+ // On Windows, the keyIdentifiers for non-letter keys are incorrect. See
+ // https://bugs.webkit.org/show_bug.cgi?id=19906 for more details.
+ if ((platform === "Windows" || platform === "Linux") && keyIdentifierCorrectionMap[keyIdentifier]) {
+ correctedIdentifiers = keyIdentifierCorrectionMap[keyIdentifier];
+ keyIdentifier = event.shiftKey ? correctedIdentifiers[0] : correctedIdentifiers[1];
+ }
+ var unicodeKeyInHex = "0x" + keyIdentifier.substring(2);
+ return String.fromCharCode(parseInt(unicodeKeyInHex)).toLowerCase();
+}
+
+function isPrimaryModifierKey(event) {
+ if (platform === "Mac")
+ return event.metaKey;
+ else
+ return event.ctrlKey;
+}
+
+function isEscape(event) {
+ return event.keyCode === keyCodes.ESC ||
+ (event.ctrlKey && getKeyChar(event) === '['); // c-[ is mapped to ESC in Vim by default.
+}
diff --git a/app_extension/vimari/extension/js/link-hints.js b/app_extension/vimari/extension/js/link-hints.js
new file mode 100644
index 0000000..66d62e8
--- /dev/null
+++ b/app_extension/vimari/extension/js/link-hints.js
@@ -0,0 +1,404 @@
+/*
+ * This implements link hinting. Typing "F" will enter link-hinting mode, where all clickable items on
+ * the page have a hint marker displayed containing a sequence of letters. Typing those letters will select
+ * a link.
+ *
+ * The characters we use to show link hints are a user-configurable option. By default they're the home row.
+ * The CSS which is used on the link hints is also a configurable option.
+ */
+
+var hintMarkers = [];
+var hintMarkerContainingDiv = null;
+// The characters that were typed in while in "link hints" mode.
+var hintKeystrokeQueue = [];
+var linkHintsModeActivated = false;
+var shouldOpenLinkHintInNewTab = false;
+var shouldOpenLinkHintWithQueue = false;
+// Whether link hint's "open in current/new tab" setting is currently toggled
+var openLinkModeToggle = false;
+// Whether we have added to the page the CSS needed to display link hints.
+var linkHintsCssAdded = false;
+
+// We need this as a top-level function because our command system doesn't yet support arguments.
+function activateLinkHintsModeToOpenInNewTab() { activateLinkHintsMode(true, false); }
+
+function activateLinkHintsModeWithQueue() { activateLinkHintsMode(true, true); }
+
+function activateLinkHintsMode(openInNewTab, withQueue) {
+ if (!linkHintsCssAdded)
+ addCssToPage(linkHintCss); // linkHintCss is declared by vimiumFrontend.js
+ linkHintCssAdded = true;
+ linkHintsModeActivated = true;
+ setOpenLinkMode(openInNewTab, withQueue);
+ buildLinkHints();
+ document.addEventListener("keydown", onKeyDownInLinkHintsMode, true);
+ document.addEventListener("keyup", onKeyUpInLinkHintsMode, true);
+}
+
+function setOpenLinkMode(openInNewTab, withQueue) {
+ shouldOpenLinkHintInNewTab = openInNewTab;
+ shouldOpenLinkHintWithQueue = withQueue;
+ return;
+}
+
+/*
+ * Builds and displays link hints for every visible clickable item on the page.
+ */
+function buildLinkHints() {
+ var visibleElements = getVisibleClickableElements();
+
+ // Initialize the number used to generate the character hints to be as many digits as we need to
+ // highlight all the links on the page; we don't want some link hints to have more chars than others.
+ var digitsNeeded = Math.ceil(logXOfBase(visibleElements.length, settings.linkHintCharacters.length));
+ var linkHintNumber = 0;
+ for (var i = 0; i < visibleElements.length; i++) {
+ hintMarkers.push(createMarkerFor(visibleElements[i], linkHintNumber, digitsNeeded));
+ linkHintNumber++;
+ }
+ // Note(philc): Append these markers as top level children instead of as child nodes to the link itself,
+ // because some clickable elements cannot contain children, e.g. submit buttons. This has the caveat
+ // that if you scroll the page and the link has position=fixed, the marker will not stay fixed.
+ // Also note that adding these nodes to document.body all at once is significantly faster than one-by-one.
+ hintMarkerContainingDiv = document.createElement("div");
+ hintMarkerContainingDiv.id = "vimiumHintMarkerContainer";
+ hintMarkerContainingDiv.className = "vimiumReset";
+ for (var i = 0; i < hintMarkers.length; i++)
+ hintMarkerContainingDiv.appendChild(hintMarkers[i]);
+ document.body.appendChild(hintMarkerContainingDiv);
+}
+
+function logXOfBase(x, base) { return Math.log(x) / Math.log(base); }
+
+/*
+ * Returns all clickable elements that are not hidden and are in the current viewport.
+ * We prune invisible elements partly for performance reasons, but moreso it's to decrease the number
+ * of digits needed to enumerate all of the links on screen.
+ */
+function getVisibleClickableElements() {
+ // Get all clickable elements.
+ var elements = getClickableElements();
+
+ // Get those that are visible too.
+ var visibleElements = [];
+
+ for (var i = 0; i < elements.length; i++) {
+ var element = elements[i];
+
+ var selectedRect = getFirstVisibleRect(element);
+ if (selectedRect) {
+ visibleElements.push(selectedRect);
+ }
+ }
+
+ return visibleElements;
+}
+
+function getClickableElements() {
+ var elements = document.getElementsByTagName('*');
+ var clickableElements = [];
+ for (var i = 0; i < elements.length; i++) {
+ var element = elements[i];
+ if (isClickable(element))
+ clickableElements.push(element);
+ }
+ return clickableElements;
+}
+
+function isClickable(element) {
+ var name = element.nodeName.toLowerCase();
+ var role = element.getAttribute('role');
+
+ return (
+ // normal html elements that can be clicked
+ name === 'a' ||
+ name === 'button' ||
+ name === 'input' && element.getAttribute('type') !== 'hidden' ||
+ name === 'select' ||
+ name === 'textarea' ||
+ // elements having an ARIA role implying clickability
+ // (see http://www.w3.org/TR/wai-aria/roles#widget_roles)
+ role === 'button' ||
+ role === 'checkbox' ||
+ role === 'combobox' ||
+ role === 'link' ||
+ role === 'menuitem' ||
+ role === 'menuitemcheckbox' ||
+ role === 'menuitemradio' ||
+ role === 'radio' ||
+ role === 'tab' ||
+ role === 'textbox' ||
+ // other ways by which we can know an element is clickable
+ element.hasAttribute('onclick') ||
+ settings.detectByCursorStyle && window.getComputedStyle(element).cursor === 'pointer' &&
+ (!element.parentNode ||
+ window.getComputedStyle(element.parentNode).cursor !== 'pointer')
+ );
+}
+
+/*
+ * Get firs visible rect under an element.
+ *
+ * Inline elements can have more than one rect.
+ * Block elemens only have one rect.
+ * So, in general, add element's first visible rect, if any.
+ * If element does not have any visible rect,
+ * it can still be wrapping other visible children.
+ * So, in that case, recurse to get the first visible rect
+ * of the first child that has one.
+ */
+function getFirstVisibleRect(element) {
+ // find visible clientRect of element itself
+ var clientRects = element.getClientRects();
+ for (var i = 0; i < clientRects.length; i++) {
+ var clientRect = clientRects[i];
+ if (isVisible(element, clientRect)) {
+ return {element: element, rect: clientRect};
+ }
+ }
+ // Only iterate over elements with a children property. This is mainly to
+ // avoid issues with SVG elements, as Safari doesn't expose a children
+ // property on them.
+ if (element.children) {
+ // find visible clientRect of child
+ for (var j = 0; j < element.children.length; j++) {
+ var childClientRect = getFirstVisibleRect(element.children[j]);
+ if (childClientRect) {
+ return childClientRect;
+ }
+ }
+ }
+ return null;
+}
+
+/*
+ * Returns true if element is visible.
+ */
+function isVisible(element, clientRect) {
+ // Exclude links which have just a few pixels on screen, because the link hints won't show for them anyway.
+ var zoomFactor = currentZoomLevel / 100.0;
+ if (!clientRect || clientRect.top < 0 || clientRect.top * zoomFactor >= window.innerHeight - 4 ||
+ clientRect.left < 0 || clientRect.left * zoomFactor >= window.innerWidth - 4)
+ return false;
+
+ if (clientRect.width < 3 || clientRect.height < 3)
+ return false;
+
+ // eliminate invisible elements (see test_harnesses/visibility_test.html)
+ var computedStyle = window.getComputedStyle(element, null);
+ if (computedStyle.getPropertyValue('visibility') !== 'visible' ||
+ computedStyle.getPropertyValue('display') === 'none')
+ return false;
+
+ // Eliminate elements hidden by another overlapping element.
+ // To do that, get topmost element at some offset from upper-left corner of clientRect
+ // and check whether it is the element itself or one of its descendants.
+ // The offset is needed to account for coordinates truncation and elements with rounded borders.
+ //
+ // Coordinates truncation occcurs when using zoom. In that case, clientRect coords should be float,
+ // but we get integers instead. That makes so that elementFromPoint(clientRect.left, clientRect.top)
+ // sometimes returns an element different from the one clientRect was obtained from.
+ // So we introduce an offset to make sure elementFromPoint hits the right element.
+ //
+ // For elements with a rounded topleft border, the upper left corner lies outside the element.
+ // Then, we need an offset to get to the point nearest to the upper left corner, but within border.
+ var coordTruncationOffset = 2, // A value of 1 has been observed not to be enough,
+ // so we heuristically choose 2, which seems to work well.
+ // We know a value of 2 is still safe (lies within the element) because,
+ // from the code above, widht & height are >= 3.
+ radius = parseFloat(computedStyle.borderTopLeftRadius),
+ roundedBorderOffset = Math.ceil(radius * (1 - Math.sin(Math.PI / 4))),
+ offset = Math.max(coordTruncationOffset, roundedBorderOffset);
+ if (offset >= clientRect.width || offset >= clientRect.height)
+ return false;
+ var el = document.elementFromPoint(clientRect.left + offset, clientRect.top + offset);
+ while (el && el !== element)
+ el = el.parentNode;
+ if (!el)
+ return false;
+
+ return true;
+}
+
+function onKeyDownInLinkHintsMode(event) {
+ console.log("-- key down pressed --");
+ if (event.keyCode === keyCodes.shiftKey && !openLinkModeToggle) {
+ // Toggle whether to open link in a new or current tab.
+ setOpenLinkMode(!shouldOpenLinkHintInNewTab, shouldOpenLinkHintWithQueue);
+ openLinkModeToggle = true;
+ }
+
+ var keyChar = getKeyChar(event);
+ if (!keyChar)
+ return;
+
+ // TODO(philc): Ignore keys that have modifiers.
+ if (isEscape(event)) {
+ deactivateLinkHintsMode();
+ } else if (event.keyCode === keyCodes.backspace || event.keyCode === keyCodes.deleteKey) {
+ if (hintKeystrokeQueue.length === 0) {
+ deactivateLinkHintsMode();
+ } else {
+ hintKeystrokeQueue.pop();
+ updateLinkHints();
+ }
+ } else if (settings.linkHintCharacters.indexOf(keyChar) >= 0) {
+ hintKeystrokeQueue.push(keyChar);
+ updateLinkHints();
+ } else {
+ return;
+ }
+
+ event.stopPropagation();
+ event.preventDefault();
+}
+
+function onKeyUpInLinkHintsMode(event) {
+ if (event.keyCode === keyCodes.shiftKey && openLinkModeToggle) {
+ // Revert toggle on whether to open link in new or current tab.
+ setOpenLinkMode(!shouldOpenLinkHintInNewTab, shouldOpenLinkHintWithQueue);
+ openLinkModeToggle = false;
+ }
+ event.stopPropagation();
+ event.preventDefault();
+}
+
+/*
+ * Updates the visibility of link hints on screen based on the keystrokes typed thus far. If only one
+ * link hint remains, click on that link and exit link hints mode.
+ */
+function updateLinkHints() {
+ var matchString = hintKeystrokeQueue.join("");
+ var linksMatched = highlightLinkMatches(matchString);
+ if (linksMatched.length === 0)
+ deactivateLinkHintsMode();
+ else if (linksMatched.length === 1) {
+ var matchedLink = linksMatched[0];
+ if (isSelectable(matchedLink)) {
+ matchedLink.focus();
+ // When focusing a textbox, put the selection caret at the end of the textbox's contents.
+ matchedLink.setSelectionRange(matchedLink.value.length, matchedLink.value.length);
+ deactivateLinkHintsMode();
+ } else {
+ // When we're opening the link in the current tab, don't navigate to the selected link immediately;
+ // we want to give the user some feedback depicting which link they've selected by focusing it.
+ if (shouldOpenLinkHintWithQueue) {
+ simulateClick(matchedLink, false);
+ resetLinkHintsMode();
+ } else if (shouldOpenLinkHintInNewTab) {
+ simulateClick(matchedLink, true);
+ matchedLink.focus();
+ deactivateLinkHintsMode();
+ } else {
+ setTimeout(function() { simulateClick(matchedLink, false); }, 400);
+ matchedLink.focus();
+ deactivateLinkHintsMode();
+ }
+ }
+ }
+}
+
+/*
+ * Selectable means the element has a text caret; this is not the same as "focusable".
+ */
+function isSelectable(element) {
+ var selectableTypes = ["search", "text", "password"];
+ return (element.tagName === "INPUT" && selectableTypes.indexOf(element.type) >= 0) ||
+ element.tagName === "TEXTAREA";
+}
+
+/*
+ * Hides link hints which do not match the given search string. To allow the backspace key to work, this
+ * will also show link hints which do match but were previously hidden.
+ */
+function highlightLinkMatches(searchString) {
+ var linksMatched = [];
+ for (var i = 0; i < hintMarkers.length; i++) {
+ var linkMarker = hintMarkers[i];
+ if (linkMarker.getAttribute("hintString").indexOf(searchString) === 0) {
+ if (linkMarker.style.display === "none")
+ linkMarker.style.display = "";
+ for (var j = 0; j < linkMarker.childNodes.length; j++)
+ linkMarker.childNodes[j].className = (j >= searchString.length) ? "" : "matchingCharacter";
+ linksMatched.push(linkMarker.clickableItem);
+ } else {
+ linkMarker.style.display = "none";
+ }
+ }
+ return linksMatched;
+}
+
+/*
+ * Converts a number like "8" into a hint string like "JK". This is used to sequentially generate all of
+ * the hint text. The hint string will be "padded with zeroes" to ensure its length is equal to numHintDigits.
+ */
+function numberToHintString(number, numHintDigits) {
+ var base = settings.linkHintCharacters.length;
+ var hintString = [];
+ var remainder = 0;
+ do {
+ remainder = number % base;
+ hintString.unshift(settings.linkHintCharacters[remainder]);
+ number -= remainder;
+ number /= Math.floor(base);
+ } while (number > 0);
+
+ // Pad the hint string we're returning so that it matches numHintDigits.
+ var hintStringLength = hintString.length;
+ for (var i = 0; i < numHintDigits - hintStringLength; i++)
+ hintString.unshift(settings.linkHintCharacters[0]);
+ return hintString.join("");
+}
+
+function simulateClick(link, openInNewTab) {
+ if (openInNewTab) {
+ window.open(link, "_blank");
+ } else {
+ link.click();
+ }
+
+ // If clicking the link doesn't take you to a new page
+ // the focus should not stay on the link, hence calling blur()
+ link.blur();
+}
+
+function deactivateLinkHintsMode() {
+ if (hintMarkerContainingDiv)
+ hintMarkerContainingDiv.parentNode.removeChild(hintMarkerContainingDiv);
+ hintMarkerContainingDiv = null;
+ hintMarkers = [];
+ hintKeystrokeQueue = [];
+ document.removeEventListener("keydown", onKeyDownInLinkHintsMode, true);
+ document.removeEventListener("keyup", onKeyUpInLinkHintsMode, true);
+ linkHintsModeActivated = false;
+}
+
+function resetLinkHintsMode() {
+ deactivateLinkHintsMode();
+ activateLinkHintsModeWithQueue();
+}
+
+/*
+ * Creates a link marker for the given link.
+ */
+function createMarkerFor(link, linkHintNumber, linkHintDigits) {
+ var hintString = numberToHintString(linkHintNumber, linkHintDigits);
+ var marker = document.createElement("div");
+ marker.className = "internalVimiumHintMarker vimiumReset";
+ var innerHTML = [];
+ // Make each hint character a span, so that we can highlight the typed characters as you type them.
+ for (var i = 0; i < hintString.length; i++)
+ innerHTML.push('' + hintString[i].toUpperCase() + '');
+ marker.innerHTML = innerHTML.join("");
+ marker.setAttribute("hintString", hintString);
+
+ // Note: this call will be expensive if we modify the DOM in between calls.
+ var clientRect = link.rect;
+ // The coordinates given by the window do not have the zoom factor included since the zoom is set only on
+ // the document node.
+ var zoomFactor = currentZoomLevel / 100.0;
+ marker.style.left = clientRect.left + window.scrollX / zoomFactor + "px";
+ marker.style.top = clientRect.top + window.scrollY / zoomFactor + "px";
+
+ marker.clickableItem = link.element;
+ return marker;
+}
diff --git a/app_extension/vimari/extension/js/mocks.js b/app_extension/vimari/extension/js/mocks.js
new file mode 100644
index 0000000..e4e7026
--- /dev/null
+++ b/app_extension/vimari/extension/js/mocks.js
@@ -0,0 +1,9 @@
+var safari = {
+ self: {
+ tab: {
+ dispatchMessage: function () {},
+ },
+ addEventListener: function () {},
+ }
+};
+window.safari = safari;
diff --git a/app_extension/vimari/extension/js/mousetrap.js b/app_extension/vimari/extension/js/mousetrap.js
new file mode 100644
index 0000000..f8c080d
--- /dev/null
+++ b/app_extension/vimari/extension/js/mousetrap.js
@@ -0,0 +1,819 @@
+/**
+ * Copyright 2012 Craig Campbell
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * Mousetrap is a simple keyboard shortcut library for Javascript with
+ * no external dependencies
+ *
+ * @version 1.3.0
+ * @url craig.is/killing/mice
+ */
+(function() {
+
+ /**
+ * mapping of special keycodes to their corresponding keys
+ *
+ * everything in this dictionary cannot use keypress events
+ * so it has to be here to map to the correct keycodes for
+ * keyup/keydown events
+ *
+ * @type {Object}
+ */
+ var _MAP = {
+ 8: 'backspace',
+ 9: 'tab',
+ 13: 'enter',
+ 16: 'shift',
+ 17: 'ctrl',
+ 18: 'alt',
+ 20: 'capslock',
+ 27: 'esc',
+ 32: 'space',
+ 33: 'pageup',
+ 34: 'pagedown',
+ 35: 'end',
+ 36: 'home',
+ 37: 'left',
+ 38: 'up',
+ 39: 'right',
+ 40: 'down',
+ 45: 'ins',
+ 46: 'del',
+ 91: 'meta',
+ 93: 'meta',
+ 224: 'meta'
+ },
+
+ /**
+ * mapping for special characters so they can support
+ *
+ * this dictionary is only used incase you want to bind a
+ * keyup or keydown event to one of these keys
+ *
+ * @type {Object}
+ */
+ _KEYCODE_MAP = {
+ 106: '*',
+ 107: '+',
+ 109: '-',
+ 110: '.',
+ 111 : '/',
+ 186: ';',
+ 187: '=',
+ 188: ',',
+ 189: '-',
+ 190: '.',
+ 191: '/',
+ 192: '`',
+ 219: '[',
+ 220: '\\',
+ 221: ']',
+ 222: '\''
+ },
+
+ /**
+ * this is a mapping of keys that require shift on a US keypad
+ * back to the non shift equivelents
+ *
+ * this is so you can use keyup events with these keys
+ *
+ * note that this will only work reliably on US keyboards
+ *
+ * @type {Object}
+ */
+ _SHIFT_MAP = {
+ '~': '`',
+ '!': '1',
+ '@': '2',
+ '#': '3',
+ '$': '4',
+ '%': '5',
+ '^': '6',
+ '&': '7',
+ '*': '8',
+ '(': '9',
+ ')': '0',
+ '_': '-',
+ '+': '=',
+ ':': ';',
+ '\"': '\'',
+ '<': ',',
+ '>': '.',
+ '?': '/',
+ '|': '\\'
+ },
+
+ /**
+ * this is a list of special strings you can use to map
+ * to modifier keys when you specify your keyboard shortcuts
+ *
+ * @type {Object}
+ */
+ _SPECIAL_ALIASES = {
+ 'option': 'alt',
+ 'command': 'meta',
+ 'return': 'enter',
+ 'escape': 'esc'
+ },
+
+ /**
+ * variable to store the flipped version of _MAP from above
+ * needed to check if we should use keypress or not when no action
+ * is specified
+ *
+ * @type {Object|undefined}
+ */
+ _REVERSE_MAP,
+
+ /**
+ * a list of all the callbacks setup via Mousetrap.bind()
+ *
+ * @type {Object}
+ */
+ _callbacks = {},
+
+ /**
+ * direct map of string combinations to callbacks used for trigger()
+ *
+ * @type {Object}
+ */
+ _directMap = {},
+
+ /**
+ * keeps track of what level each sequence is at since multiple
+ * sequences can start out with the same sequence
+ *
+ * @type {Object}
+ */
+ _sequenceLevels = {},
+
+ /**
+ * variable to store the setTimeout call
+ *
+ * @type {null|number}
+ */
+ _resetTimer,
+
+ /**
+ * temporary state where we will ignore the next keyup
+ *
+ * @type {boolean|string}
+ */
+ _ignoreNextKeyup = false,
+
+ /**
+ * are we currently inside of a sequence?
+ * type of action ("keyup" or "keydown" or "keypress") or false
+ *
+ * @type {boolean|string}
+ */
+ _sequenceType = false;
+
+ /**
+ * loop through the f keys, f1 to f19 and add them to the map
+ * programatically
+ */
+ for (var i = 1; i < 20; ++i) {
+ _MAP[111 + i] = 'f' + i;
+ }
+
+ /**
+ * loop through to map numbers on the numeric keypad
+ */
+ for (i = 0; i <= 9; ++i) {
+ _MAP[i + 96] = i;
+ }
+
+ /**
+ * cross browser add event method
+ *
+ * @param {Element|HTMLDocument} object
+ * @param {string} type
+ * @param {Function} callback
+ * @returns void
+ */
+ function _addEvent(object, type, callback) {
+ if (object.addEventListener) {
+ object.addEventListener(type, callback, false);
+ return;
+ }
+
+ object.attachEvent('on' + type, callback);
+ }
+
+ /**
+ * takes the event and returns the key character
+ *
+ * @param {Event} e
+ * @return {string}
+ */
+ function _characterFromEvent(e) {
+
+ // for keypress events we should return the character as is
+ if (e.type == 'keypress') {
+ return String.fromCharCode(e.which);
+ }
+
+ // for non keypress events the special maps are needed
+ if (_MAP[e.which]) {
+ return _MAP[e.which];
+ }
+
+ if (_KEYCODE_MAP[e.which]) {
+ return _KEYCODE_MAP[e.which];
+ }
+
+ // if it is not in the special map
+ return String.fromCharCode(e.which).toLowerCase();
+ }
+
+ /**
+ * checks if two arrays are equal
+ *
+ * @param {Array} modifiers1
+ * @param {Array} modifiers2
+ * @returns {boolean}
+ */
+ function _modifiersMatch(modifiers1, modifiers2) {
+ return modifiers1.sort().join(',') === modifiers2.sort().join(',');
+ }
+
+ /**
+ * resets all sequence counters except for the ones passed in
+ *
+ * @param {Object} doNotReset
+ * @returns void
+ */
+ function _resetSequences(doNotReset, maxLevel) {
+ doNotReset = doNotReset || {};
+
+ var activeSequences = false,
+ key;
+
+ for (key in _sequenceLevels) {
+ if (doNotReset[key] && _sequenceLevels[key] > maxLevel) {
+ activeSequences = true;
+ continue;
+ }
+ _sequenceLevels[key] = 0;
+ }
+
+ if (!activeSequences) {
+ _sequenceType = false;
+ }
+ }
+
+ /**
+ * finds all callbacks that match based on the keycode, modifiers,
+ * and action
+ *
+ * @param {string} character
+ * @param {Array} modifiers
+ * @param {Event|Object} e
+ * @param {boolean=} remove - should we remove any matches
+ * @param {string=} combination
+ * @returns {Array}
+ */
+ function _getMatches(character, modifiers, e, remove, combination) {
+ var i,
+ callback,
+ matches = [],
+ action = e.type;
+
+ // if there are no events related to this keycode
+ if (!_callbacks[character]) {
+ return [];
+ }
+
+ // if a modifier key is coming up on its own we should allow it
+ if (action == 'keyup' && _isModifier(character)) {
+ modifiers = [character];
+ }
+
+ // loop through all callbacks for the key that was pressed
+ // and see if any of them match
+ for (i = 0; i < _callbacks[character].length; ++i) {
+ callback = _callbacks[character][i];
+
+ // if this is a sequence but it is not at the right level
+ // then move onto the next match
+ if (callback.seq && _sequenceLevels[callback.seq] != callback.level) {
+ continue;
+ }
+
+ // if the action we are looking for doesn't match the action we got
+ // then we should keep going
+ if (action != callback.action) {
+ continue;
+ }
+
+ // if this is a keypress event and the meta key and control key
+ // are not pressed that means that we need to only look at the
+ // character, otherwise check the modifiers as well
+ //
+ // chrome will not fire a keypress if meta or control is down
+ // safari will fire a keypress if meta or meta+shift is down
+ // firefox will fire a keypress if meta or control is down
+ if ((action == 'keypress' && !e.metaKey && !e.ctrlKey) || _modifiersMatch(modifiers, callback.modifiers)) {
+
+ // remove is used so if you change your mind and call bind a
+ // second time with a new function the first one is overwritten
+ if (remove && callback.combo == combination) {
+ _callbacks[character].splice(i, 1);
+ }
+
+ matches.push(callback);
+ }
+ }
+
+ return matches;
+ }
+
+ /**
+ * takes a key event and figures out what the modifiers are
+ *
+ * @param {Event} e
+ * @returns {Array}
+ */
+ function _eventModifiers(e) {
+ var modifiers = [];
+
+ if (e.shiftKey) {
+ modifiers.push('shift');
+ }
+
+ if (e.altKey) {
+ modifiers.push('alt');
+ }
+
+ if (e.ctrlKey) {
+ modifiers.push('ctrl');
+ }
+
+ if (e.metaKey) {
+ modifiers.push('meta');
+ }
+
+ return modifiers;
+ }
+
+ /**
+ * actually calls the callback function
+ *
+ * if your callback function returns false this will use the jquery
+ * convention - prevent default and stop propogation on the event
+ *
+ * @param {Function} callback
+ * @param {Event} e
+ * @returns void
+ */
+ function _fireCallback(callback, e, combo) {
+
+ // if this event should not happen stop here
+ if (Mousetrap.stopCallback(e, e.target || e.srcElement, combo)) {
+ return;
+ }
+
+ if (callback(e, combo) === false) {
+ if (e.preventDefault) {
+ e.preventDefault();
+ }
+
+ if (e.stopPropagation) {
+ e.stopPropagation();
+ }
+
+ e.returnValue = false;
+ e.cancelBubble = true;
+ }
+ }
+
+ /**
+ * handles a character key event
+ *
+ * @param {string} character
+ * @param {Event} e
+ * @returns void
+ */
+ function _handleCharacter(character, e) {
+ var callbacks = _getMatches(character, _eventModifiers(e), e),
+ i,
+ doNotReset = {},
+ maxLevel = 0,
+ processedSequenceCallback = false;
+
+ // loop through matching callbacks for this key event
+ for (i = 0; i < callbacks.length; ++i) {
+
+ // fire for all sequence callbacks
+ // this is because if for example you have multiple sequences
+ // bound such as "g i" and "g t" they both need to fire the
+ // callback for matching g cause otherwise you can only ever
+ // match the first one
+ if (callbacks[i].seq) {
+ processedSequenceCallback = true;
+
+ // as we loop through keep track of the max
+ // any sequence at a lower level will be discarded
+ maxLevel = Math.max(maxLevel, callbacks[i].level);
+
+ // keep a list of which sequences were matches for later
+ doNotReset[callbacks[i].seq] = 1;
+ _fireCallback(callbacks[i].callback, e, callbacks[i].combo);
+ continue;
+ }
+
+ // if there were no sequence matches but we are still here
+ // that means this is a regular match so we should fire that
+ if (!processedSequenceCallback && !_sequenceType) {
+ _fireCallback(callbacks[i].callback, e, callbacks[i].combo);
+ }
+ }
+
+ // if you are inside of a sequence and the key you are pressing
+ // is not a modifier key then we should reset all sequences
+ // that were not matched by this key event
+ if (e.type == _sequenceType && !_isModifier(character)) {
+ _resetSequences(doNotReset, maxLevel);
+ }
+ }
+
+ /**
+ * handles a keydown event
+ *
+ * @param {Event} e
+ * @returns void
+ */
+ function _handleKey(e) {
+
+ // normalize e.which for key events
+ // @see http://stackoverflow.com/questions/4285627/javascript-keycode-vs-charcode-utter-confusion
+ if (typeof e.which !== 'number') {
+ e.which = e.keyCode;
+ }
+
+ var character = _characterFromEvent(e);
+
+ // no character found then stop
+ if (!character) {
+ return;
+ }
+
+ if (e.type == 'keyup' && _ignoreNextKeyup == character) {
+ _ignoreNextKeyup = false;
+ return;
+ }
+
+ _handleCharacter(character, e);
+ }
+
+ /**
+ * determines if the keycode specified is a modifier key or not
+ *
+ * @param {string} key
+ * @returns {boolean}
+ */
+ function _isModifier(key) {
+ return key == 'shift' || key == 'ctrl' || key == 'alt' || key == 'meta';
+ }
+
+ /**
+ * called to set a 1 second timeout on the specified sequence
+ *
+ * this is so after each key press in the sequence you have 1 second
+ * to press the next key before you have to start over
+ *
+ * @returns void
+ */
+ function _resetSequenceTimer() {
+ clearTimeout(_resetTimer);
+ _resetTimer = setTimeout(_resetSequences, 1000);
+ }
+
+ /**
+ * reverses the map lookup so that we can look for specific keys
+ * to see what can and can't use keypress
+ *
+ * @return {Object}
+ */
+ function _getReverseMap() {
+ if (!_REVERSE_MAP) {
+ _REVERSE_MAP = {};
+ for (var key in _MAP) {
+
+ // pull out the numeric keypad from here cause keypress should
+ // be able to detect the keys from the character
+ if (key > 95 && key < 112) {
+ continue;
+ }
+
+ if (_MAP.hasOwnProperty(key)) {
+ _REVERSE_MAP[_MAP[key]] = key;
+ }
+ }
+ }
+ return _REVERSE_MAP;
+ }
+
+ /**
+ * picks the best action based on the key combination
+ *
+ * @param {string} key - character for key
+ * @param {Array} modifiers
+ * @param {string=} action passed in
+ */
+ function _pickBestAction(key, modifiers, action) {
+
+ // if no action was picked in we should try to pick the one
+ // that we think would work best for this key
+ if (!action) {
+ action = _getReverseMap()[key] ? 'keydown' : 'keypress';
+ }
+
+ // modifier keys don't work as expected with keypress,
+ // switch to keydown
+ if (action == 'keypress' && modifiers.length) {
+ action = 'keydown';
+ }
+
+ return action;
+ }
+
+ /**
+ * binds a key sequence to an event
+ *
+ * @param {string} combo - combo specified in bind call
+ * @param {Array} keys
+ * @param {Function} callback
+ * @param {string=} action
+ * @returns void
+ */
+ function _bindSequence(combo, keys, callback, action) {
+
+ // start off by adding a sequence level record for this combination
+ // and setting the level to 0
+ _sequenceLevels[combo] = 0;
+
+ // if there is no action pick the best one for the first key
+ // in the sequence
+ if (!action) {
+ action = _pickBestAction(keys[0], []);
+ }
+
+ /**
+ * callback to increase the sequence level for this sequence and reset
+ * all other sequences that were active
+ *
+ * @param {Event} e
+ * @returns void
+ */
+ var _increaseSequence = function(e) {
+ _sequenceType = action;
+ ++_sequenceLevels[combo];
+ _resetSequenceTimer();
+ },
+
+ /**
+ * wraps the specified callback inside of another function in order
+ * to reset all sequence counters as soon as this sequence is done
+ *
+ * @param {Event} e
+ * @returns void
+ */
+ _callbackAndReset = function(e) {
+ _fireCallback(callback, e, combo);
+
+ // we should ignore the next key up if the action is key down
+ // or keypress. this is so if you finish a sequence and
+ // release the key the final key will not trigger a keyup
+ if (action !== 'keyup') {
+ _ignoreNextKeyup = _characterFromEvent(e);
+ }
+
+ // weird race condition if a sequence ends with the key
+ // another sequence begins with
+ setTimeout(_resetSequences, 10);
+ },
+ i;
+
+ // loop through keys one at a time and bind the appropriate callback
+ // function. for any key leading up to the final one it should
+ // increase the sequence. after the final, it should reset all sequences
+ for (i = 0; i < keys.length; ++i) {
+ _bindSingle(keys[i], i < keys.length - 1 ? _increaseSequence : _callbackAndReset, action, combo, i);
+ }
+ }
+
+ /**
+ * binds a single keyboard combination
+ *
+ * @param {string} combination
+ * @param {Function} callback
+ * @param {string=} action
+ * @param {string=} sequenceName - name of sequence if part of sequence
+ * @param {number=} level - what part of the sequence the command is
+ * @returns void
+ */
+ function _bindSingle(combination, callback, action, sequenceName, level) {
+
+ // store a direct mapped reference for use with Mousetrap.trigger
+ _directMap[combination + ':' + action] = callback;
+
+ // make sure multiple spaces in a row become a single space
+ combination = combination.replace(/\s+/g, ' ');
+
+ var sequence = combination.split(' '),
+ i,
+ key,
+ keys,
+ modifiers = [];
+
+ // if this pattern is a sequence of keys then run through this method
+ // to reprocess each pattern one key at a time
+ if (sequence.length > 1) {
+ _bindSequence(combination, sequence, callback, action);
+ return;
+ }
+
+ // take the keys from this pattern and figure out what the actual
+ // pattern is all about
+ keys = combination === '+' ? ['+'] : combination.split('+');
+
+ for (i = 0; i < keys.length; ++i) {
+ key = keys[i];
+
+ // normalize key names
+ if (_SPECIAL_ALIASES[key]) {
+ key = _SPECIAL_ALIASES[key];
+ }
+
+ // if this is not a keypress event then we should
+ // be smart about using shift keys
+ // this will only work for US keyboards however
+ if (action && action != 'keypress' && _SHIFT_MAP[key]) {
+ key = _SHIFT_MAP[key];
+ modifiers.push('shift');
+ }
+
+ // if this key is a modifier then add it to the list of modifiers
+ if (_isModifier(key)) {
+ modifiers.push(key);
+ }
+ }
+
+ // depending on what the key combination is
+ // we will try to pick the best event for it
+ action = _pickBestAction(key, modifiers, action);
+
+ // make sure to initialize array if this is the first time
+ // a callback is added for this key
+ if (!_callbacks[key]) {
+ _callbacks[key] = [];
+ }
+
+ // remove an existing match if there is one
+ _getMatches(key, modifiers, {type: action}, !sequenceName, combination);
+
+ // add this call back to the array
+ // if it is a sequence put it at the beginning
+ // if not put it at the end
+ //
+ // this is important because the way these are processed expects
+ // the sequence ones to come first
+ _callbacks[key][sequenceName ? 'unshift' : 'push']({
+ callback: callback,
+ modifiers: modifiers,
+ action: action,
+ seq: sequenceName,
+ level: level,
+ combo: combination
+ });
+ }
+
+ /**
+ * binds multiple combinations to the same callback
+ *
+ * @param {Array} combinations
+ * @param {Function} callback
+ * @param {string|undefined} action
+ * @returns void
+ */
+ function _bindMultiple(combinations, callback, action) {
+ for (var i = 0; i < combinations.length; ++i) {
+ _bindSingle(combinations[i], callback, action);
+ }
+ }
+
+ // start!
+ _addEvent(document, 'keypress', _handleKey);
+ _addEvent(document, 'keydown', _handleKey);
+ _addEvent(document, 'keyup', _handleKey);
+
+ var Mousetrap = {
+
+ /**
+ * binds an event to mousetrap
+ *
+ * can be a single key, a combination of keys separated with +,
+ * an array of keys, or a sequence of keys separated by spaces
+ *
+ * be sure to list the modifier keys first to make sure that the
+ * correct key ends up getting bound (the last key in the pattern)
+ *
+ * @param {string|Array} keys
+ * @param {Function} callback
+ * @param {string=} action - 'keypress', 'keydown', or 'keyup'
+ * @returns void
+ */
+ bind: function(keys, callback, action) {
+ keys = keys instanceof Array ? keys : [keys];
+ _bindMultiple(keys, callback, action);
+ return this;
+ },
+
+ /**
+ * unbinds an event to mousetrap
+ *
+ * the unbinding sets the callback function of the specified key combo
+ * to an empty function and deletes the corresponding key in the
+ * _directMap dict.
+ *
+ * TODO: actually remove this from the _callbacks dictionary instead
+ * of binding an empty function
+ *
+ * the keycombo+action has to be exactly the same as
+ * it was defined in the bind method
+ *
+ * @param {string|Array} keys
+ * @param {string} action
+ * @returns void
+ */
+ unbind: function(keys, action) {
+ return Mousetrap.bind(keys, function() {}, action);
+ },
+
+ /**
+ * triggers an event that has already been bound
+ *
+ * @param {string} keys
+ * @param {string=} action
+ * @returns void
+ */
+ trigger: function(keys, action) {
+ if (_directMap[keys + ':' + action]) {
+ _directMap[keys + ':' + action]();
+ }
+ return this;
+ },
+
+ /**
+ * resets the library back to its initial state. this is useful
+ * if you want to clear out the current keyboard shortcuts and bind
+ * new ones - for example if you switch to another page
+ *
+ * @returns void
+ */
+ reset: function() {
+ _callbacks = {};
+ _directMap = {};
+ return this;
+ },
+
+ /**
+ * should we stop this event before firing off callbacks
+ *
+ * @param {Event} e
+ * @param {Element} element
+ * @return {boolean}
+ */
+ stopCallback: function(e, element, combo) {
+
+ // if the element has the class "mousetrap" then no need to stop
+ if ((' ' + element.className + ' ').indexOf(' mousetrap ') > -1) {
+ return false;
+ }
+
+ // stop for input, select, and textarea
+ return element.tagName == 'INPUT' || element.tagName == 'SELECT' || element.tagName == 'TEXTAREA' || (element.contentEditable && element.contentEditable == 'true');
+ }
+ };
+
+ // expose mousetrap to the global object
+ window.Mousetrap = Mousetrap;
+
+ // expose mousetrap as an AMD module
+ if (typeof define === 'function' && define.amd) {
+ define(Mousetrap);
+ }
+}) ();
diff --git a/app_extension/vimari/extension/js/settings.js b/app_extension/vimari/extension/js/settings.js
new file mode 100644
index 0000000..81d777f
--- /dev/null
+++ b/app_extension/vimari/extension/js/settings.js
@@ -0,0 +1,33 @@
+function getSettings() {
+ return {
+ 'modifier': undefined,
+ 'excludedUrls': '',
+
+ 'hintToggle': 'f',
+ 'newTabHintToggle': 'F',
+ 'linkHintCharacters': 'asdfjklqwerzxc',
+ 'detectByCursorStyle': false,
+
+ 'scrollUp': 'k',
+ 'scrollDown': 'j',
+ 'scrollLeft': 'h',
+ 'scrollRight': 'l',
+ 'scrollSize': 50,
+ 'scrollUpHalfPage': 'u',
+ 'scrollDownHalfPage': 'd',
+ 'goToPageTop': 'g g',
+ 'goToPageBottom': 'shift+g',
+
+ 'goBack': 'shift+h',
+ 'goForward': 'shift+l',
+ 'reload': 'r',
+ 'tabForward': 'w',
+ 'tabBack': 'q',
+ 'closeTab': 'x',
+ 'closeTabReverse': 'shift+x',
+
+ 'openTab': '', // This doesn't map to anything from what I can tell
+ };
+}
+
+window.getSettings = getSettings;
diff --git a/app_extension/vimari/extension/js/vimium-scripts.js b/app_extension/vimari/extension/js/vimium-scripts.js
new file mode 100644
index 0000000..698725d
--- /dev/null
+++ b/app_extension/vimari/extension/js/vimium-scripts.js
@@ -0,0 +1,108 @@
+/*
+ * Code in this file is taken directly from vimium
+ */
+
+
+/*
+ * A heads-up-display (HUD) for showing Vimium page operations.
+ * Note: you cannot interact with the HUD until document.body is available.
+ */
+HUD = {
+ _tweenId: -1,
+ _displayElement: null,
+ _upgradeNotificationElement: null,
+
+ showForDuration: function(text, duration) {
+ HUD.show(text);
+ HUD._showForDurationTimerId = setTimeout(function() { HUD.hide(); }, duration);
+ },
+
+ show: function(text) {
+ clearTimeout(HUD._showForDurationTimerId);
+ HUD.displayElement().innerHTML = text;
+ clearInterval(HUD._tweenId);
+ HUD._tweenId = Tween.fade(HUD.displayElement(), 1.0, 150);
+ HUD.displayElement().style.display = "";
+ },
+
+ onUpdateLinkClicked: function(event) {
+ HUD.hideUpgradeNotification();
+ chrome.extension.sendRequest({ handler: "upgradeNotificationClosed" });
+ },
+
+ hideUpgradeNotification: function(clickEvent) {
+ Tween.fade(HUD.upgradeNotificationElement(), 0, 150,
+ function() { HUD.upgradeNotificationElement().style.display = "none"; });
+ },
+
+ updatePageZoomLevel: function(pageZoomLevel) {
+ // Since the chrome HUD does not scale with the page's zoom level, neither will this HUD.
+ var inverseZoomLevel = (100.0 / pageZoomLevel) * 100;
+ if (HUD._displayElement)
+ HUD.displayElement().style.zoom = inverseZoomLevel + "%";
+ if (HUD._upgradeNotificationElement)
+ HUD.upgradeNotificationElement().style.zoom = inverseZoomLevel + "%";
+ },
+
+ /*
+ * Retrieves the HUD HTML element.
+ */
+ displayElement: function() {
+ if (!HUD._displayElement) {
+ HUD._displayElement = HUD.createHudElement();
+ HUD.updatePageZoomLevel(currentZoomLevel);
+ }
+ return HUD._displayElement;
+ },
+
+ createHudElement: function() {
+ var element = document.createElement("div");
+ element.className = "vimiumHUD";
+ document.body.appendChild(element);
+ return element;
+ },
+
+ hide: function() {
+ clearInterval(HUD._tweenId);
+ HUD._tweenId = Tween.fade(HUD.displayElement(), 0, 150,
+ function() { HUD.displayElement().style.display = "none"; });
+ },
+
+ isReady: function() { return document.body != null; }
+};
+
+
+
+
+Tween = {
+ /*
+ * Fades an element's alpha. Returns a timer ID which can be used to stop the tween via clearInterval.
+ */
+ fade: function(element, toAlpha, duration, onComplete) {
+ var state = {};
+ state.duration = duration;
+ state.startTime = (new Date()).getTime();
+ state.from = parseInt(element.style.opacity) || 0;
+ state.to = toAlpha;
+ state.onUpdate = function(value) {
+ element.style.opacity = value;
+ if (value == state.to && onComplete)
+ onComplete();
+ };
+ state.timerId = setInterval(function() { Tween.performTweenStep(state); }, 50);
+ return state.timerId;
+ },
+
+ performTweenStep: function(state) {
+ var elapsed = (new Date()).getTime() - state.startTime;
+ if (elapsed >= state.duration) {
+ clearInterval(state.timerId);
+ state.onUpdate(state.to);
+ } else {
+ var value = (elapsed / state.duration) * (state.to - state.from) + state.from;
+ state.onUpdate(value);
+ }
+ }
+};
+
+
diff --git a/app_extension/vimari/vimari.xcodeproj/project.pbxproj b/app_extension/vimari/vimari.xcodeproj/project.pbxproj
new file mode 100644
index 0000000..0b79f9d
--- /dev/null
+++ b/app_extension/vimari/vimari.xcodeproj/project.pbxproj
@@ -0,0 +1,550 @@
+// !$*UTF8*$!
+{
+ archiveVersion = 1;
+ classes = {
+ };
+ objectVersion = 50;
+ objects = {
+
+/* Begin PBXBuildFile section */
+ F43C527020F93B6C0049445F /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F43C526F20F93B6C0049445F /* AppDelegate.swift */; };
+ F43C527220F93B6D0049445F /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F43C527120F93B6D0049445F /* Assets.xcassets */; };
+ F43C527520F93B6D0049445F /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = F43C527320F93B6D0049445F /* MainMenu.xib */; };
+ F43C528420F93B840049445F /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F43C528320F93B840049445F /* Cocoa.framework */; };
+ F43C528720F93B840049445F /* SafariExtensionHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = F43C528620F93B840049445F /* SafariExtensionHandler.swift */; };
+ F43C528920F93B840049445F /* SafariExtensionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F43C528820F93B840049445F /* SafariExtensionViewController.swift */; };
+ F43C528C20F93B840049445F /* SafariExtensionViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = F43C528A20F93B840049445F /* SafariExtensionViewController.xib */; };
+ F43C529520F93B840049445F /* extension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = F43C528120F93B840049445F /* extension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
+ F43C52AC20F93BDF0049445F /* injected.css in Resources */ = {isa = PBXBuildFile; fileRef = F43C52AB20F93BDF0049445F /* injected.css */; };
+ F43C52C520F93EE00049445F /* link-hints.js in Resources */ = {isa = PBXBuildFile; fileRef = F43C52BD20F93EE00049445F /* link-hints.js */; };
+ F43C52C620F93EE00049445F /* global.js in Resources */ = {isa = PBXBuildFile; fileRef = F43C52BE20F93EE00049445F /* global.js */; };
+ F43C52C720F93EE00049445F /* injected.js in Resources */ = {isa = PBXBuildFile; fileRef = F43C52BF20F93EE00049445F /* injected.js */; };
+ F43C52C820F93EE00049445F /* mocks.js in Resources */ = {isa = PBXBuildFile; fileRef = F43C52C020F93EE00049445F /* mocks.js */; };
+ F43C52C920F93EE00049445F /* keyboard-utils.js in Resources */ = {isa = PBXBuildFile; fileRef = F43C52C120F93EE00049445F /* keyboard-utils.js */; };
+ F43C52CA20F93EE00049445F /* mousetrap.js in Resources */ = {isa = PBXBuildFile; fileRef = F43C52C320F93EE00049445F /* mousetrap.js */; };
+ F43C52CB20F93EE00049445F /* vimium-scripts.js in Resources */ = {isa = PBXBuildFile; fileRef = F43C52C420F93EE00049445F /* vimium-scripts.js */; };
+ F48E129520F96604007357DF /* settings.js in Resources */ = {isa = PBXBuildFile; fileRef = F48E129420F96603007357DF /* settings.js */; };
+/* End PBXBuildFile section */
+
+/* Begin PBXContainerItemProxy section */
+ F43C529320F93B840049445F /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = F43C526420F93B6C0049445F /* Project object */;
+ proxyType = 1;
+ remoteGlobalIDString = F43C528020F93B840049445F;
+ remoteInfo = extension;
+ };
+/* End PBXContainerItemProxy section */
+
+/* Begin PBXCopyFilesBuildPhase section */
+ F43C529920F93B840049445F /* Embed App Extensions */ = {
+ isa = PBXCopyFilesBuildPhase;
+ buildActionMask = 2147483647;
+ dstPath = "";
+ dstSubfolderSpec = 13;
+ files = (
+ F43C529520F93B840049445F /* extension.appex in Embed App Extensions */,
+ );
+ name = "Embed App Extensions";
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXCopyFilesBuildPhase section */
+
+/* Begin PBXFileReference section */
+ F43C526C20F93B6C0049445F /* vimari.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = vimari.app; sourceTree = BUILT_PRODUCTS_DIR; };
+ F43C526F20F93B6C0049445F /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; };
+ F43C527120F93B6D0049445F /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
+ F43C527420F93B6D0049445F /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; };
+ F43C527620F93B6D0049445F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
+ F43C527720F93B6D0049445F /* vimari.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = vimari.entitlements; sourceTree = ""; };
+ F43C528120F93B840049445F /* extension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = extension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
+ F43C528320F93B840049445F /* Cocoa.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Cocoa.framework; path = System/Library/Frameworks/Cocoa.framework; sourceTree = SDKROOT; };
+ F43C528620F93B840049445F /* SafariExtensionHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SafariExtensionHandler.swift; sourceTree = ""; };
+ F43C528820F93B840049445F /* SafariExtensionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SafariExtensionViewController.swift; sourceTree = ""; };
+ F43C528B20F93B840049445F /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/SafariExtensionViewController.xib; sourceTree = ""; };
+ F43C528D20F93B840049445F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
+ F43C529220F93B840049445F /* extension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = extension.entitlements; sourceTree = ""; };
+ F43C52AB20F93BDF0049445F /* injected.css */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.css; path = injected.css; sourceTree = ""; };
+ F43C52BD20F93EE00049445F /* link-hints.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = "link-hints.js"; sourceTree = ""; };
+ F43C52BE20F93EE00049445F /* global.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = global.js; sourceTree = ""; };
+ F43C52BF20F93EE00049445F /* injected.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = injected.js; sourceTree = ""; };
+ F43C52C020F93EE00049445F /* mocks.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = mocks.js; sourceTree = ""; };
+ F43C52C120F93EE00049445F /* keyboard-utils.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = "keyboard-utils.js"; sourceTree = ""; };
+ F43C52C320F93EE00049445F /* mousetrap.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = mousetrap.js; sourceTree = ""; };
+ F43C52C420F93EE00049445F /* vimium-scripts.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = "vimium-scripts.js"; sourceTree = ""; };
+ F48E129420F96603007357DF /* settings.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = settings.js; sourceTree = ""; };
+/* End PBXFileReference section */
+
+/* Begin PBXFrameworksBuildPhase section */
+ F43C526920F93B6C0049445F /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ F43C527E20F93B840049445F /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ F43C528420F93B840049445F /* Cocoa.framework in Frameworks */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXFrameworksBuildPhase section */
+
+/* Begin PBXGroup section */
+ F43C526320F93B6C0049445F = {
+ isa = PBXGroup;
+ children = (
+ F43C526E20F93B6C0049445F /* vimari */,
+ F43C528520F93B840049445F /* extension */,
+ F43C528220F93B840049445F /* Frameworks */,
+ F43C526D20F93B6C0049445F /* Products */,
+ );
+ sourceTree = "";
+ };
+ F43C526D20F93B6C0049445F /* Products */ = {
+ isa = PBXGroup;
+ children = (
+ F43C526C20F93B6C0049445F /* vimari.app */,
+ F43C528120F93B840049445F /* extension.appex */,
+ );
+ name = Products;
+ sourceTree = "";
+ };
+ F43C526E20F93B6C0049445F /* vimari */ = {
+ isa = PBXGroup;
+ children = (
+ F43C526F20F93B6C0049445F /* AppDelegate.swift */,
+ F43C527120F93B6D0049445F /* Assets.xcassets */,
+ F43C527320F93B6D0049445F /* MainMenu.xib */,
+ F43C527620F93B6D0049445F /* Info.plist */,
+ F43C527720F93B6D0049445F /* vimari.entitlements */,
+ );
+ path = vimari;
+ sourceTree = "";
+ };
+ F43C528220F93B840049445F /* Frameworks */ = {
+ isa = PBXGroup;
+ children = (
+ F43C528320F93B840049445F /* Cocoa.framework */,
+ );
+ name = Frameworks;
+ sourceTree = "";
+ };
+ F43C528520F93B840049445F /* extension */ = {
+ isa = PBXGroup;
+ children = (
+ F43C52AD20F93CAF0049445F /* css */,
+ F43C52BC20F93EE00049445F /* js */,
+ F43C528620F93B840049445F /* SafariExtensionHandler.swift */,
+ F43C528820F93B840049445F /* SafariExtensionViewController.swift */,
+ F43C528A20F93B840049445F /* SafariExtensionViewController.xib */,
+ F43C528D20F93B840049445F /* Info.plist */,
+ F43C529220F93B840049445F /* extension.entitlements */,
+ );
+ path = extension;
+ sourceTree = "";
+ };
+ F43C52AD20F93CAF0049445F /* css */ = {
+ isa = PBXGroup;
+ children = (
+ F43C52AB20F93BDF0049445F /* injected.css */,
+ );
+ path = css;
+ sourceTree = "";
+ };
+ F43C52BC20F93EE00049445F /* js */ = {
+ isa = PBXGroup;
+ children = (
+ F43C52BD20F93EE00049445F /* link-hints.js */,
+ F48E129420F96603007357DF /* settings.js */,
+ F43C52BE20F93EE00049445F /* global.js */,
+ F43C52BF20F93EE00049445F /* injected.js */,
+ F43C52C020F93EE00049445F /* mocks.js */,
+ F43C52C320F93EE00049445F /* mousetrap.js */,
+ F43C52C420F93EE00049445F /* vimium-scripts.js */,
+ F43C52C120F93EE00049445F /* keyboard-utils.js */,
+ );
+ path = js;
+ sourceTree = "";
+ };
+/* End PBXGroup section */
+
+/* Begin PBXNativeTarget section */
+ F43C526B20F93B6C0049445F /* vimari */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = F43C527A20F93B6D0049445F /* Build configuration list for PBXNativeTarget "vimari" */;
+ buildPhases = (
+ F43C526820F93B6C0049445F /* Sources */,
+ F43C526920F93B6C0049445F /* Frameworks */,
+ F43C526A20F93B6C0049445F /* Resources */,
+ F43C529920F93B840049445F /* Embed App Extensions */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ F43C529420F93B840049445F /* PBXTargetDependency */,
+ );
+ name = vimari;
+ productName = vimari;
+ productReference = F43C526C20F93B6C0049445F /* vimari.app */;
+ productType = "com.apple.product-type.application";
+ };
+ F43C528020F93B840049445F /* extension */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = F43C529620F93B840049445F /* Build configuration list for PBXNativeTarget "extension" */;
+ buildPhases = (
+ F43C527D20F93B840049445F /* Sources */,
+ F43C527E20F93B840049445F /* Frameworks */,
+ F43C527F20F93B840049445F /* Resources */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ );
+ name = extension;
+ productName = extension;
+ productReference = F43C528120F93B840049445F /* extension.appex */;
+ productType = "com.apple.product-type.app-extension";
+ };
+/* End PBXNativeTarget section */
+
+/* Begin PBXProject section */
+ F43C526420F93B6C0049445F /* Project object */ = {
+ isa = PBXProject;
+ attributes = {
+ LastSwiftUpdateCheck = 0940;
+ LastUpgradeCheck = 0940;
+ ORGANIZATIONNAME = vimari;
+ TargetAttributes = {
+ F43C526B20F93B6C0049445F = {
+ CreatedOnToolsVersion = 9.4.1;
+ };
+ F43C528020F93B840049445F = {
+ CreatedOnToolsVersion = 9.4.1;
+ };
+ };
+ };
+ buildConfigurationList = F43C526720F93B6C0049445F /* Build configuration list for PBXProject "vimari" */;
+ compatibilityVersion = "Xcode 9.3";
+ developmentRegion = en;
+ hasScannedForEncodings = 0;
+ knownRegions = (
+ en,
+ Base,
+ );
+ mainGroup = F43C526320F93B6C0049445F;
+ productRefGroup = F43C526D20F93B6C0049445F /* Products */;
+ projectDirPath = "";
+ projectRoot = "";
+ targets = (
+ F43C526B20F93B6C0049445F /* vimari */,
+ F43C528020F93B840049445F /* extension */,
+ );
+ };
+/* End PBXProject section */
+
+/* Begin PBXResourcesBuildPhase section */
+ F43C526A20F93B6C0049445F /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ F43C527220F93B6D0049445F /* Assets.xcassets in Resources */,
+ F43C527520F93B6D0049445F /* MainMenu.xib in Resources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ F43C527F20F93B840049445F /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ F43C52C620F93EE00049445F /* global.js in Resources */,
+ F48E129520F96604007357DF /* settings.js in Resources */,
+ F43C52C520F93EE00049445F /* link-hints.js in Resources */,
+ F43C52C920F93EE00049445F /* keyboard-utils.js in Resources */,
+ F43C52CA20F93EE00049445F /* mousetrap.js in Resources */,
+ F43C52AC20F93BDF0049445F /* injected.css in Resources */,
+ F43C52CB20F93EE00049445F /* vimium-scripts.js in Resources */,
+ F43C52C720F93EE00049445F /* injected.js in Resources */,
+ F43C52C820F93EE00049445F /* mocks.js in Resources */,
+ F43C528C20F93B840049445F /* SafariExtensionViewController.xib in Resources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXResourcesBuildPhase section */
+
+/* Begin PBXSourcesBuildPhase section */
+ F43C526820F93B6C0049445F /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ F43C527020F93B6C0049445F /* AppDelegate.swift in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ F43C527D20F93B840049445F /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ F43C528920F93B840049445F /* SafariExtensionViewController.swift in Sources */,
+ F43C528720F93B840049445F /* SafariExtensionHandler.swift in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXSourcesBuildPhase section */
+
+/* Begin PBXTargetDependency section */
+ F43C529420F93B840049445F /* PBXTargetDependency */ = {
+ isa = PBXTargetDependency;
+ target = F43C528020F93B840049445F /* extension */;
+ targetProxy = F43C529320F93B840049445F /* PBXContainerItemProxy */;
+ };
+/* End PBXTargetDependency section */
+
+/* Begin PBXVariantGroup section */
+ F43C527320F93B6D0049445F /* MainMenu.xib */ = {
+ isa = PBXVariantGroup;
+ children = (
+ F43C527420F93B6D0049445F /* Base */,
+ );
+ name = MainMenu.xib;
+ sourceTree = "";
+ };
+ F43C528A20F93B840049445F /* SafariExtensionViewController.xib */ = {
+ isa = PBXVariantGroup;
+ children = (
+ F43C528B20F93B840049445F /* Base */,
+ );
+ name = SafariExtensionViewController.xib;
+ sourceTree = "";
+ };
+/* End PBXVariantGroup section */
+
+/* Begin XCBuildConfiguration section */
+ F43C527820F93B6D0049445F /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
+ CLANG_CXX_LIBRARY = "libc++";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_ENABLE_OBJC_WEAK = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_COMMA = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_STRICT_PROTOTYPES = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
+ CLANG_WARN_UNREACHABLE_CODE = YES;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ CODE_SIGN_IDENTITY = "Mac Developer";
+ COPY_PHASE_STRIP = NO;
+ DEBUG_INFORMATION_FORMAT = dwarf;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ ENABLE_TESTABILITY = YES;
+ GCC_C_LANGUAGE_STANDARD = gnu11;
+ GCC_DYNAMIC_NO_PIC = NO;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_OPTIMIZATION_LEVEL = 0;
+ GCC_PREPROCESSOR_DEFINITIONS = (
+ "DEBUG=1",
+ "$(inherited)",
+ );
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNDECLARED_SELECTOR = YES;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ MACOSX_DEPLOYMENT_TARGET = 10.13;
+ MTL_ENABLE_DEBUG_INFO = YES;
+ ONLY_ACTIVE_ARCH = YES;
+ SDKROOT = macosx;
+ SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
+ SWIFT_OPTIMIZATION_LEVEL = "-Onone";
+ };
+ name = Debug;
+ };
+ F43C527920F93B6D0049445F /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
+ CLANG_CXX_LIBRARY = "libc++";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_ENABLE_OBJC_WEAK = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_COMMA = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_STRICT_PROTOTYPES = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
+ CLANG_WARN_UNREACHABLE_CODE = YES;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ CODE_SIGN_IDENTITY = "Mac Developer";
+ COPY_PHASE_STRIP = NO;
+ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
+ ENABLE_NS_ASSERTIONS = NO;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ GCC_C_LANGUAGE_STANDARD = gnu11;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNDECLARED_SELECTOR = YES;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ MACOSX_DEPLOYMENT_TARGET = 10.13;
+ MTL_ENABLE_DEBUG_INFO = NO;
+ SDKROOT = macosx;
+ SWIFT_COMPILATION_MODE = wholemodule;
+ SWIFT_OPTIMIZATION_LEVEL = "-O";
+ };
+ name = Release;
+ };
+ F43C527B20F93B6D0049445F /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ CODE_SIGN_ENTITLEMENTS = vimari/vimari.entitlements;
+ CODE_SIGN_STYLE = Automatic;
+ COMBINE_HIDPI_IMAGES = YES;
+ DEVELOPMENT_TEAM = SK24TBMYCC;
+ INFOPLIST_FILE = vimari/Info.plist;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/../Frameworks",
+ );
+ PRODUCT_BUNDLE_IDENTIFIER = org.vimari.vimari;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_VERSION = 4.0;
+ };
+ name = Debug;
+ };
+ F43C527C20F93B6D0049445F /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ CODE_SIGN_ENTITLEMENTS = vimari/vimari.entitlements;
+ CODE_SIGN_STYLE = Automatic;
+ COMBINE_HIDPI_IMAGES = YES;
+ DEVELOPMENT_TEAM = SK24TBMYCC;
+ INFOPLIST_FILE = vimari/Info.plist;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/../Frameworks",
+ );
+ PRODUCT_BUNDLE_IDENTIFIER = org.vimari.vimari;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_VERSION = 4.0;
+ };
+ name = Release;
+ };
+ F43C529720F93B840049445F /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ CODE_SIGN_ENTITLEMENTS = extension/extension.entitlements;
+ CODE_SIGN_STYLE = Automatic;
+ DEVELOPMENT_TEAM = SK24TBMYCC;
+ INFOPLIST_FILE = extension/Info.plist;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/../Frameworks",
+ "@executable_path/../../../../Frameworks",
+ );
+ PRODUCT_BUNDLE_IDENTIFIER = org.vimari.vimari.extension;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SKIP_INSTALL = YES;
+ SWIFT_VERSION = 4.0;
+ };
+ name = Debug;
+ };
+ F43C529820F93B840049445F /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ CODE_SIGN_ENTITLEMENTS = extension/extension.entitlements;
+ CODE_SIGN_STYLE = Automatic;
+ DEVELOPMENT_TEAM = SK24TBMYCC;
+ INFOPLIST_FILE = extension/Info.plist;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/../Frameworks",
+ "@executable_path/../../../../Frameworks",
+ );
+ PRODUCT_BUNDLE_IDENTIFIER = org.vimari.vimari.extension;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SKIP_INSTALL = YES;
+ SWIFT_VERSION = 4.0;
+ };
+ name = Release;
+ };
+/* End XCBuildConfiguration section */
+
+/* Begin XCConfigurationList section */
+ F43C526720F93B6C0049445F /* Build configuration list for PBXProject "vimari" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ F43C527820F93B6D0049445F /* Debug */,
+ F43C527920F93B6D0049445F /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ F43C527A20F93B6D0049445F /* Build configuration list for PBXNativeTarget "vimari" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ F43C527B20F93B6D0049445F /* Debug */,
+ F43C527C20F93B6D0049445F /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ F43C529620F93B840049445F /* Build configuration list for PBXNativeTarget "extension" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ F43C529720F93B840049445F /* Debug */,
+ F43C529820F93B840049445F /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+/* End XCConfigurationList section */
+ };
+ rootObject = F43C526420F93B6C0049445F /* Project object */;
+}
diff --git a/app_extension/vimari/vimari.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/app_extension/vimari/vimari.xcodeproj/project.xcworkspace/contents.xcworkspacedata
new file mode 100644
index 0000000..3a8c8fd
--- /dev/null
+++ b/app_extension/vimari/vimari.xcodeproj/project.xcworkspace/contents.xcworkspacedata
@@ -0,0 +1,7 @@
+
+
+
+
+
diff --git a/app_extension/vimari/vimari.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/app_extension/vimari/vimari.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
new file mode 100644
index 0000000..18d9810
--- /dev/null
+++ b/app_extension/vimari/vimari.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
@@ -0,0 +1,8 @@
+
+
+
+
+ IDEDidComputeMac32BitWarning
+
+
+
diff --git a/app_extension/vimari/vimari/AppDelegate.swift b/app_extension/vimari/vimari/AppDelegate.swift
new file mode 100644
index 0000000..2a32d36
--- /dev/null
+++ b/app_extension/vimari/vimari/AppDelegate.swift
@@ -0,0 +1,27 @@
+//
+// AppDelegate.swift
+// vimari
+//
+// Created by Simon on 2018-07-13.
+// Copyright © 2018 vimari. All rights reserved.
+//
+
+import Cocoa
+
+@NSApplicationMain
+class AppDelegate: NSObject, NSApplicationDelegate {
+
+ @IBOutlet weak var window: NSWindow!
+
+
+ func applicationDidFinishLaunching(_ aNotification: Notification) {
+ // Insert code here to initialize your application
+ }
+
+ func applicationWillTerminate(_ aNotification: Notification) {
+ // Insert code here to tear down your application
+ }
+
+
+}
+
diff --git a/app_extension/vimari/vimari/Assets.xcassets/AppIcon.appiconset/Contents.json b/app_extension/vimari/vimari/Assets.xcassets/AppIcon.appiconset/Contents.json
new file mode 100644
index 0000000..2db2b1c
--- /dev/null
+++ b/app_extension/vimari/vimari/Assets.xcassets/AppIcon.appiconset/Contents.json
@@ -0,0 +1,58 @@
+{
+ "images" : [
+ {
+ "idiom" : "mac",
+ "size" : "16x16",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "mac",
+ "size" : "16x16",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "mac",
+ "size" : "32x32",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "mac",
+ "size" : "32x32",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "mac",
+ "size" : "128x128",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "mac",
+ "size" : "128x128",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "mac",
+ "size" : "256x256",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "mac",
+ "size" : "256x256",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "mac",
+ "size" : "512x512",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "mac",
+ "size" : "512x512",
+ "scale" : "2x"
+ }
+ ],
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ }
+}
\ No newline at end of file
diff --git a/app_extension/vimari/vimari/Assets.xcassets/Contents.json b/app_extension/vimari/vimari/Assets.xcassets/Contents.json
new file mode 100644
index 0000000..da4a164
--- /dev/null
+++ b/app_extension/vimari/vimari/Assets.xcassets/Contents.json
@@ -0,0 +1,6 @@
+{
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ }
+}
\ No newline at end of file
diff --git a/app_extension/vimari/vimari/Base.lproj/MainMenu.xib b/app_extension/vimari/vimari/Base.lproj/MainMenu.xib
new file mode 100644
index 0000000..a53c414
--- /dev/null
+++ b/app_extension/vimari/vimari/Base.lproj/MainMenu.xib
@@ -0,0 +1,692 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app_extension/vimari/vimari/Info.plist b/app_extension/vimari/vimari/Info.plist
new file mode 100644
index 0000000..1a0bd05
--- /dev/null
+++ b/app_extension/vimari/vimari/Info.plist
@@ -0,0 +1,32 @@
+
+
+
+
+ CFBundleDevelopmentRegion
+ $(DEVELOPMENT_LANGUAGE)
+ CFBundleExecutable
+ $(EXECUTABLE_NAME)
+ CFBundleIconFile
+
+ CFBundleIdentifier
+ $(PRODUCT_BUNDLE_IDENTIFIER)
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleName
+ $(PRODUCT_NAME)
+ CFBundlePackageType
+ APPL
+ CFBundleShortVersionString
+ 1.0
+ CFBundleVersion
+ 1
+ LSMinimumSystemVersion
+ $(MACOSX_DEPLOYMENT_TARGET)
+ NSHumanReadableCopyright
+ Copyright © 2018 vimari. All rights reserved.
+ NSMainNibFile
+ MainMenu
+ NSPrincipalClass
+ NSApplication
+
+
diff --git a/app_extension/vimari/vimari/vimari.entitlements b/app_extension/vimari/vimari/vimari.entitlements
new file mode 100644
index 0000000..f2ef3ae
--- /dev/null
+++ b/app_extension/vimari/vimari/vimari.entitlements
@@ -0,0 +1,10 @@
+
+
+
+
+ com.apple.security.app-sandbox
+
+ com.apple.security.files.user-selected.read-only
+
+
+
diff --git a/docs/safari_12.md b/docs/safari_12.md
new file mode 100644
index 0000000..dce79dc
--- /dev/null
+++ b/docs/safari_12.md
@@ -0,0 +1,33 @@
+# Installation notes for Safari version 12
+
+A new version of macOS is being released, macOS Mojave, and it's expected to
+have a stable release out September or October of 2018. With that new version
+comes Safari 12, and a [completely new way of dealing with browser
+extensions](https://developer.apple.com/documentation/safariservices/safari_app_extensions).
+ [We have had some issues](./crowdfunding.md) related to releasing new version
+of this extension, but they are now fixed and it's possible to install a version
+of vimari for Safari 12.
+
+## How to install
+**Note: We are currently working on improving this installation flow, as well
+as the extension itself. Because vimari now has to be released as a _Safari
+App Extension_ instead of a _Safari Extension_ it requires some fundamental
+changes to the code. We can't guarantee that all the features work in
+this version. It's a learning process for us so bare with us.**
+
+1. Clone this repo
+ ```sh
+ $ git clone git@github.com:guyht/vimari.git
+ ```
+2. Open the Swift project located at `app_extension/vimari` in Xcode
+3. If you want different settings than the default ones, make your changes in
+ `settings.js`. You can always come back later to change the settings again.
+4. Build and run the project (`⌘ + r`)
+5. An empty GUI box will show up - ignore it (we'll fix it later). Go to
+ Safari and open up settings (`⌘ + ,`). Go to _Extensions_ and you should
+ see **vimari** in the list of extensions. Enable it.
+6. You may now press stop in Xcode and close Xcode. The extension will be
+ available even if you restart Safari.
+
+This was tested on High Sierra with Safari Technology Preview (version 12). Let
+us know if something is not working for you.