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** - -![Funding progress](http://progressed.io/bar/100) - -**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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Default + + + + + + + Left to Right + + + + + + + Right to Left + + + + + + + + + + + Default + + + + + + + Left to Right + + + + + + + Right to Left + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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.