about summary refs log tree commit diff
diff options
context:
space:
mode:
authorV <v@anomalous.eu>2021-07-01 02:48:26 +0200
committerV <v@anomalous.eu>2021-07-01 02:48:26 +0200
commitd5fcf5b30357e8ed46edcde0c6ad5f0bd2c9ed00 (patch)
treed74a8f0ea62e0e96f55ab69b7747a8ca8d9afbda
downloadhidit-d5fcf5b30357e8ed46edcde0c6ad5f0bd2c9ed00.tar.zst
Root commit
-rw-r--r--README.adoc3
-rw-r--r--background.js110
-rw-r--r--manifest.json18
-rwxr-xr-xnative182
-rw-r--r--native-manifest.json7
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" ]
+}