diff options
author | V <v@anomalous.eu> | 2021-07-01 02:48:26 +0200 |
---|---|---|
committer | V <v@anomalous.eu> | 2021-07-01 02:48:26 +0200 |
commit | d5fcf5b30357e8ed46edcde0c6ad5f0bd2c9ed00 (patch) | |
tree | d74a8f0ea62e0e96f55ab69b7747a8ca8d9afbda | |
download | hidit-d5fcf5b30357e8ed46edcde0c6ad5f0bd2c9ed00.tar.zst |
Root commit
-rw-r--r-- | README.adoc | 3 | ||||
-rw-r--r-- | background.js | 110 | ||||
-rw-r--r-- | manifest.json | 18 | ||||
-rwxr-xr-x | native | 182 | ||||
-rw-r--r-- | native-manifest.json | 7 |
5 files changed, 320 insertions, 0 deletions
diff --git a/README.adoc b/README.adoc new file mode 100644 index 0000000..326bbb2 --- /dev/null +++ b/README.adoc @@ -0,0 +1,3 @@ += Help, I'm drowning in tabs! + +HIDIT (pronounced "hide it") is […] diff --git a/background.js b/background.js new file mode 100644 index 0000000..c1be6e4 --- /dev/null +++ b/background.js @@ -0,0 +1,110 @@ +const PROTOCOL_VERSION = 0 + +const port = browser.runtime.connectNative("hidit") + +port.onDisconnect.addListener(port => { + if (port.error) { + // TODO(V): Write a more precise error message + console.error(`Disconnected due to an error: ${port.error.message}`) + // TODO(V): Do we want to restart the application here? Do we want to kill the extension? + } +}) + +function checkVersion(version) { + if (version !== PROTOCOL_VERSION) { + console.error(`Native application protocol has an incompatible version! Wanted ${PROTOCOL_VERSION}, got ${version}`) + // TODO(V): Fatal error. Ideally we would repeat this process once a second or so, and display + // an error icon in the toolbar to alert the user, along with some information on how to solve it. + } + + port.onMessage.removeListener(checkVersion) + port.onMessage.addListener(([command, args]) => { + switch (command) { + case "console:log": console.log(...args); break + case "console:error": console.error(...args); break + default: throw new Error(`Unknown command ${command}`) + } + }) + + browser.windows.onCreated.addListener(window => { + port.postMessage(["window:create", window]) + }) + + browser.windows.onFocusChanged.addListener(windowId => { + port.postMessage(["window:focus", windowId]) + }) + + browser.windows.onRemoved.addListener(windowId => { + port.postMessage(["window:destroy", windowId]) + }) + + browser.windows.getAll().then(windows => { + port.postMessage(["init:windows", windows]) + }) + + browser.tabs.onActivated.addListener(({ windowId, tabId, previousTabId }) => { + port.postMessage(["tab:activate", [windowId, tabId, previousTabId]]) + }) + + browser.tabs.onAttached.addListener((tabId, { newWindowId, newPosition }) => { + port.postMessage(["tab:attach", [tabId, newWindowId, newPosition]]) + }) + + browser.tabs.onCreated.addListener(tab => { + port.postMessage(["tab:create", tab]) + }) + + browser.tabs.onDetached.addListener((tabId, { oldWindowId, oldPosition }) => { + port.postMessage(["tab:detach", [tabId, oldWindowId, oldPosition]]) + }) + + browser.tabs.onHighlighted.addListener(({ windowId, tabIds }) => { + port.postMessage(["tab:select", [windowId, tabIds]]) + }) + + browser.tabs.onMoved.addListener((tabId, info) => { + port.postMessage(["tab:move", [tabId, info]]) + }) + + browser.tabs.onRemoved.addListener((tabId, { windowId, isWindowClosing }) => { + port.postMessage(["tab:destroy", [tabId, windowID, isWindowClosing]]) + }) + + // Note: MDN has the following to say: + // "This event may not be relevant for or supported by browsers other than Chrome." + browser.tabs.onReplaced.addListener((newTabId, oldTabId) => { + port.postMessage(["tab:replace", [oldTabId, newTabId]]) + }) + + // TODO(V): Remove the tab parameter, it shouldn't be necessary if we're doing deltas properly + browser.tabs.onUpdated.addListener((tabId, info, tab) => { + port.postMessage(["tab:update", [tabId, info, tab]]) + }) + + browser.tabs.onZoomChange.addListener(info => { + port.postMessage(["tab:zoom", info]) + }) + + browser.tabs.query({}).then(tabs => { + port.postMessage(["init:tabs", tabs]) + }) + + browser.contextualIdentities.onCreated.addListener(({ contextualIdentity }) => { + port.postMessage(["context:create", contextualIdentity]) + }) + + browser.contextualIdentities.onRemoved.addListener(({ contextualIdentity }) => { + port.postMessage(["context:destroy", contextualIdentity]) + }) + + browser.contextualIdentities.onUpdated.addListener(({ contextualIdentity }) => { + port.postMessage(["context:update", contextualIdentity]) + }) + + browser.contextualIdentities.query({}).then(contexts => { + port.postMessage(["init:contexts", contexts]) + // TODO(V): either send over the files in resource://usercontext-content/ or vendor them + }) +} +port.onMessage.addListener(checkVersion) +port.postMessage(PROTOCOL_VERSION) diff --git a/manifest.json b/manifest.json new file mode 100644 index 0000000..926b1fe --- /dev/null +++ b/manifest.json @@ -0,0 +1,18 @@ +{ + "manifest_version": 2, + + "name": "Help, I'm drowning in tabs!", + "version": "0.1", + + "browser_specific_settings": { + "gecko": { + "id": "@hidit" + } + }, + + "background": { + "scripts": [ "background.js" ] + }, + + "permissions": [ "nativeMessaging", "tabs", "contextualIdentities" ] +} diff --git a/native b/native new file mode 100755 index 0000000..e64bd6e --- /dev/null +++ b/native @@ -0,0 +1,182 @@ +#! /usr/bin/env nix-shell +#! nix-shell -i ruby -p ruby + +require 'json' + +PROTOCOL_VERSION = 0 + +WINDOW_ID_NONE = -1 +WINDOW_ID_CURRENT = -2 +TAB_ID_NONE = -1 + +module WebExtension + class <<self + def read() + length = $stdin.read(4).unpack1('L') + json = $stdin.read(length) + JSON.parse(json) + end + + def write(obj) + json = JSON.generate(obj) + raise "tried to send object larger than maximum allowed 1 MiB, was #{json.length/1024/1024} MiB" if json.length > 1024*1024 + $stdout.write([ json.length ].pack('L')) + $stdout.write(json) + $stdout.flush() + end + end +end + +module API + class <<self + def call(command, *args) + WebExtension::write([ command, args ]) + end + + def console_log(*args) + call('console:log', *args) + end + + def console_error(*args) + call('console:error', *args) + end + end +end + +# alwaysOnTop: false +# focused: true +# height: 1080 +# id: 18 +# incognito: false +# left: 0 +# state: "fullscreen" +# title: "Toolbox - Extension / Help, I'm drowning in tabs! — Mozilla Firefox" +# top: 0 +# type: "normal" +# width: 1920 +# Window = Struct( +# :id, +# :type, +# :state, + +# :title, + +# :always_on_top?, +# :focused?, +# :incognito?, + +# :width, +# :height, +# :top, +# :left, +# ) + +# active: true +# attention: false +# audible: false +# cookieStoreId: "firefox-default" +# discarded: false +# favIconUrl: "chrome://branding/content/icon32.png" +# height: 1006 +# hidden: false +# highlighted: true +# id: 2834 +# incognito: false +# index: 1228 +# isArticle: false +# isInReaderMode: false +# lastAccessed: 1625021510454 +# mutedInfo: Object { muted: false } +# pinned: false +# sharingState: Object { camera: false, microphone: false } +# status: "complete" +# successorTabId: -1 +# title: "New Tab" +# url: "about:newtab" +# width: 1599 +# windowId: 18 +# Tab = Struct( +# :id, +# :window_id, +# :index, +# :successor_tab_id, +# :last_accessed, + +# :status, +# :url, +# :icon_url, +# :title, + +# :active?, +# :attention?, +# :audible?, +# :pinned?, +# :hidden?, +# :discarded?, +# :incognito?, +# :selected?, + +# :width, +# :height, + +# :cookie_store_id, +# :article?, +# :in_reader_mode?, + +# :muted_info, +# :sharing_state, +# ) + +# class State +# attr_reader :windows, :tabs + +# def initialize(raw) +# @tabs = raw.concat_map +# end +# end + +if $stdin.tty? + $stderr.puts 'TODO' + exit 1 +end + +WebExtension::write(PROTOCOL_VERSION) +version = WebExtension::read() +if version != PROTOCOL_VERSION + $stderr.puts "Extension protocol has an incompatible version! Wanted #{PROTOCOL_VERSION}, got #{version}" + exit 1 +end + +API::console_log('Native application successfully loaded.') + +loop do + command, data = WebExtension::read() + + case command + when 'init:windows' then API::console_log("Initialised #{data.length} windows.") + when 'init:tabs' then API::console_log("Initialised #{data.length} tabs.") + when 'init:contexts' then API::console_log("Initialised #{data.length} contexts.") + + when 'window:create' then API::console_log('Window created', data) + when 'window:destroy' then API::console_log("Window #{data} destroyed") + when 'window:focus' + if data == WINDOW_ID_NONE + then API::console_log('Browser lost focus') + else API::console_log("Window #{data} focused") + end + + when 'tab:create' then API::console_log('Tab created', data) + when 'tab:destroy' then API::console_log("Tab #{data[0]} destroyed in window #{data[1]}#{", due to the window closing" if data[2]}") + when 'tab:update' then API::console_log("Tab #{data[0]} updated", data[1]) + when 'tab:activate' then API::console_log("Tab #{data[1]} in window #{data[0]} activated#{", previous tab was #{data[2]}" if not data[2].nil?}") + when 'tab:select' then API::console_log("Tabs #{data[1]} in window #{data[0]} selected") + when 'tab:detach' then API::console_log("Tab #{data[0]} detached from window #{data[1]} at position #{data[2]}") + when 'tab:attach' then API::console_log("Tab #{data[0]} attached to window #{data[1]} at position #{data[2]}") + + when 'context:create' then API::console_error('context:create NYI') + when 'context:destroy' then API::console_error('context:destroy NYI') + when 'context:update' then API::console_error('context:update NYI') + + else API::console_error("Unknown native command #{command}") + end +end diff --git a/native-manifest.json b/native-manifest.json new file mode 100644 index 0000000..f0d4f91 --- /dev/null +++ b/native-manifest.json @@ -0,0 +1,7 @@ +{ + "name": "hidit", + "description": "Help, I'm drowning in tabs!", + "path": "/home/v/src/hidit/native", + "type": "stdio", + "allowed_extensions": [ "@hidit" ] +} |