{"id":16661518,"url":"https://github.com/colonelpanic8/dominion-hud","last_synced_at":"2026-04-21T18:06:53.654Z","repository":{"id":142101839,"uuid":"119631032","full_name":"colonelpanic8/dominion-hud","owner":"colonelpanic8","description":null,"archived":false,"fork":false,"pushed_at":"2018-02-09T20:01:28.000Z","size":76,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":3,"default_branch":"master","last_synced_at":"2025-01-19T13:47:09.817Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Clojure","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/colonelpanic8.png","metadata":{"files":{"readme":"readme.md","changelog":null,"contributing":null,"funding":null,"license":"license.txt","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null}},"created_at":"2018-01-31T03:43:38.000Z","updated_at":"2018-02-08T18:31:17.000Z","dependencies_parsed_at":"2023-08-09T23:43:39.289Z","dependency_job_id":"c821e250-83c4-46c4-a12e-0df918595d10","html_url":"https://github.com/colonelpanic8/dominion-hud","commit_stats":null,"previous_names":["colonelpanic8/dominion-hud"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/colonelpanic8%2Fdominion-hud","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/colonelpanic8%2Fdominion-hud/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/colonelpanic8%2Fdominion-hud/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/colonelpanic8%2Fdominion-hud/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/colonelpanic8","download_url":"https://codeload.github.com/colonelpanic8/dominion-hud/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":243293795,"owners_count":20268142,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-10-12T10:35:20.326Z","updated_at":"2025-12-11T18:52:13.157Z","avatar_url":"https://github.com/colonelpanic8.png","language":"Clojure","funding_links":[],"categories":[],"sub_categories":[],"readme":"# chromex-sample [![GitHub license](https://img.shields.io/github/license/binaryage/chromex-sample.svg)](license.txt)\n\n### An example extension using Chromex library\n\nThis project acts as a code example for [chromex library](https://github.com/binaryage/chromex) but also as a skeleton\nwith project configuration following best practices. We recommend to use it as a starting point when starting development\nof your own extension.\n\n#### **chromex-sample** has a minimalist **background page**, **popup button** and **content script**:\n\n  * background page listens for connections from popup buttons and content scripts (there can be multiple of them)\n  * popup button connects to the background page and sends a simple \"HELLO\" message after connection\n  * content script connects to the background page and sends a simple \"HELLO\" message after connection\n  * content script does a simple page analysis upon launch (it counts number of script tags) and sends an info message to the background page\n  * background page listens to tab creation events and notifies all connected clients about new tabs being created\n\n#### **chromex-sample** project has following configuration:\n\n  * uses [leiningen](http://leiningen.org) + [lein-cljsbuild](https://github.com/emezeske/lein-cljsbuild)\n  * integrates [cljs-devtools](https://github.com/binaryage/cljs-devtools)\n  * integrates [figwheel](https://github.com/bhauman/lein-figwheel) (for background page and popup buttons)\n  * under `:unpacked` profile (development)\n    * background page and popup button\n      * compiles with `optimizations :none`\n      * namespaces are included as individual files and source maps work as expected\n      * figwheel works\n    * content script\n      * due to [security restrictions](https://github.com/binaryage/chromex-sample/issues/2), content script has to be provided as a single file\n      * compiles with `:optimizations :whitespace` and `:pretty-print true`\n      * figwheel cannot be used in this context (eval is not allowed)\n  * under `:release` profile\n    * background page, popup button and content script compile with `optimizations :advanced`\n    * elides asserts\n    * no figwheel support\n    * no cljs-devtools support\n    * `lein package` task is provided for building an extension package for release\n\n### Local setup\n\n#### Extension development\n\nWe assume you are familiar with ClojureScript tooling and you have your machine in a good shape running recent versions of\njava, maven, leiningen, etc.\n\n  * clone this repo somewhere:\n    ```bash\n    git clone https://github.com/binaryage/chromex-sample.git\n    cd chromex-sample\n    ```\n  * chromex sample is gets built into `resources/unpacked/compiled` folder.\n\n    In one terminal session run (will build background and popup pages using figwheel):\n    ```bash\n    lein fig\n    ```\n    In a second terminal session run (will auto-build content-script):\n    ```bash\n    lein content\n    ```\n  * use latest Chrome Canary with [Custom Formatters](https://github.com/binaryage/cljs-devtools#enable-custom-formatters-in-your-chrome-canary) enabled\n  * In Chrome Canary, open `chrome://extensions` and add `resources/unpacked` via \"Load unpacked extension...\"\n\n##### Debugging\n\nChrome extension development is more complex than regular ClojureScript front-end\nwork. You are writing (and debugging) multiple parallel communicating processes: your\nbackground page, your popup, and all the browser pages running your content script.\n\nAmazingly, the ClojureScript tooling and Figwheel live coding remain very usable in this\nenvironment. But, you need to be aware of a few things, particularly in regard to\ncompiler warnings:\n\nMost warnings do not appear in the repls. Figwheel intercepts them for display in the\nbrowser. Warning will appear in the Chrome console and, when possible, as an overlay in\nthe browser window. But, the exact behavior depends upon which part of your code has the\nerror:\n\n_Content script_: Warnings and errors will appear in the repl running `lein content`.\n\n_popup_: Chrome normally closes the popup anytime focus leaves Chrome. So, if you are\nworking in your editor, the popup is closed and you will not see any error messages\nanywhere. This can be very frustrating but is easy to fix. When you first open the\npopup, right click on its icon and select `Inspect popup`. This opens the Chrome\ninspector/console and keeps the popup open while the inspector remains open. Any errrors\nwill appear in both the console and as the Figwheel overlay in your popup window. Also,\nof course, this gives you niceties of Figwheel live coding. Your changes will appear\nimmediately, with no need to close and reopen the popup.\n\n_background_: The background code is running under Figwheel, so no messages will appear\nin the repl. It also has no visibile window, so no Figwheel overlay can appear. You will\nonly see warnings in the Chrome console. You can open the inspector/console from\n`chrome://extensions`. Under your extension, click on the `Inspect Views` line.\n\nIn summary, effective live debugging requires up to five open windows on your screen:\n- Your editor;\n- The shell running `lein content`, if you are making changes to content script code;\n- The web browser, with open popup and/or content page;\n- A Chrome inspector console, watching the background page; and\n- A Chrome inspector console, watching the popup page.\n\n\n\n#### Extension packaging\n\n[Leiningen project](project.clj) has defined \"release\" profile for compilation in advanced mode. Run:\n```bash\nlein release\n```\n\nThis will build an optimized build into [resources/release](resources/release). You can add this folder via \"Load unpacked extension...\"\nto test it.\n\nWhen satisfied, you can run:\n```bash\nlein package\n```\n\nThis will create a folder `releases/chromex-sample-0.1.0` where 0.1.0 will be current version from [project.clj](project.clj).\nThis folder will contain only files meant to be packaged.\n\nFinally you can use Chrome's \"Pack extension\" tool to prepare the final package (.crx and .pem files).\n\n### Code discussion\n\nBefore reading the code below you should get familiar with [Chrome Extension System architecture](https://developer.chrome.com/extensions/overview#arch).\n\n#### Popup page\n\nLet's start with [popup button code](src/popup/chromex_sample/popup/core.cljs):\n\n```clojure\n; -- a message loop -------------------------------------------------------------------------------\n\n(defn process-message! [message]\n  (log \"POPUP: got message:\" message))\n\n(defn run-message-loop! [message-channel]\n  (log \"POPUP: starting message loop...\")\n  (go-loop []\n    (when-some [message (\u003c! message-channel)]\n      (process-message! message)\n      (recur))\n    (log \"POPUP: leaving message loop\")))\n\n(defn connect-to-background-page! []\n  (let [background-port (runtime/connect)]\n    (post-message! background-port \"hello from POPUP!\")\n    (run-message-loop! background-port)))\n\n; -- main entry point -----------------------------------------------------------------------------\n\n(defn init! []\n  (log \"POPUP: init\")\n  (connect-to-background-page!))\n```\n\nWhen a popup button is clicked, Chrome creates a new javascript context and runs our code by calling `init!`.\nAt this point we call [`runtime/connect`](https://developer.chrome.com/extensions/runtime#method-connect) to connect to our background page.\nWe get a `background-port` back which is a wrapper of [`runtime.Port`](https://developer.chrome.com/extensions/runtime#type-Port).\n`background-port` implements chromex protocol [`IChromePort`](https://github.com/binaryage/chromex/blob/master/src/lib/chromex/chrome_port.cljs)\nwhich we can use to `post-message!` to our background page. `background-port` also implements `core-async/ReadPort` so we can treat\nit as a core.async channel for reading incoming messages sent by our background page. You can see that implemented in `run-message-loop!`\nwhich takes messages off the channel and simply prints them into console (in `process-message!`).\n\n##### Marshalling\n\nAt this point you might ask. How is it possible that we called API method `runtime/connect` and got back `background-port` implementing `IChromePort`?\nThat is not documented behaviour described in [Chrome's extension APIs docs](https://developer.chrome.com/extensions/runtime#method-connect).\nWe would expect a native javascript object of type `runtime.Port`.\n\nThis transformation was done by [marshalling subsystem](https://github.com/binaryage/chromex/#flexible-marshalling) implemented in Chromex library. Marshalling is responsible for converting\nparameter values when crossing API boundary. Parameter values can be automatically converted to ClojureScript values when returned from native Javascript API calls and\nin the other direction parameters can be converted to native Javascript values when passed into API calls. This is a way how to ease\nextension development and promote idiomatic ClojureScript patterns.\n\nChromex library does not try to do heavy marshalling. You should review marshalling logic in [marshalling.clj](https://github.com/binaryage/chromex/blob/master/src/lib/chromex/marshalling.clj) and [marshalling.cljs](https://github.com/binaryage/chromex/blob/master/src/lib/chromex/marshalling.cljs)\nfiles to understand which parameter types get converted and how. You can also later use this subsystem to marshall\nadditional parameter types of your own interest. For example automatic calling of `js-\u003eclj` and `clj-\u003ejs` would come handy at many places.\n\n##### Message loop\n\nIt is worth noting that core.async channel [returns `nil` when closed](https://clojure.github.io/core.async/#clojure.core.async/\u003c!).\nThat is why we leave the message loop after receiving a `nil` message. If you wanted to terminate the message channel from popup side,\nyou could call core.async's `close!` on the message-channel (it implements [`core-async/Channel`](https://github.com/binaryage/chromex/blob/master/src/lib/chromex/chrome_port.cljs) and will properly disconnect `runtime.Port`).\n\nAs a consequence you cannot send a `nil` message through our channel.\n\n#### Background page\n\nLet's take a look at [background page](src/background/chromex_sample/background/core.cljs) which is also pretty simple. It just has to handle multiple clients and their individual\nmessage loops. Also it maintains one main event loop for receiving events from Chrome. With core.async channels the code\nreads quite well:\n\n```clojure\n(def clients (atom []))\n\n; -- clients manipulation -------------------------------------------------------------------------\n\n(defn add-client! [client]\n  (log \"BACKGROUND: client connected\" (get-sender client))\n  (swap! clients conj client))\n\n(defn remove-client! [client]\n  (log \"BACKGROUND: client disconnected\" (get-sender client))\n  (let [remove-item (fn [coll item] (remove #(identical? item %) coll))]\n    (swap! clients remove-item client)))\n\n; -- client event loop ----------------------------------------------------------------------------\n\n(defn run-client-message-loop! [client]\n  (log \"BACKGROUND: starting event loop for client:\" (get-sender client))\n  (go-loop []\n    (when-some [message (\u003c! client)]\n      (log \"BACKGROUND: got client message:\" message \"from\" (get-sender client))\n      (recur))\n    (log \"BACKGROUND: leaving event loop for client:\" (get-sender client))\n    (remove-client! client)))\n\n; -- event handlers -------------------------------------------------------------------------------\n\n(defn handle-client-connection! [client]\n  (add-client! client)\n  (post-message! client \"hello from BACKGROUND PAGE!\")\n  (run-client-message-loop! client))\n\n(defn tell-clients-about-new-tab! []\n  (doseq [client @clients]\n    (post-message! client \"a new tab was created\")))\n\n; -- main event loop ------------------------------------------------------------------------------\n\n(defn process-chrome-event [event-num event]\n  (log (gstring/format \"BACKGROUND: got chrome event (%05d)\" event-num) event)\n  (let [[event-id event-args] event]\n    (case event-id\n      ::runtime/on-connect (apply handle-client-connection! event-args)\n      ::tabs/on-created (tell-clients-about-new-tab!)\n      nil)))\n\n(defn run-chrome-event-loop! [chrome-event-channel]\n  (log \"BACKGROUND: starting main event loop...\")\n  (go-loop [event-num 1]\n    (when-some [event (\u003c! chrome-event-channel)]\n      (process-chrome-event event-num event)\n      (recur (inc event-num)))\n    (log \"BACKGROUND: leaving main event loop\")))\n\n(defn boot-chrome-event-loop! []\n  (let [chrome-event-channel (make-chrome-event-channel (chan))]\n    (tabs/tap-all-events chrome-event-channel)\n    (runtime/tap-all-events chrome-event-channel)\n    (run-chrome-event-loop! chrome-event-channel)))\n\n; -- main entry point -----------------------------------------------------------------------------\n\n(defn init! []\n  (log \"BACKGROUND: init\")\n  (boot-chrome-event-loop!))\n```\n\nAgain, main entry point for background page is our `init!` function. We start by running main event loop by subscribing\nto some Chrome events. `tabs/tap-all-events` is a convenience method which subscribes to all events defined in [tabs namespace](https://github.com/binaryage/chromex/blob/master/src/exts/chromex/ext/tabs.clj) to be delivered into provided channel.\nSimilarly `runtime/tap-all-events` subscribes all runtime events. We could as well subscribe individual events for example by calling `tabs/tap-on-created-events`,\nbut subscribing in bulk is more convenient in this case. As you can see we create our own ordinary core.async channel and wrap it in `make-chrome-event-channel` call.\nThis is an optional step, but convenient. `make-chrome-event-channel` returns a channel which is aware of Chrome event subscriptions and is able to unsubscribe\nthem when the channel is about to be closed (for whatever reason). This way we don't have to do any book keeping for future cleanup.\n\nEvents delivered into the channel are in a form `[event-id event-args]` where event-args is a vector of parameters which were passed into event's callback function (after marshalling).\nSo you can read Chrome documentation to figure out what to expect there. For example our `:chromex.ext.runtime/on-connect` event-id is\ndocumented under [runtime/on-connect event](https://developer.chrome.com/extensions/runtime#event-onConnect) and claims that\nthe callback has a single parameter `port` of type `runtime.Port`. Se we get `IChromePort` wrapper, because marshalling converted native `runtime.Port` into ClojureScript-friendly `IChromePort` on the way out.\n\nOk, when anything connects to our background page, we receive an event with `::runtime/on-connect` id. We call `handle-client-connection!` with event-args.\nHere we have to do some client-specific work. First, add this new client into a collection of active clients. Second, send a hello message to the client and\nfinally run client-specific event loop for receiving messages from this client. We don't do anything with received messages, we just print them into console with a bit of information about the sender.\nWhen our client message channel gets terminated (for whatever reason), we remove client from active clients and forget about it.\n\n##### Notifying clients about interesting events\n\nWe provide an additional separate functionality from maintaining client message loops.\nWhen Chrome notifies us about a new tab being created. We simply send a message to all our connected clients by calling `tell-clients-about-new-tab!`.\n\n##### More on cleanup\n\nYou might be asking why there is no explicit cleanup code here? There should be some `.removeListener` calls when we are\nleaving message loops, no?\n\nThis cleanup is done under the hood because we are using Chromex wrappers here. Wrappers act as core.async channels but know\nhow to gracefully disconnect when channel is closed (or close channel when client disconnected). In case of client connections you\nget a `runtime.Port` wrapper automatically thanks to marshalling. In case of main event loop you created a wrapper explicitly by calling\n`make-chrome-event-channel`.\n\nPlease keep in mind that you can always access underlying objects and talk to them directly if needed.\n\n#### Content Script\n\nOur [content script](src/content_script/chromex_sample/content_script/core.cljs) is almost copy\u0026paste of popup page code:\n\n```clojure\n; -- a message loop -------------------------------------------------------------------------------\n\n(defn process-message! [message]\n  (log \"CONTENT SCRIPT: got message:\" message))\n\n(defn run-message-loop! [message-channel]\n  (log \"CONTENT SCRIPT: starting message loop...\")\n  (go-loop []\n    (when-some [message (\u003c! message-channel)]\n      (process-message! message)\n      (recur))\n    (log \"CONTENT SCRIPT: leaving message loop\")))\n\n; -- a simple page analysis  ----------------------------------------------------------------------\n\n(defn do-page-analysis! [background-port]\n  (let [script-elements (.getElementsByTagName js/document \"script\")\n        script-count (.-length script-elements)\n        title (.-title js/document)\n        msg (str \"CONTENT SCRIPT: document '\" title \"' contains \" script-count \" script tags.\")]\n    (log msg)\n    (post-message! background-port msg)))\n\n(defn connect-to-background-page! []\n  (let [background-port (runtime/connect)]\n    (post-message! background-port \"hello from CONTENT SCRIPT!\")\n    (run-message-loop! background-port)\n    (do-page-analysis! background-port)))\n\n; -- main entry point -----------------------------------------------------------------------------\n\n(defn init! []\n  (log \"CONTENT SCRIPT: init\")\n  (connect-to-background-page!))\n```\n\nUpon launch we connect to the background page, send hello message and start a message loop with background page.\n\nAdditionally we call `do-page-analysis!` which does some simple DOM access, counts\nnumber of script tags and sends a reporting message to the background page.\n\n##### Receiving messages from background page\n\nAs you can see, we don't have any interesting logic here for processing messages from background page. In `process-message!`\nwe simply print the received message into console. It works! You can test it by creating new tabs. Background page should be\nsending notifications about new tabs being created.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcolonelpanic8%2Fdominion-hud","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fcolonelpanic8%2Fdominion-hud","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcolonelpanic8%2Fdominion-hud/lists"}