A (...probably overengineered) chromium->eww Workflow (October, 2024)

I prefer the Emacs Web Wowser (not just because it has the cooler name) over my local ungoogled Chromium build for reading long articles, blog posts, and generally text-focussed things. And if the rest of the web wouldn't be as dependent on JavaScript as it is, I would probably use it as my main browser. The advantage eww has over another browser is, that I have all the note-taking functionality (denote, org-capture) at my fingertips, plus that I am (especially with eww-readable) in a text-focussed distraction-free environment (that is Emacs for me).

My most common usage scenario is, that I have a tab open on chromium and want to have it in eww instead, that I want to open an interesting link in eww instead of in another chromium tab, or that I want to org-capture a text-selection to file it away in org-mode. If I only would want to use the latter part, there's already org-capture as a Chromium extension that does a good job at utilizing the org-capture part. However, wanting more flexibility and well... another welcome excuse to yak shave and hack on elisp and javascript code for a bit, I started to come up with my own chrome extension that, besides supporting org-capture and org-store-link (which are default actions to org-protocol), allowed me to use custom handlers for eww.

In short, I wanted to be able to

  1. Open links or the current page
  2. org-store links or the current page
  3. bookmark links or the current page
  4. org-capture text selections with multiple capture templates
all from the context menu in chromium.

The entire project is available at theesm/chrewwmium.el (Codeberg) if anyone's interested.

A Tiny Bit Of Elisp Hackery

So some time ago I came up with a hack to send URLs from a chromium session to eww utilizing org-protocol for this by registering a eww-specific handler that opens URLs it gets in eww:

  
(require 'org-protocol)

(defun org-protocol-eww-open (plist)
  "Handle org-protocol URLs and open them in eww."
  (let ((url (plist-get plist :url)))
    (eww-browse-url (org-link-unescape url))))

(add-to-list 'org-protocol-protocol-alist
             '("eww"
               :protocol "eww"
               :function org-protocol-eww-open
               :kill-client t))
  

this, of course, only works if org-protocol has been set-up properly (https://orgmode.org/worg/org-contrib/org-protocol.html) so emacsclient can handle all things x-scheme-handler/org-protocol. (As soon as it's configured one can verify if it works or not relatively easy as xdg-open "org-protocol://eww?url=fsfe.org" should open fsfe.org in eww). Later in the process I also came up with a bookmark handler for eww bookmarks (on some days I'd prefer eww using emacses bookmark system instead of their own, but for maintaining a separate reading list having it separate does seem coincidentally advantageous).

  
(defun chrewwmium-bookmark-handler (plist)
  "Bookmark a URL in eww using org-protocol."
  (let* ((url (plist-get plist :url))
         (title (plist-get plist :title)))
    (eww-read-bookmarks)
    (dolist (bookmark eww-bookmarks)
      (when (equal url (plist-get bookmark :url))
        (user-error "URL already bookmarked")))
    (push (list :url url
                :title title
                :time (current-time-string))
          eww-bookmarks)
    (eww-write-bookmarks)
    (message "Bookmarked %s (%s)" url title))
nil)
  

And Some Lines of Messy JavaScript Later

We have yet to teach Chromiums context menu to open links in a clearly better browser, let's do 'so by writing horrendous JavaScript… eww.js enters the stage:

  
function createContextMenus() {
    chrome.contextMenus.removeAll(() => {
        chrome.storage.sync.get("captureTemplates", (data) => {
            const captureTemplates = data.captureTemplates || ["d"];

            const actions = [
                { name: "open", protocol: "eww" },
                { name: "bookmark", protocol: "eww-bookmark" },
                { name: "store", protocol: "store-link" },
                { name: "capture", protocol: "capture", contexts: ["selection"], templates: captureTemplates }
            ];

            const defaultContexts = ["link", "page"];

            actions.flatMap(({ name, protocol, contexts = defaultContexts, templates = [""] }) =>
                contexts.flatMap(context =>
                    templates.map(template => {
                        const templateSuffix = template ? `Template${template.toUpperCase()}` : "";
                        const id = `${name}${context.charAt(0).toUpperCase() + context.slice(1)}${templateSuffix}`;
                        const title = `${name.charAt(0).toUpperCase() + name.slice(1)} ${context === "link" ? "Link" : context === "page" ? "Current Page" : "Selection"}${template ? ` with Template ${template.toLowerCase()}` : ""}`;
                        return { id, title, contexts: [context], protocol, template };
                    })
                )
            ).forEach(({ id, title, contexts }) => {
                chrome.contextMenus.create({ id, title, contexts });
            });
        });
    });
}

chrome.storage.onChanged.addListener((changes, areaName) => {
    if (areaName === "sync" && changes.captureTemplates) {
        createContextMenus();
    }
});

chrome.runtime.onInstalled.addListener(createContextMenus);

chrome.contextMenus.onClicked.addListener((info, tab) => {
  const protocols = {
    open: "eww",
    bookmark: "eww-bookmark",
    store: "store-link",
    capture: "capture"
  };

  const [action, context, , templateMatch] = info.menuItemId.match(/^(open|bookmark|store|capture)(Link|Page|Selection)(Template(\w+))?/).slice(1);
  const protocol = protocols[action];

  const url = context === "Link" ? info.linkUrl : tab.url;
  const title = context === "Link" ? (info.selectionText || "Link") : tab.title;
  const body = context === "Selection" ? info.selectionText : "";
  const template = templateMatch ? templateMatch.toLowerCase() : "";

  if (url) {
    const params = new URLSearchParams({
      url,
      title,
      ...(action === "capture" && { template, body })
    }).toString();

    chrome.tabs.create({ url: `org-protocol://${protocol}?${params}` });
  }
});
  

a simple manifest.json for our prospective chrome extension later:

  
  {
      "manifest_version": 3,
      "name": "Open in Emacs",
      "version": "1.0",
      "permissions": [
  	"contextMenus",
  	"tabs"
      ],
      "background": {
  	"service_worker": "eww.js"
      }
  }
  

and having it loaded via chrome://extensions we're now able to either open the current page in eww, open a link from its context menu in eww or utilize org-capture or org-store-link from said menu, wowsers!