Spookfox

Spookfox is a Browser extension and an Emacs package, which allow Emacs and Browser to communicate with each other. Its primary goal is to offer an Emacs tinkerer similar (to Emacs) framework to tinker their browser.

I use Spookfox as my daily driver to enable a number of workflow enhancements, e.g capturing articles I read and Youtube videos I watch.

Supported browsers:

  1. Firefox
  2. Chrome (thanks to @kajalelohai)

    But can you run it? Although Chrome is supported and a .crx file is generated in the releases, Chrome won't let you install and use it. Stop using Chrome. PS Check installation below.

Installation

There are 2 parts to install spookfox.

  1. Install the browser addon

    Download the addon (.xpi file for Firefox) from releases page. Browser will prompt you to install as soon as you download it.

    What is it not available on Browser addons page?

    Because it is not approved on Firefox addons yet.

    ⚠️ You'll have to drag-n-drop the downloaded crx file to Chrome to even install it, because Google. You will probably need to clone this repo, run yarn build, and load unpacked extension to run Spookfox in Chrome.

  2. Install Emacs package
    • Using straight.el

      (use-package spookfox
        :straight
        (spookfox :type git
                  :host github
                  :repo "bitspook/spookfox"
                  :files ("lisp/*.el" "lisp/apps/*.el"))
        :config
        (spookfox-start-server))
      
    • Manually
      • Clone this repository

        git clone https://github.com/bitspook/spookfox <path-to-spookfox>
        
      • Add spookfox and its apps load-path

        (add-to-list 'load-path "<path-to-spookfox>/lisp")
        (add-to-list 'load-path "<path-to-spookfox>/lisp/apps")
        

Usage

Spookfox itself is a thin layer which provide websockets based communication between Emacs and Browser. More functionality on top of it is provided with apps; an app is simply some elisp that uses primitives provided by spookfox.el to do something useful. See apps section below.

  1. Load the apps you want to use

    If you followed the installation instructions, all apps bundled with the package itself should be ready to be loaded with a call to require. For example, to load spookfox-tabs app, you'd write this Elisp:

    (require 'spookfox-tabs)
      
    Or call an autoloaded function like spookfox-switch-tab.
  2. Start spookfox

    Start the websockets server so spookfox addon in Firefox can connect to it.

    (spookfox-start-server)
    

Apps

Spookfox has a modular architecture. An "app" is a bundle of functionality written using primitives from =spookfox.el=. Following apps come bundled with this package.

spookfox-tabs

Basic access to browser's tabs.

Features

  1. Access browser tabs in Elisp

    You can use this to enhance your Emacs usage. For example, check my Emacs config to see how I use it to more easily capture notes for articles I read in the browser.

  2. Commands for manipulating tabs
    • spookfox-itabs
  • spookfox-itabs-mode

    Major mode derived from ‘special-mode’ by ‘define-derived-mode’. It inherits all of the parent’s attributes, but has its own keymap, abbrev table and syntax table:

    ‘spookfox-itabs-mode-map’, ‘spookfox-itabs-mode-abbrev-table’ and ‘spookfox-itabs-mode-syntax-table’

    which more-or-less shadow special-mode’s corresponding tables.

    In addition to any hooks its parent mode might have run, this mode runs the hook ‘spookfox-itabs-mode-hook’, as the final or penultimate step during initialization.

Key Binding

SPC scroll-up-command

  • negative-argument

0 .. 9 digit-argument < beginning-of-buffer > end-of-buffer ? describe-mode g revert-buffer h describe-mode q quit-window DEL scroll-down-command H-SPC [byte-code] S-SPC scroll-down-command

  • spookfox-switch-tab

    Like ‘switch-buffer’ but for browser tabs. When you have too many tabs to find what you want; or you want to jump to browser with your desired tab already in focus. Or to open a new tab.

    Note that this do not bring the browser window to focus. Depending on the kind of system, user have to do it by themselves. Example

js-injection

Inject Javascript into the browser. From a web extension's pov, there are three places to inject JS in:

  1. The background script; which can be considered the addon itself.
  2. The content script; which runs inside a web-page e.g on youtube.com
  3. The popup; which runs in addon's popup-ui page. This is the popup you see when you click the addon's icon in top browser bar.

This app provide following functions:

  • spookfox-js-injection-eval-in-active-tab

    spookfox-js-injection-eval-in-active-tab is a byte-compiled Lisp function in ‘spookfox-js-injection.el’.

    (spookfox-js-injection-eval-in-active-tab JS &optional JUST-THE-TIP-P)

    Evaluate JS in active firefox tab. Return value is a list of lists. Browser can have multiple active tabs (one per window). Every active tab can have multiple frames. If JUST-THE-TIP-P is non-nil, first tab’s first frame’s return value from the results is returned (instead of list of lists).

    JS is subjected to limitations of browser’s ability to execute it. It is similar to executing js in browser’s console. So for example running a script which declares a variable with ‘let‘ or ‘const‘ might cause the script to fail execution.

    Details about js execution: https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/tabs/executeScript

Inject Javascript into any website open in your browser's active tab, and get the results back. I use it to help me take notes for Youtube videos with automatically added timestamp links.

  • spookfox-js-injection-eval

    spookfox-js-injection-eval is a Lisp closure in ‘/mnt/data/channi-documents/work/spookfox/lisp/apps/spookfox-js-injection.el’.

    (spookfox-js-injection-eval JS &optional (CONTEXT 'background) (SELECT-TAB-P nil))

    Evaluate JS in CONTEXT. Return the result of evaluation.

    Supported contexts:

    1. background Eval JS in addon’s background page.
    2. tab Eval JS in tab. When non-nil SELECT-TAB-P should be a function which receives each tab (a plist of at least :url, :id, :title, :windowId), and JS is evaluated in every tab for which it returns non-nil.

Contribute

  1. Write apps

    If you want to write apps for Spookfox to handle a use-case not covered by existing apps, for now you need to go through the source code to figure things out. I am still working on a web-accessible documentation for Spookfox. Code is allegedly well-commented and existing apps can act as good examples.

  2. Modify Spookfox

    To make changes on the Browser side of things, you'll have to modify and rebuild the browser addon itself. Unfortunately browsers don't allow injecting code into the running addon anymore (although it is possible to inject code into a website).

    Or you might want to fix a bug, or make the code cleaner.

    Please take a look at the contributing.org for setting up the development environment to hack Spookfox.

Tips

Although making Spookfox apps is easy, you might not need that for some quick and easy tinkering. Here are some tips that might be helpful.

Make browser speak to Emacs

Spookfox has a request-response model. You can register custom request handlers in Emacs to run Elisp code which is triggered from browser e.g using a JavaScript bookmarklet. For this you need to do 2 things:

  1. Add a handle for request in Emacs

    Here is some example code for registering a request handler:

    (defun my--capture-the-flag (url)
      (raise-frame)
      (message "CAPTURING THE FLAG: %s" url))
    
    (spookfox--register-req-handler "CAPTURE_THE_FLAG" #'my--capture-the-flag)
    
  2. Send a request from browser

    postMessage({
      type: 'SPOOKFOX_RELAY_TO_EMACS',
      action: {
        name: 'CAPTURE_THE_FLAG',
        payload: { url: window.location.href }
      }
    })
    

    action part here decides which request handler will execute. Elisp version of payload will be provided to your request-handler as an argument.

Make Emacs speak to browser

js-injection spookfox app can be used to send requests from Emacs to browser, and even get results back. e.g to get current tab's URL, we can use

(spookfox-js-injection-eval-in-active-tab "window.location.href" t)
;; => "https://github.com/bitspook/spookfox/issues/38"

spookfox-js-injection-eval-in-active-tab runs js in current-window's active tab. js-injection also enable running JS in any tab (and even addon's background script) with more general purpose spookfox-js-injection-eval.

E.g run following code to run a script in all tabs which are visiting bitspook.in:

(spookfox-js-injection-eval
 "console.log('Hello from Emacs'); 5 + 5;"
 'tab
 (lambda (tab)
   (s-contains-p "bitspook.in" (plist-get tab :url))))