Firefox WebExtensions Theme API issue
I finally got round to learning how to create a browser extension. The idea for my first project was very simple: an extension that changes browser theme when particular URLs are open in the active tab. I thought that could be useful to, for example, make browser window red when programmer visits a production environment instead of a test environment.
I wanted my extension to work with Firefox, as this is the browser I use daily. It’s been just two months since Firefox stopped supporting old-style extensions and forced addon authors to migrate to new WebExtensions JavaScript APIs so I expected some challenges.
The documentation at developer.mozilla.org is quite comprehensive and there are multiple example extensions in the webextension-examples github repository I quickly found out what I needed. Getting active tab and its URL is easy with
browser.tabs.onActivated.addListener(onTabChanged);
browser.tabs.onUpdated.addListener(onTabUpdated);
The onActivated
listener is provided a tabId
that can be later used to get additional info about the tab
async function onTabChanged(obj) {
let tabId = obj.tabId;
let tab = await browser.tabs.get(tabId);
selectThemeByTab(tab);
}
The tab
object has two useful properties, url
to filter out interesting tabs and windowId
which is needed to update theme for particular window.
The onUpdated
listener is provided the tab
object directly as an argument. Registering this listener makes it possible to act when URL changes in a tab that is continuously active.
Updating theme is also quite straightforward:
function applyTheme(windowId, color) {
browser.theme.update(windowId, getBasicThemeWithAccentColor(color));
}
function getBasicThemeWithAccentColor(color) {
return {
images: {
headerURL: 'nothing.png',
},
colors: {
accentcolor: color,
textcolor: '#111',
}
}
}
I actually didn’t want to have any picture in my theme but I couldn’t find a way to skip it, so I just created a one-pixel image. This made the browser window look like this:
Once I had a proof-of-concept working I created a simple options page. Persisting settings is easy as API is quite similar to localStorage (but being able to store actual objects instead of only strings)
function saveRules(rules){
browser.storage.local.set({"rules": rules});
}
As I wanted to quickly get options page working, I decided to just use the vue.js
framework for the ‘front-end’. At first it threw some errors
[Vue warn]: It seems you are using the standalone build of Vue.js in an environment with Content Security Policy that prohibits unsafe-eval. The template compiler cannot work in this environment. Consider relaxing the policy to allow unsafe-eval or pre-compiling your templates into render functions.
but adding
"content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self';",
to the manifest.json
made them go away. I have no idea if including the whole JS framework for something as simple as an addon options page is actually a good idea or a common practice, but the effect was satisfying.
One thing remained that didn’t work as I wanted to: bringing back original theme for ‘normal’ tabs. Initially I thought I could just back up current theme with
browser.theme.getCurrent(windowId)
and then restore it when needed. That API was then shiny new as it came out with Firefox 57. I thought I could maybe even use the value returned by getCurrent
to only change configurable parts of theme instead of changing the whole theme to the new one.
Unfortunately, it didn’t work. I had a built-in “light” theme set up in my browser and my extension kept bringing back the default theme. At first I thought it was just some kind of bug in my code but I could not find it. The debugger revealed that the getCurrent
function kept returning an empty object when I wanted to back up the original theme. Calling browser.theme.update
with that empty object resulted in bringing back the default theme instead of the “light” one.
The getCurrent
function returned correct results when theme was already changed by my extension, but there seemed to no way to get the initial browser theme description from it.
I found the “theme manager” extension in the webextensions-examples github repository and inspired by it I wrote the following workaround to reset theme:
function resetTheme(windowId) {
browser.management.getAll().then((extensions) => {
for (let extension of extensions) {
if(extension.type === "theme" && extension.enabled) {
browser.management.setEnabled(extension.id, false);
browser.management.setEnabled(extension.id, true);
}
}
});
}
It uses the fact that when my extension changes theme using browser.theme.update
, the original ‘theme extension’ is still selected as an active extension. Disabling it and enabling it again removed theme changes made by my extension and correctly brought back the “light” theme.
Even though this function did what I wanted, resetting theme this way looked bad from user perspective (it took too long and was distracting). I didn’t want to use this hack and was quite confused by having two ways of configuring theme (browser.management
and browser.theme
) that seemed incompatible. I headed to #webextensions
at irc.mozilla.org
for help. People were very helpful and asked me a lot of questions to understand the issue I had.
Finally someone confirmed what I supposed could be true. The browser.theme.getCurrent
API works only with
“webextension themes”. The old-style themes, like lightweight themes called “personas”, are not supported by this API. It was also mentioned that built-in Firefox themes (like the “light” one I’m using) would be rewritten to web-extensions in the future.
The user story in the bugzilla ticket to create theme.getCurrent
API says:
As a developer who creates an add-on that has a UI component, I would really like a way to obtain information about the active theme so I can make my UI fit in with it. Additionally, I would like to receive information when the active theme changes so I can continue to make my UI fit in.
It seems to me that until Firefox completely stops supporting old-style themes the theme.getCurrent
API will not meet its goals.
Update (April 4, 2018)
I got an e-mail today (thanks Tim!) advising me to try the browser.theme.reset()
API. It reminded me that I actually tried using this one too but forgot to mention it in the post. It has basically the same issue I had with getCurrent
. Calling browser.theme.reset()
brings back the default theme, even if I have the “bright” theme selected. Documentation confirms this behavior is intended:
Note that this will always reset the theme back to the original default theme, even if the user had selected a different theme before this extension’s theme was applied.