diff --git a/CHANGELOG.md b/CHANGELOG.md
index cdd42f6..04a5719 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,9 @@ Changelog
-------------
### Unreleased
+* Add user customisation (based on the work of @nieldm [#163](https://github.com/televator-apps/vimari/pull/163)).
+* Update Vimari interface to allow users access to their configuration.
+* Remove `closeTabReverse` action.
* Normal mode now isolates keybindings from the underlying website, this means that to interact with the underlying website you need to enter insert mode.
* You can enter insert mode by pressing i and exit the mode by pressing esc.
diff --git a/Vimari Extension/ConfigurationModel.swift b/Vimari Extension/ConfigurationModel.swift
new file mode 100644
index 0000000..879b677
--- /dev/null
+++ b/Vimari Extension/ConfigurationModel.swift
@@ -0,0 +1,118 @@
+//
+// ConfigurationModel.swift
+// Vimari Extension
+//
+// Created by Daniel Mendez on 12/15/19.
+// Copyright © 2019 net.televator. All rights reserved.
+//
+
+protocol ConfigurationModelProtocol {
+ func editConfigFile() throws
+ func resetConfigFile() throws
+ func getDefaultSettings() throws -> [String: Any]
+ func getUserSettings() throws -> [String : Any]
+}
+
+import Foundation
+import SafariServices
+
+class ConfigurationModel: ConfigurationModelProtocol {
+
+ private enum Constant {
+ static let settingsFileName = "defaultSettings"
+ static let userSettingsFileName = "userSettings"
+ static let defaultEditor = "TextEdit"
+ }
+
+ let userSettingsUrl: URL = FileManager.documentDirectoryURL
+ .appendingPathComponent(Constant.userSettingsFileName)
+ .appendingPathExtension("json")
+
+ func editConfigFile() throws {
+ let settingsFilePath = try findOrCreateUserSettings()
+ NSWorkspace.shared.openFile(
+ settingsFilePath,
+ withApplication: Constant.defaultEditor
+ )
+ }
+
+ func resetConfigFile() throws {
+ let settingsFilePath = try overwriteUserSettings()
+ NSWorkspace.shared.openFile(
+ settingsFilePath,
+ withApplication: Constant.defaultEditor
+ )
+ }
+
+ func getDefaultSettings() throws -> [String : Any] {
+ return try loadSettings(fromFile: Constant.settingsFileName)
+ }
+
+ func getUserSettings() throws -> [String : Any] {
+ let userFilePath = try findOrCreateUserSettings()
+ let urlSettingsFile = URL(fileURLWithPath: userFilePath)
+ let settingsData = try Data(contentsOf: urlSettingsFile)
+ return try settingsData.toJSONObject()
+ }
+
+ private func loadSettings(fromFile file: String) throws -> [String : Any] {
+ let settingsData = try Bundle.main.getJSONData(from: file)
+ return try settingsData.toJSONObject()
+ }
+
+ private func findOrCreateUserSettings() throws -> String {
+ let url = userSettingsUrl
+ let urlString = url.path
+ if FileManager.default.fileExists(atPath: urlString) {
+ return urlString
+ }
+ let data = try Bundle.main.getJSONData(from: Constant.settingsFileName)
+ try data.write(to: url)
+ return urlString
+ }
+
+ private func overwriteUserSettings() throws -> String {
+ let url = userSettingsUrl
+ let urlString = userSettingsUrl.path
+ let data = try Bundle.main.getJSONData(from: Constant.settingsFileName)
+ try data.write(to: url)
+ return urlString
+ }
+}
+
+enum DataError: Error {
+ case unableToParse
+ case notFound
+}
+
+private extension Data {
+ func toJSONObject() throws -> [String: Any] {
+ let serialized = try JSONSerialization.jsonObject(with: self, options: [])
+ guard let result = serialized as? [String: Any] else {
+ throw DataError.unableToParse
+ }
+ return result
+ }
+}
+
+private extension Bundle {
+ func getJSONPath(for file: String) throws -> String {
+ guard let result = self.path(forResource: file, ofType: ".json") else {
+ throw DataError.notFound
+ }
+ return result
+ }
+
+ func getJSONData(from file: String) throws -> Data {
+ let settingsPath = try self.getJSONPath(for: file)
+ let urlSettingsFile = URL(fileURLWithPath: settingsPath)
+ return try Data(contentsOf: urlSettingsFile)
+ }
+}
+
+private extension FileManager {
+ static var documentDirectoryURL: URL {
+ let documentDirectoryURL = try! FileManager.default.url(for: .applicationSupportDirectory, in: .userDomainMask, appropriateFor: nil, create: false)
+ return documentDirectoryURL
+ }
+}
diff --git a/Vimari Extension/Info.plist b/Vimari Extension/Info.plist
index 455d752..8a2de28 100644
--- a/Vimari Extension/Info.plist
+++ b/Vimari Extension/Info.plist
@@ -32,7 +32,7 @@
Script
- settings.js
+ SafariExtensionCommunicator.js
Script
diff --git a/Vimari Extension/SafariExtensionHandler.swift b/Vimari Extension/SafariExtensionHandler.swift
index 1e8eab9..f238730 100644
--- a/Vimari Extension/SafariExtensionHandler.swift
+++ b/Vimari Extension/SafariExtensionHandler.swift
@@ -6,6 +6,12 @@ enum ActionType: String {
case tabForward
case tabBackward
case closeTab
+ case updateSettings
+}
+
+enum InputAction: String {
+ case openSettings
+ case resetSettings
}
enum TabDirection: String {
@@ -13,22 +19,38 @@ enum TabDirection: String {
case backward
}
-func mod(_ a: Int, _ n: Int) -> Int {
- // https://stackoverflow.com/questions/41180292/negative-number-modulo-in-swift
- precondition(n > 0, "modulus must be positive")
- let r = a % n
- return r >= 0 ? r : r + n
-}
-
class SafariExtensionHandler: SFSafariExtensionHandler {
- override func messageReceived(withName messageName: String, from page: SFSafariPage, userInfo: [String: Any]?) {
- guard let action = ActionType(rawValue: messageName) else {
- NSLog("Received message with unsupported type: \(messageName)")
- return
+
+ private enum Constant {
+ static let mainAppName = "Vimari"
+ static let newTabPageURL = "https://duckduckgo.com" //Try it :D
+ }
+
+ let configuration: ConfigurationModelProtocol = ConfigurationModel()
+
+ //MARK: Overrides
+
+ // This method handles messages from the Vimari App (located /Vimari in the repository)
+ override func messageReceivedFromContainingApp(withName messageName: String, userInfo: [String : Any]? = nil) {
+ do {
+ switch InputAction(rawValue: messageName) {
+ case .openSettings:
+ try configuration.editConfigFile()
+ case .resetSettings:
+ try configuration.resetConfigFile()
+ case .none:
+ NSLog("Input not supported " + messageName)
+ }
+ } catch {
+ NSLog(error.localizedDescription)
}
+ }
+
+ // This method handles messages from the extension (in the browser page)
+ override func messageReceived(withName messageName: String, from page: SFSafariPage, userInfo: [String: Any]?) {
NSLog("Received message: \(messageName)")
- switch action {
+ switch ActionType(rawValue: messageName) {
case .openLinkInTab:
let url = URL(string: userInfo?["url"] as! String)
openInNewTab(url: url!)
@@ -40,10 +62,31 @@ class SafariExtensionHandler: SFSafariExtensionHandler {
changeTab(withDirection: .backward, from: page)
case .closeTab:
closeTab(from: page)
+ case .updateSettings:
+ updateSettings(page: page)
+ case .none:
+ NSLog("Received message with unsupported type: \(messageName)")
}
}
- func openInNewTab(url: URL) {
+ override func toolbarItemClicked(in _: SFSafariWindow) {
+ // This method will be called when your toolbar item is clicked.
+ NSLog("The extension's toolbar item was clicked")
+ NSWorkspace.shared.launchApplication(Constant.mainAppName)
+ }
+
+ override func validateToolbarItem(in _: 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
+ }
+
+ // MARK: Tabs Methods
+
+ private func openInNewTab(url: URL) {
SFSafariApplication.getActiveWindow { activeWindow in
activeWindow?.openTab(with: url, makeActiveIfPossible: false, completionHandler: { _ in
// Perform some action here after the page loads
@@ -51,9 +94,12 @@ class SafariExtensionHandler: SFSafariExtensionHandler {
}
}
- func openNewTab() {
- // Ideally this URL would be something that represents an empty tab better than localhost
- let url = URL(string: "http://localhost")!
+ private func openNewTab() {
+ var newPageUrl: String? = getSetting("openTabUrl") as? String
+ if newPageUrl == nil || newPageUrl!.isEmpty {
+ newPageUrl = Constant.newTabPageURL
+ }
+ let url = URL(string: newPageUrl!)!
SFSafariApplication.getActiveWindow { activeWindow in
activeWindow?.openTab(with: url, makeActiveIfPossible: true, completionHandler: { _ in
// Perform some action here after the page loads
@@ -61,10 +107,12 @@ class SafariExtensionHandler: SFSafariExtensionHandler {
}
}
- func changeTab(withDirection direction: TabDirection, from page: SFSafariPage, completionHandler: (() -> Void)? = nil ) {
- page.getContainingTab(completionHandler: { currentTab in
- currentTab.getContainingWindow(completionHandler: { window in
- window?.getAllTabs(completionHandler: { tabs in
+ private func changeTab(withDirection direction: TabDirection, from page: SFSafariPage, completionHandler: (() -> Void)? = nil ) {
+ page.getContainingTab() { currentTab in
+ // Using .currentWindow instead of .containingWindow, this prevents the window being nil in the case of a pinned tab.
+ self.currentWindow(from: page) { window in
+ window?.getAllTabs() { tabs in
+ tabs.forEach { tab in NSLog(tab.description) }
if let currentIndex = tabs.firstIndex(of: currentTab) {
let indexStep = direction == TabDirection.forward ? 1 : -1
@@ -72,34 +120,98 @@ class SafariExtensionHandler: SFSafariExtensionHandler {
// % calculates the remainder, not the modulus, so we need a
// custom function.
let newIndex = mod(currentIndex + indexStep, tabs.count)
-
+
tabs[newIndex].activate(completionHandler: completionHandler ?? {})
-
+
}
- })
- })
- })
+ }
+ }
+ }
+ }
+
+ /**
+ Returns the containing window of a SFSafariPage, if not available default to the current active window.
+ */
+ private func currentWindow(from page: SFSafariPage, completionHandler: @escaping ((SFSafariWindow?) -> Void)) {
+ page.getContainingTab() { $0.getContainingWindow() { window in
+ if window != nil {
+ return completionHandler(window)
+ } else {
+ SFSafariApplication.getActiveWindow() { window in
+ return completionHandler(window)
+ }
+ }
+ }}
}
- func closeTab(from page: SFSafariPage) {
+ private func closeTab(from page: SFSafariPage) {
page.getContainingTab {
tab in
tab.close()
}
}
+
+ // MARK: Settings
- override func toolbarItemClicked(in _: SFSafariWindow) {
- // This method will be called when your toolbar item is clicked.
- NSLog("The extension's toolbar item was clicked")
- NSWorkspace.shared.launchApplication("Vimari")
+ private func getSetting(_ settingKey: String) -> Any? {
+ do {
+ let settings = try configuration.getUserSettings()
+ return settings[settingKey]
+ } catch {
+ NSLog("Was not able to retrieve the user settings\n\(error.localizedDescription)")
+ return nil
+ }
+ }
+
+ private func updateSettings(page: SFSafariPage) {
+ do {
+ let settings: [String: Any]
+ if let userSettings = try? configuration.getUserSettings() {
+ settings = userSettings
+ } else {
+ settings = try configuration.getDefaultSettings()
+ }
+ page.dispatch(settings: settings)
+ } catch {
+ NSLog(error.localizedDescription)
+ }
+ }
+
+ private func fallbackSettings(page: SFSafariPage) {
+ do {
+ let settings = try configuration.getUserSettings()
+ page.dispatch(settings: settings)
+ } catch {
+ NSLog(error.localizedDescription)
+ }
}
+}
- override func validateToolbarItem(in _: 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, "")
+// MARK: Helpers
+
+private func mod(_ a: Int, _ n: Int) -> Int {
+ // https://stackoverflow.com/questions/41180292/negative-number-modulo-in-swift
+ precondition(n > 0, "modulus must be positive")
+ let r = a % n
+ return r >= 0 ? r : r + n
+}
+
+private extension SFSafariPage {
+ func dispatch(settings: [String: Any]) {
+ self.dispatchMessageToScript(
+ withName: "updateSettingsEvent",
+ userInfo: settings
+ )
}
+}
- override func popoverViewController() -> SFSafariExtensionViewController {
- return SafariExtensionViewController.shared
+private extension SFSafariApplication {
+ static func getActivePage(completionHandler: @escaping (SFSafariPage?) -> Void) {
+ SFSafariApplication.getActiveWindow {
+ $0?.getActiveTab {
+ $0?.getActivePage(completionHandler: completionHandler)
+ }
+ }
}
}
+
diff --git a/Vimari Extension/js/SafariExtensionCommunicator.js b/Vimari Extension/js/SafariExtensionCommunicator.js
new file mode 100644
index 0000000..9afe592
--- /dev/null
+++ b/Vimari Extension/js/SafariExtensionCommunicator.js
@@ -0,0 +1,30 @@
+var SafariExtensionCommunicator = (function (msgHandler) {
+ 'use strict'
+ var publicAPI = {}
+
+ // Connect the provided message handler to the received messages.
+ safari.self.addEventListener("message", msgHandler)
+
+ var sendMessage = function(msgName) {
+ safari.extension.dispatchMessage(msgName)
+ }
+
+ publicAPI.requestSettingsUpdate = function() {
+ sendMessage("updateSettings")
+ }
+ publicAPI.requestNewTab = function() {
+ sendMessage("openNewTab")
+ }
+ publicAPI.requestTabForward = function() {
+ sendMessage("tabForward")
+ }
+ publicAPI.requestTabBackward = function() {
+ sendMessage("tabBackward")
+ }
+ publicAPI.requestCloseTab = function () {
+ sendMessage("closeTab")
+ }
+
+ // Return only the public methods.
+ return publicAPI;
+});
diff --git a/Vimari Extension/js/global.js b/Vimari Extension/js/global.js
deleted file mode 100644
index 695c50f..0000000
--- a/Vimari Extension/js/global.js
+++ /dev/null
@@ -1,155 +0,0 @@
-// 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/Vimari Extension/js/injected.js b/Vimari Extension/js/injected.js
index ff68bd2..406a3d6 100644
--- a/Vimari Extension/js/injected.js
+++ b/Vimari Extension/js/injected.js
@@ -25,7 +25,8 @@ var topWindow = (window.top === window),
extensionActive = true,
insertMode = false,
shiftKeyToggle = false,
- hudDuration = 5000;
+ hudDuration = 5000,
+ extensionCommunicator = SafariExtensionCommunicator(messageHandler);
var actionMap = {
'hintToggle' : function() {
@@ -37,10 +38,10 @@ var actionMap = {
activateLinkHintsMode(true, false); },
'tabForward':
- function() { safari.extension.dispatchMessage("tabForward"); },
+ function() { extensionCommunicator.requestTabForward(); },
'tabBack':
- function() { safari.extension.dispatchMessage("tabBackward"); },
+ function() { extensionCommunicator.requestTabBackward() },
'scrollDown':
function() { window.scrollBy(0, settings.scrollSize); },
@@ -64,13 +65,10 @@ var actionMap = {
function() { window.location.reload(); },
'openTab':
- function() { openNewTab(); },
+ function() { extensionCommunicator.requestNewTab(); },
'closeTab':
- function() { safari.extension.dispatchMessage("closeTab"); },
-
- 'closeTabReverse':
- function() { safari.self.tab.dispatchMessage('closeTab', 1); },
+ function() { extensionCommunicator.requestCloseTab(); },
'scrollDownHalfPage':
function() { window.scrollBy(0, window.innerHeight / 2); },
@@ -106,10 +104,14 @@ Mousetrap.prototype.stopCallback = function(e, element, combo) {
};
// Set up key codes to event handlers
-function bindKeyCodesToActions() {
+function bindKeyCodesToActions(settings) {
+ var excludedUrl = false
+ if (typeof settings != "undefined") {
+ excludedUrl = isExcludedUrl(settings.excludedUrls, document.URL)
+ }
// Only add if topWindow... not iframe
- if (topWindow && !isExcludedUrl(settings.excludedUrls, document.URL)) {
- Mousetrap.reset();
+ Mousetrap.reset();
+ if (topWindow && !excludedUrl) {
Mousetrap.bind('esc', enterNormalMode);
Mousetrap.bind('ctrl+[', enterNormalMode);
Mousetrap.bind('i', enterInsertMode);
@@ -181,10 +183,13 @@ function isActiveElementEditable() {
// 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];
+ if (typeof settings != 'undefined') {
+ if(settings.modifier) {
+ keyCode += settings.modifier + '+';
+ }
+ return keyCode + settings["bindings"][actionName];
+ }
+ return keyCode;
}
@@ -224,19 +229,10 @@ function isEmbed(element) { return ["EMBED", "OBJECT"].indexOf(element.tagName)
// 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;
- }
+function messageHandler(event){
+ if (event.name == "updateSettingsEvent") {
+ setSettings(event.message);
+ }
}
/*
@@ -244,25 +240,13 @@ function handleMessage(msg) {
*/
function setSettings(msg) {
settings = msg;
- activateExtension();
-}
-
-/*
- * Enable or disable the extension on this tab
- */
-function setActive(msg) {
- extensionActive = msg;
- if(msg) {
- activateExtension();
- } else {
- unbindKeyCodes();
- }
+ activateExtension(settings);
}
-function activateExtension() {
+function activateExtension(settings) {
// Stop keydown propagation
document.addEventListener("keydown", stopSitePropagation(), true);
- bindKeyCodesToActions();
+ bindKeyCodesToActions(settings);
}
function isExcludedUrl(storedExcludedUrls, currentUrl) {
@@ -275,7 +259,7 @@ function isExcludedUrl(storedExcludedUrls, currentUrl) {
for (_i = 0, _len = excludedUrls.length; _i < _len; _i++) {
url = excludedUrls[_i];
formattedUrl = stripProtocolAndWww(url);
- formattedUrl = formattedUrl.toLowerCase();
+ formattedUrl = formattedUrl.toLowerCase().trim();
regexp = new RegExp('((.*)?(' + formattedUrl + ')+(.*))');
if (currentUrl.toLowerCase().match(regexp)) {
return true;
@@ -284,11 +268,6 @@ function isExcludedUrl(storedExcludedUrls, currentUrl) {
return false;
}
-function openNewTab() {
- console.log("-- Open new empty tab --");
- safari.extension.dispatchMessage("openNewTab");
-}
-
// These formations removes the protocol and www so that
// the regexp can catch less AND more specific excluded
// domains than the current URL.
@@ -302,13 +281,20 @@ function stripProtocolAndWww(url) {
return url;
}
-// Bootstrap extension
-setSettings(window.getSettings());
// Add event listener
-// safari.self.addEventListener("message", handleMessage, false);
-// Retrieve settings
-// safari.self.tab.dispatchMessage('getSettings', '');
+function inIframe () {
+ try {
+ return window.self !== window.top;
+ }
+ catch (e) {
+ return true;
+ }
+}
+if(!inIframe()){
+ extensionCommunicator.requestSettingsUpdate()
+}
+
// Export to make it testable
window.isExcludedUrl = isExcludedUrl;
window.stripProtocolAndWww = stripProtocolAndWww;
diff --git a/Vimari Extension/js/settings.js b/Vimari Extension/js/settings.js
deleted file mode 100644
index 1b931cc..0000000
--- a/Vimari Extension/js/settings.js
+++ /dev/null
@@ -1,33 +0,0 @@
-function getSettings() {
- return {
- 'modifier': undefined,
- 'excludedUrls': '',
-
- 'hintToggle': 'f',
- 'newTabHintToggle': 'shift+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': 't',
- };
-}
-
-window.getSettings = getSettings;
diff --git a/Vimari Extension/json/defaultSettings.json b/Vimari Extension/json/defaultSettings.json
new file mode 100644
index 0000000..06fb864
--- /dev/null
+++ b/Vimari Extension/json/defaultSettings.json
@@ -0,0 +1,27 @@
+{
+ "excludedUrls": "",
+ "linkHintCharacters": "asdfjklqwerzxc",
+ "detectByCursorStyle": false,
+ "scrollSize": 50,
+ "openTabUrl": "https://duckduckgo.com/",
+ "modifier": "",
+ "bindings": {
+ "hintToggle": "f",
+ "newTabHintToggle": "shift+f",
+ "scrollUp": "k",
+ "scrollDown": "j",
+ "scrollLeft": "h",
+ "scrollRight": "l",
+ "scrollUpHalfPage": "u",
+ "scrollDownHalfPage": "d",
+ "goToPageTop": "g g",
+ "goToPageBottom": "shift+g",
+ "goBack": "shift+h",
+ "goForward": "shift+l",
+ "reload": "r",
+ "tabForward": "w",
+ "tabBack": "q",
+ "closeTab": "x",
+ "openTab": "t"
+ }
+}
diff --git a/Vimari.xcodeproj/project.pbxproj b/Vimari.xcodeproj/project.pbxproj
index e5f95cb..56635a7 100644
--- a/Vimari.xcodeproj/project.pbxproj
+++ b/Vimari.xcodeproj/project.pbxproj
@@ -7,6 +7,9 @@
objects = {
/* Begin PBXBuildFile section */
+ 65E444F324CC3A1B008EA1DC /* SafariExtensionCommunicator.js in Resources */ = {isa = PBXBuildFile; fileRef = 65E444F224CC3A1B008EA1DC /* SafariExtensionCommunicator.js */; };
+ B1E3C17023A65ED400A56807 /* ConfigurationModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1E3C16F23A65ED400A56807 /* ConfigurationModel.swift */; };
+ B1FD3B9923A588DE00677A52 /* defaultSettings.json in Resources */ = {isa = PBXBuildFile; fileRef = B1FD3B9823A588DE00677A52 /* defaultSettings.json */; };
E320D0662337FC9800F2C3A4 /* Credits.rtf in Resources */ = {isa = PBXBuildFile; fileRef = E320D0652337FC9800F2C3A4 /* Credits.rtf */; };
E320D06823397C5C00F2C3A4 /* CHANGELOG.md in Resources */ = {isa = PBXBuildFile; fileRef = E320D06723397C5C00F2C3A4 /* CHANGELOG.md */; };
E380F24A2331806400640547 /* Vimari.entitlements in Resources */ = {isa = PBXBuildFile; fileRef = E380F2492331806400640547 /* Vimari.entitlements */; };
@@ -21,13 +24,11 @@
E380F2672331806500640547 /* SafariExtensionViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = E380F2652331806500640547 /* SafariExtensionViewController.xib */; };
E380F26C2331806500640547 /* ToolbarItemIcon.pdf in Resources */ = {isa = PBXBuildFile; fileRef = E380F26B2331806500640547 /* ToolbarItemIcon.pdf */; };
E380F283233183EF00640547 /* link-hints.js in Resources */ = {isa = PBXBuildFile; fileRef = E380F278233183EE00640547 /* link-hints.js */; };
- E380F284233183EF00640547 /* global.js in Resources */ = {isa = PBXBuildFile; fileRef = E380F279233183EE00640547 /* global.js */; };
E380F285233183EF00640547 /* injected.js in Resources */ = {isa = PBXBuildFile; fileRef = E380F27A233183EE00640547 /* injected.js */; };
E380F286233183EF00640547 /* mocks.js in Resources */ = {isa = PBXBuildFile; fileRef = E380F27B233183EE00640547 /* mocks.js */; };
E380F287233183EF00640547 /* keyboard-utils.js in Resources */ = {isa = PBXBuildFile; fileRef = E380F27C233183EE00640547 /* keyboard-utils.js */; };
E380F288233183EF00640547 /* mousetrap.js in Resources */ = {isa = PBXBuildFile; fileRef = E380F27E233183EE00640547 /* mousetrap.js */; };
E380F289233183EF00640547 /* vimium-scripts.js in Resources */ = {isa = PBXBuildFile; fileRef = E380F27F233183EE00640547 /* vimium-scripts.js */; };
- E380F28A233183EF00640547 /* settings.js in Resources */ = {isa = PBXBuildFile; fileRef = E380F280233183EE00640547 /* settings.js */; };
E380F28B233183EF00640547 /* injected.css in Resources */ = {isa = PBXBuildFile; fileRef = E380F282233183EE00640547 /* injected.css */; };
/* End PBXBuildFile section */
@@ -56,6 +57,9 @@
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
+ 65E444F224CC3A1B008EA1DC /* SafariExtensionCommunicator.js */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.javascript; path = SafariExtensionCommunicator.js; sourceTree = ""; };
+ B1E3C16F23A65ED400A56807 /* ConfigurationModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigurationModel.swift; sourceTree = ""; };
+ B1FD3B9823A588DE00677A52 /* defaultSettings.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = defaultSettings.json; sourceTree = ""; };
E320D0652337FC9800F2C3A4 /* Credits.rtf */ = {isa = PBXFileReference; lastKnownFileType = text.rtf; path = Credits.rtf; sourceTree = ""; };
E320D06723397C5C00F2C3A4 /* CHANGELOG.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = CHANGELOG.md; sourceTree = ""; };
E380F2462331806400640547 /* Vimari.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Vimari.app; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -74,13 +78,11 @@
E380F26B2331806500640547 /* ToolbarItemIcon.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = ToolbarItemIcon.pdf; sourceTree = ""; };
E380F26D2331806500640547 /* Vimari_Extension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Vimari_Extension.entitlements; sourceTree = ""; };
E380F278233183EE00640547 /* link-hints.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = "link-hints.js"; sourceTree = ""; };
- E380F279233183EE00640547 /* global.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = global.js; sourceTree = ""; };
E380F27A233183EE00640547 /* injected.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = injected.js; sourceTree = ""; };
E380F27B233183EE00640547 /* mocks.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = mocks.js; sourceTree = ""; };
E380F27C233183EE00640547 /* keyboard-utils.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = "keyboard-utils.js"; sourceTree = ""; };
E380F27E233183EE00640547 /* mousetrap.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = mousetrap.js; sourceTree = ""; };
E380F27F233183EE00640547 /* vimium-scripts.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = "vimium-scripts.js"; sourceTree = ""; };
- E380F280233183EE00640547 /* settings.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = settings.js; sourceTree = ""; };
E380F282233183EE00640547 /* injected.css */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.css; path = injected.css; sourceTree = ""; };
/* End PBXFileReference section */
@@ -103,6 +105,14 @@
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
+ B1FD3B9723A588DE00677A52 /* json */ = {
+ isa = PBXGroup;
+ children = (
+ B1FD3B9823A588DE00677A52 /* defaultSettings.json */,
+ );
+ path = json;
+ sourceTree = "";
+ };
E380F23D2331806300640547 = {
isa = PBXGroup;
children = (
@@ -149,8 +159,10 @@
isa = PBXGroup;
children = (
E380F2612331806500640547 /* SafariExtensionHandler.swift */,
+ B1E3C16F23A65ED400A56807 /* ConfigurationModel.swift */,
E380F2632331806500640547 /* SafariExtensionViewController.swift */,
E380F2652331806500640547 /* SafariExtensionViewController.xib */,
+ B1FD3B9723A588DE00677A52 /* json */,
E380F281233183EE00640547 /* css */,
E380F277233183EE00640547 /* js */,
E380F2682331806500640547 /* Info.plist */,
@@ -164,12 +176,11 @@
isa = PBXGroup;
children = (
E380F278233183EE00640547 /* link-hints.js */,
- E380F279233183EE00640547 /* global.js */,
E380F27A233183EE00640547 /* injected.js */,
E380F27B233183EE00640547 /* mocks.js */,
E380F27C233183EE00640547 /* keyboard-utils.js */,
E380F27D233183EE00640547 /* lib */,
- E380F280233183EE00640547 /* settings.js */,
+ 65E444F224CC3A1B008EA1DC /* SafariExtensionCommunicator.js */,
);
path = js;
sourceTree = "";
@@ -285,11 +296,11 @@
buildActionMask = 2147483647;
files = (
E380F26C2331806500640547 /* ToolbarItemIcon.pdf in Resources */,
- E380F28A233183EF00640547 /* settings.js in Resources */,
+ B1FD3B9923A588DE00677A52 /* defaultSettings.json in Resources */,
+ 65E444F324CC3A1B008EA1DC /* SafariExtensionCommunicator.js in Resources */,
E380F28B233183EF00640547 /* injected.css in Resources */,
E380F285233183EF00640547 /* injected.js in Resources */,
E380F287233183EF00640547 /* keyboard-utils.js in Resources */,
- E380F284233183EF00640547 /* global.js in Resources */,
E380F289233183EF00640547 /* vimium-scripts.js in Resources */,
E380F2672331806500640547 /* SafariExtensionViewController.xib in Resources */,
E380F286233183EF00640547 /* mocks.js in Resources */,
@@ -316,6 +327,7 @@
files = (
E380F2642331806500640547 /* SafariExtensionViewController.swift in Sources */,
E380F2622331806500640547 /* SafariExtensionHandler.swift in Sources */,
+ B1E3C17023A65ED400A56807 /* ConfigurationModel.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
diff --git a/Vimari/Base.lproj/Main.storyboard b/Vimari/Base.lproj/Main.storyboard
index c8ca3a9..1676e97 100644
--- a/Vimari/Base.lproj/Main.storyboard
+++ b/Vimari/Base.lproj/Main.storyboard
@@ -1,8 +1,8 @@
-
+
-
+
@@ -78,7 +78,7 @@
-
+
@@ -100,115 +100,179 @@
-
+
-
-
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
+
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
-
@@ -220,6 +284,6 @@ DQ
-
+
diff --git a/Vimari/ViewController.swift b/Vimari/ViewController.swift
index b3295c7..2b246b3 100644
--- a/Vimari/ViewController.swift
+++ b/Vimari/ViewController.swift
@@ -1,10 +1,16 @@
import Cocoa
import SafariServices.SFSafariApplication
+import OSLog
class ViewController: NSViewController {
- @IBOutlet var appNameLabel: NSTextField!
@IBOutlet var extensionStatus: NSTextField!
@IBOutlet var spinner: NSProgressIndicator!
+
+ private enum Constant {
+ static let extensionIdentifier = "net.televator.Vimari.SafariExtension"
+ static let openSettings = "openSettings"
+ static let resetSettings = "resetSettings"
+ }
func refreshExtensionStatus() {
NSLog("Refreshing extension status")
@@ -12,7 +18,8 @@ class ViewController: NSViewController {
extensionStatus.stringValue = "Checking extension status"
if SFSafariServicesAvailable() {
- SFSafariExtensionManager.getStateOfSafariExtension(withIdentifier: "net.televator.Vimari.SafariExtension") {
+ SFSafariExtensionManager.getStateOfSafariExtension(
+ withIdentifier: Constant.extensionIdentifier) {
state, error in
print("State", state as Any, "Error", error as Any, state?.isEnabled as Any)
@@ -45,16 +52,46 @@ class ViewController: NSViewController {
override func viewDidLoad() {
super.viewDidLoad()
- appNameLabel.stringValue = "Vimari"
refreshExtensionStatus()
}
@IBAction func openSafariExtensionPreferences(_: AnyObject?) {
- SFSafariApplication.showPreferencesForExtension(withIdentifier: "net.televator.Vimari.SafariExtension") { error in
+ SFSafariApplication.showPreferencesForExtension(
+ withIdentifier: Constant.extensionIdentifier) { error in
if let _ = error {
// Insert code to inform the user that something went wrong.
}
}
}
+
+ @IBAction func openSettingsAction(_ sender: Any) {
+ dispatchOpenSettings()
+ }
+
+ @IBAction func resetSettingsAction(_ sender: Any) {
+ dispatchResetSettings()
+ }
+
+ func dispatchOpenSettings() {
+ SFSafariApplication.dispatchMessage(
+ withName: Constant.openSettings,
+ toExtensionWithIdentifier: Constant.extensionIdentifier,
+ userInfo: nil) { (error) in
+ if let error = error {
+ print(error.localizedDescription)
+ }
+ }
+ }
+
+ func dispatchResetSettings() {
+ SFSafariApplication.dispatchMessage(
+ withName: Constant.resetSettings,
+ toExtensionWithIdentifier: Constant.extensionIdentifier,
+ userInfo: nil) { (error) in
+ if let error = error {
+ print(error.localizedDescription)
+ }
+ }
+ }
}