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-init))
      
    • 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. Different apps need to be configured as documented in 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-org-tabs app, you'd write this Elisp:

    (require 'spookfox-tabs)
    
  2. Tell spookfox which apps you want to enable

    Provide the list of enabled apps to spookfox-enabled-apps variable:

    (setq spookfox-enabled-apps '(spookfox-org-tabs))
    
  3. Initialize spookfox

    Changes to spookfox-enabled-apps take effect when spookfox-init function is called. This function also starts the websockets server.

    (spookfox-init)
    

Apps

Spookfox has a modular architecture. An "app" is a bundle of functionality isolated and opt-in. 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.

jscl

Spookfox ships JSCL compiler, which can be used to run a subset of common-lisp in the browser. For example:

(sfcl-eval
   `(progn
      (js:browser:tabs:update ,tab-id ,(sfcl-js-obj '(("active" . t))))
      (js:browser:windows:update ,window-id ,(sfcl-js-obj '(("focused" . t))))
      t))

spookfox-tabs.el use jscl for spookfox-switch-tab

Following functions are available:

  • spookfox-jscl-eval

    spookfox-jscl-eval is a byte-compiled Lisp function in ‘spookfox-jscl.el’.

    (spookfox-jscl-eval FORM &optional (CONTEXT 'background))

    Evaluate LISP FORM in background script in CONTEXT. CONTEXT can be one of (background).

    Note: JSCL uses #j: for FFI, but FORM must use ‘js:‘ for that, because emacs-lisp do not allow writing #j: forms, even in quoted form.

To make it a little easier to work with JS from CL, following utility functions can be used:

  • spookfox-jscl-js-obj

    spookfox-jscl-js-obj is a byte-compiled Lisp function in ‘spookfox-jscl.el’.

    (spookfox-jscl-js-obj ALIST)

    Create a javascript object from ALIST.

spookfox-org-tabs

⚠️ I am not using this app myself anymore. It is the most buggy of 'em all. I am keeping it around because list of my open tabs is increasing again, and I might once again need this.

Manage browser's tabs in an org file (separate file or as a subtree in an existing one).

Features

  1. Organize tabs freely in org file

    Tabs are stored as org-mode subtrees, which you are free to structure as you desire. You can group tabs by assigning org-mode tags, to manipulate tabs (open, close) in bulk.

  2. Chain tabs, so any changes to the tab in Browser (e.g url change) are synced with the org-file
  3. Commands for manipulating tabs
    • spookfox-org-tabs-open-group

      Prompt for a tab group, and open all tabs belonging to that group.

    • spookfox-org-tabs-open

      Prompt user to select a tab and open it in spookfox browser.

    • spookfox-org-tabs-save-active-tab

      Save active tab in browser.

    • spookfox-org-tabs-save-all-tabs

      Save all currently open browser tabs at ‘spookfox-saved-tabs-target‘. It will open a capture buffer so user get a chance to preview and make changes.

Configuration

  1. spookfox-saved-tabs-target

    spookfox-saved-tabs-target is an org-capture-templates target, where the browser tabs are saved. For example:

    ;; Store tabs in a file named =spookfox.org=, under '* Tabs' heading
    (setq spookfox-saved-tabs-target
          `(file+headline
            ,(expand-file-name "spookfox.org" org-directory)
            "Tabs"))
    

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