Complete Computing Environment: Web Browsing

Table of Contents

Web Browsing using EWW and Firefox

Web browsing is still hard; I want to use the Emacs-native EWW, but without native Javascript or anything that makes the modern "Web Apps" I use at work, well, work, I need a GUI browser. Right now my GUI browser of choice is Firefox, which provides enough keyboard-driven facilities as well as performance that is bearable.

(provide 'cce-browsers)

We can browse to anything that looks like a URL with SPC u.

(evil-leader/set-key (kbd "u") 'browse-url-at-point)

I use EWW as my default browser in Emacs; certain things, I need to jump out of EWW and in to a real browser with javascript; which I can do if I just hit & – it'll drop me in to a firefox session without a hassle.

There are certain sites that, despite my best abilities, I cannot make work in EWW. For these, I launch Firefox directly, instead of hitting a silly man in the middle. Eventually I'll probably try Qutebrowser again, but what I really want in life is Qutebrowser, but with Mozilla's tech driving it.

(setq browse-url-generic-program (executable-find "firefox"))

(setq browse-url-browser-function
   '(("https?://[a-z0-9.]+berinternal.com/.*" . browse-url-generic)
     ("https?://docs.google.com/.*" . browse-url-generic)
     (".*" . eww-browse-url)))

EWW

I use this advice to rename all of the EWW buffers based on their <title> elements.

(defadvice eww-tag-title (after rrix/eww-rename-buffer-ad (cont))
    "Update EWW buffer title with new page load."
    (let ((eww-current-title (plist-get eww-data :title)))
      (rename-buffer (format "*eww : %s *" (cce/str-chomp eww-current-title)) t)))
(ad-activate 'eww-tag-title)

Always open links in existing window, if possible.

(setq browse-url-new-window-flag t)

Part of my Expansion technique is to just open a shitload of browser buffers throughout the day and pick through them in my IDLE times. EWW has basically no session support like emacs-w3m does, which is actually Okay given that that didn't serialize out to any format which I can sync between my workstations easily. Instead, I've crafted this function which uses my W buffer capture template to dump every EWW buffer to my refile.

(install-pkgs '(el-pocket))
(load-library "el-pocket")
(defun rrix/capture-all-eww-buffers ()
  (interactive)
  (map 'list (lambda (b)
               (with-current-buffer b
                 (when (eq major-mode 'eww-mode)
                   (let ((url(plist-get eww-data :url))
                         (title (plist-get eww-data :title)))
                     (if (y-or-n-p (format "Capture %s (%s)? " title url))
                         (progn
                           (el-pocket-authorize)
                           (el-pocket-add url))
                       (kill-this-buffer-y-or-n))))))
       (buffer-list)))

Having a global history is an incredibly useful thing to have but EWW doesn't give me one of these out of the box; I advise this in after eww-render like so:

(defvar rrix/eww-global-history '())
(defadvice eww-render (after rrix/eww-global-history-ad ())
  "Add EWW history to a global history variable"
  (push (list :url (plist-get eww-data :url)
              :title (plist-get eww-data :title)
              :point (point)
              :date (format-time-string "%FT%T%z"))
        rrix/eww-global-history))
(ad-activate 'eww-render)
(add-to-list 'savehist-additional-variables 'rrix/eww-global-history)

I'll need to find some way to load that in to eww-list-histories, though… This is close, but it doesn't work since I'm not loading the full DOM, source and text in to the variable.

(defun rrix/list-eww-histories ()
  ""
  (interactive)
  (with-current-buffer (get-buffer-create "*eww-global-history*")
    (setq-local eww-history rrix/eww-global-history)
    (eww-list-histories)))

eww-lnum is a package that lets you follow any URL in an eww buffer with a keybinding, similar to Avy

(install-pkgs '(eww-lnum))
(eval-after-load 'eww-mode
  (lambda ()
    (define-key eww-mode-map "f" 'eww-lnum-follow)))

Firefox

- name: firefox installed
  apt:
    state: installed
    name: firefox
  when: ansible_pkg_mgr == "apt"

- name: firefox installed
  dnf:
    state: installed
    name: firefox
  when: ansible_pkg_mgr == "dnf"

KDE Plasma Integration

KDE Plasma has a new feature for integrating browsers in to the OS by combining a native d-bus service and a browser WebExtension. The Emacs d-bus library lets us use it to do an ivy integration to swap Firefox tabs.

(require 'dbus)

(defun cce/browser-activate-tabs-cb (choice)
  (funcall dbus "Activate" :int32 (truncate (string-to-number (gethash choice ivy-hash)))))

(defun browser-activate-tab ()
  "Activate a browser tab using Ivy. Requires plasma-browser integration"
  (interactive)
  (let ((ivy-hash (make-hash-table :test 'equal))
        (dbus (apply-partially 'dbus-call-method :session
                               "org.kde.plasma.browser_integration" "/TabsRunner"
                               "org.kde.plasma.browser_integration.TabsRunner")))
    (let ((cb #'cce/browser-activate-tabs-cb)
          (res (funcall dbus "GetTabs")))
      (mapc
       (lambda (obj)
         (let ((id (number-to-string (car (car (alist-get "id" (car obj) nil nil #'equal)))))
               (title (car (car (alist-get "title" (car obj) nil nil #'equal)))))
           (puthash title id ivy-hash)))
       res)
      (ivy-read "Activate tab: " ivy-hash :action cb))))

(eval-after-load "exwm"
  (lambda ()
    (evil-leader/set-key (kbd "t") #'browser-activate-tab)
    (define-key exwm-mode-map (kbd "<XF86Tools>") #'browser-activate-tab)
    (define-key exwm-mode-map (kbd "<f8>") #'browser-activate-tab)))

Customizing Firefox

Everyone is up in arms about Firefox Quantum removing core features, but it seems like we're doing alright. Firefox's work on building out WebExtension-based APIs gives us a good shot to customize it even in this new ecosystem. These are all from Timvde/UserChrome-Tweaks and modified to taste.

Hide the tab toolbar since I'm using the venerable Tree Style Tabs.

/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

#TabsToolbar {
    visibility: collapse !important;
}

Hide the header for the sidebar, and color the splitter to be the dark background color rather than light.

/* #sidebar-header is hidden by default, change "none" to "inherit" to restore it. */
#sidebar-header {
    display: none !important;
}

/* #sidebar-splitter styles the divider between the sidebar and the rest of the browser. */
#sidebar-splitter {
    background-color: var(--url-and-searchbar-background-color, hsla(0,0%,10%,.8)) !important;
}

This pushes the find-text bar to the top and to the right rather than the bottom.

.browserContainer > findbar {
    -moz-box-ordinal-group: 0;
    position: fixed !important;
    right: 1em;
    border: 1px solid threedshadow !important;

    /* Hide the "border" at the top by removing the box-shadow and background-image */
    border-top: none !important;
    box-shadow: none !important;
    background-image: none !important;

    transition: 400ms !important;
    z-index: 0 !important;
}

findbar[hidden]{ margin-top: -2em !important; }

.findbar-closebutton {
    margin-inline-start: 0.5em !important;
    vertical-align: middle !important;
    margin-bottom: 0.25em !important;
    height: inherit !important;
}

Make the default browser background dark instead of light. Why this isn't in the dark theme by default is beyond me.

.browserContainer {
    background-color: var(--url-and-searchbar-background-color, hsla(0,0%,100%,.8)) !important;
}
/*
 * Auto-hide the URL-bar and bookmarks bar, show on hover or focus
 *
 * Contributor(s): Alex Vallat
 */

:root[uidensity=compact] #navigator-toolbox {
  --nav-bar-height: 33px;
  --tab-min-height: 5px;
}

:root[uidensity=compact][extradragspace]:not([sizemode="normal"]) #navigator-toolbox {
  --nav-bar-height: 33px;
  --tab-min-height: 5px;
}

:root:not([uidensity]) #navigator-toolbox {
  --nav-bar-height: 39px;
  --tab-min-height: 5px;
}

:root:not([uidensity])[extradragspace]:not([sizemode="normal"]) #navigator-toolbox {
  --nav-bar-height: 39px;
  --tab-min-height: 5px;
}

:root[uidensity=touch] #navigator-toolbox {
  --nav-bar-height: 41px;
  --tab-min-height: 41px;
}

:root[uidensity=touch][extradragspace]:not([sizemode="normal"]) #navigator-toolbox {
  --nav-bar-height: 41px;
  --tab-min-height: 33px;
}

#navigator-toolbox {
  --tabbar-height: calc(var(--tab-min-height) + var(--space-above-tabbar));
  --trigger-area-height: 5px;
}

:root[chromehidden~="toolbar"] #navigator-toolbox {
  --tabbar-height: 0.1px;
}

#toolbar-menubar {
  margin-top: 0px !important; /* This is usually 0, but under Win7 can be given an extra 1px when not maximized */
}

/* Undo add of 4px extra margin on top of the tabs toolbar on Windows 7. */
/* Note: @media -moz-os-version does not work in userChrome.css (https://bugzilla.mozilla.org/show_bug.cgi?id=1418963) */
:root[sizemode="normal"][chromehidden~="menubar"] #TabsToolbar,
:root[sizemode="normal"] #toolbar-menubar[autohide="true"][inactive] + #TabsToolbar {
  padding-top: var(--space-above-tabbar) !important;
}

#nav-bar, #PersonalToolbar {
    /* Otherwise spacers will not count as hover-able areas */
    -moz-window-dragging: default;
}

:root:not([customizing]) #nav-bar
{
  overflow-y: hidden;
  max-height:0;
  min-height:0 !important;
  padding-top:0 !important;
  padding-bottom:0 !important;
  opacity: 0;
}

:root:not([customizing]) :hover > #nav-bar,
:root:not([customizing]) #nav-bar:focus-within {
  max-height: var(--nav-bar-height);
  opacity: 1;
}

:root:not([customizing]) #navigator-toolbox {
  max-height: calc(var(--tabbar-height) + var(--trigger-area-height));
  min-height: var(--tabbar-height);
  margin-bottom: calc(-1 * var(--trigger-area-height));
  transition: opacity 0.15s ease-in, max-height 0.15s linear;
}

:root:not([customizing]) #navigator-toolbox:hover,
:root:not([customizing]) #navigator-toolbox:focus-within {
  max-height: calc(var(--tabbar-height) + var(--nav-bar-height));
  margin-bottom: calc(0px - var(--nav-bar-height));
}

/* If the bookmarks bar is turned on, auto-hide that too */
:root:not([customizing]) #PersonalToolbar {
  max-height: 0 !important;
  min-height: 0.1px !important;
  opacity: 0;
}

:root:not([customizing]) :hover > #PersonalToolbar,
:root:not([customizing]) #navigator-toolbox:focus-within #PersonalToolbar {
  max-height: 4em !important;
  opacity: 1;
  transition: opacity 0.15s ease-in !important;
}

/* Lightweight Theme Support */
:root:-moz-lwtheme #nav-bar,
:root:-moz-lwtheme #PersonalToolbar {
  background-color: var(--lwt-accent-color) !important;
  background-image: var(--lwt-header-image), var(--lwt-additional-images) !important;
  background-position: var(--lwt-background-alignment) !important;
  background-repeat: var(--lwt-background-tiling) !important;
}

#main-window[sizemode="normal"]:-moz-lwtheme #nav-bar {
    background-position-y: calc(-2px - var(--tabbar-height)) !important;
}
#main-window[sizemode="normal"]:-moz-lwtheme #PersonalToolbar {
    background-position-y: calc(-2px - var(--tabbar-height) - var(--nav-bar-height)) !important;
}

#main-window[sizemode="maximized"]:-moz-lwtheme #nav-bar {
    background-position-y: calc(-8px - var(--tabbar-height)) !important;
}
#main-window[sizemode="maximized"]:-moz-lwtheme #PersonalToolbar {
    background-position-y: calc(-8px - var(--tabbar-height) - var(--nav-bar-height)) !important;
}
- name: find userChrome.css directory
  shell: ls -d ~/.mozilla/firefox/*default/
  args:
    executable: bash
  changed_when: False
  register: user_chrome_path
  become: yes
  become_user: "{{local_account}}"

- name: userChrome.css installed
  copy:
    src: out/userChrome.css
    dest: "{{user_chrome_path['stdout_lines'][0]}}/chrome/userChrome.css"
  become: yes
  become_user: "{{local_account}}"

Vim-style Modal Firefox without Vimperator or Vimium

I recently discovered exwm-firefox-evil, a neat little hack that uses EXWM to add a normal/insert mode dichotomy to Firefox. I've been using Vimium for a fair while now and I've been mostly content with it, but a few of my muscle-memory habits are tough to integrate. I've taken to using <SPACE> <SPACE> to open up my execute-extended-command, which wasn't working in this paradigm, obviously. Additionally, there are things like Reader Mode where Vimium doesn't work. This works on a nearly-stock Firefox, with only an add-on to set up the link-hinting that Vimium provided or like ace-line.

We'll see if this sticks, or how I expand upon it.

Install exwm-firefox-core and exwm-firefox-evil a pair of packages that are attempting to provide an evil experience for firefox.

(add-to-list 'el-get-sources '(:name exwm-firefox-core
                               :type github
                               :pkgname "walseb/exwm-firefox-core"))
(add-to-list 'el-get-sources '(:name exwm-firefox-evil
                               :type github
                               :pkgname "walseb/exwm-firefox-evil"))
(add-to-list 'cce/el-get-packages 'exwm-firefox-core)
(add-to-list 'cce/el-get-packages 'exwm-firefox-evil)

(el-get 'sync cce/el-get-packages)

Auto-enable exwm-firefox-evil-mode on Firefox windows.

(require 'exwm-firefox-evil)
(add-hook 'exwm-manage-finish-hook 'exwm-firefox-evil-activate-if-firefox)
(push (aref (kbd "<escape>") 0) exwm-input-prefix-keys)

Having things which go in to insert state for a short period of time, rather than stay there is nice. I have this hack to accomplish this, in two parts: a function which can be advice'd around one of the exwm-firefox-FOO functions which will set a buffer-local variable to true. Then, I have <return> re-bound in insert mode to a function which sends a return key event to the Firefox window, and then, if that variable was set to true, set it to false and enter normal mode. It's gaudy and might not work in the long term but here we are in the meantime.

(defun exwm-firefox-intercept-next-ret ()
  (interactive)
  (setq-local exwm-firefox-next-ret-normal t))

(defun exwm-firefox-intercept-return ()
  (interactive)
  (exwm-input--fake-key (aref (kbd "<return>") 0))
  (when (and (boundp 'exwm-firefox-next-ret-normal)
             exwm-firefox-next-ret-normal)
    (exwm-firefox-evil-normal)
    (setq-local exwm-firefox-next-ret-normal nil)))
(evil-define-key 'insert exwm-mode-map (kbd "<return>") 'exwm-firefox-intercept-return)
(evil-define-key 'emacs exwm-mode-map (kbd "<return>") 'exwm-firefox-intercept-return)

(push (aref (kbd "<return>") 0) exwm-input-prefix-keys)

I have defined here a helper macro which translates a set of mapped keys in to an interactive function which plumbs the key through, and then optionally dumps in to insert mode.

(defmacro define-evil-firefox-key (command-name input-key mapped-key insert-after doc)
  (let ((fname (intern (format "exwm-firefox-%s" command-name))))
    `(progn
       (defun ,fname ()
         ,doc
         (interactive)
         (exwm-input--fake-key (aref ,mapped-key 0))
         ,(when insert-after
            '(exwm-firefox-evil-insert)))
       (evil-define-key 'normal exwm-firefox-evil-mode-map ,input-key #',fname)
       ,(when insert-after
          `(advice-add #',fname :after #'exwm-firefox-intercept-next-ret)))))

Wire f up to showing link-hints using this Firefox add-on.

(define-evil-firefox-key show-link-hints
  (kbd "f") (kbd "C-m") true
  "Show link hints using https://addons.mozilla.org/en-US/firefox/addon/yet-another-hints-extension/")

Plumb C-k through in insert mode, return to normal mode after hitting enter. Chat clients use this as the "room switcher" keybinding usually and everyone loves chatting with their friends in a web browser.

(define-evil-firefox-key show-room-switcher
  (kbd "C-k") (kbd "C-k") true
  "Chat clients generally intercept C-k to show a room/chat
  switcher. This does that and moves to insert mode")

Wire up A to toggling Dark Reader extension

(define-evil-firefox-key toggle-dark-reader
  (kbd "A") (kbd "M-A") nil
  "Toggle between dark CSS and light CSS.")

Wire e up to switching in to Firefox reader view.

(define-evil-firefox-key toggle-reader-mode
  (kbd "e") (kbd "C-M-r") nil
  "Toggle between firefox Reader Mode view.")

Wire U up to restoring a closed tab.

(evil-define-key 'normal exwm-firefox-evil-mode-map (kbd "U") 'exwm-firefox-core-tab-close-undo)

Wire T up to toggling tree-style-tabs.

(define-evil-firefox-key toggle-tree-tabs
  (kbd "T") (kbd "C-M-t") nil
  "toggle visibility of tree-style-tabs sidebar")

When I'm in insert mode, I don't have arrow keys bound any more, so use C-NPBF in insert mode.

(evil-define-key 'insert exwm-firefox-evil-mode-map (kbd "C-n") 'exwm-firefox-core-down)
(evil-define-key 'insert exwm-firefox-evil-mode-map (kbd "C-p") 'exwm-firefox-core-up)
(evil-define-key 'insert exwm-firefox-evil-mode-map (kbd "C-b") 'exwm-firefox-core-left)
(evil-define-key 'insert exwm-firefox-evil-mode-map (kbd "C-f") 'exwm-firefox-core-right)

(evil-define-key 'insert exwm-firefox-evil-mode-map (kbd "C-a") 'exwm-firefox-core-top)
(evil-define-key 'insert exwm-firefox-evil-mode-map (kbd "C-e") 'exwm-firefox-core-bottom)

This sort of thing is what's so incredible to me about working inside of Emacs. Even my GUI browser becomes programmable in this way and in ways that the software itself doesn't allow. Firefox doesn't allow extensions to intercept or rebind certain keys (C-n and C-q, I'm looking at you), other things (like the Reader Mode and Pocket shortcuts) aren't even exposed as bindable or externally callable. Having a programmable windowing manager between you and the browser lets you do crazy shit like this.

This is the sort of pattern which could be applied to any GUI application, too, I just don't really use that many other than Firefox.

Author: Ryan Rix

Created: 2019-05-07 Tue 11:16

Validate XHTML 1.0