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:
- Firefox
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.
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.- 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.
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 loadspookfox-org-tabs
app, you'd write this Elisp:(require 'spookfox-tabs)
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))
Initialize spookfox
Changes to
spookfox-enabled-apps
take effect whenspookfox-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
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.
- 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:
- The background script; which can be considered the addon itself.
- The content script; which runs inside a web-page e.g on youtube.com
- 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:
- background Eval JS in addon’s background page.
- 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
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.
- Chain tabs, so any changes to the tab in Browser (e.g url change) are synced with the org-file
- 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
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
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.
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:
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)
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 ofpayload
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))))