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
- Open links or the current page
- org-store links or the current page
- bookmark links or the current page
- org-capture text selections with multiple capture templates
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!