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) + } + } + } }