I recently blogged about discontinuing Positron. I’m trying a different tack with a new experiment, codenamed qbrt, that reuses an existing Gecko runtime (and its existing APIs) while simplifying the process of developing and packaging a desktop app using web technologies.
qbrt is a command-line interface written in Node.js and available via NPM:
npm install -g qbrt
Installing it also installs a Gecko runtime (currently a nightly build of Firefox, but in the future it could be a stable build of Firefox or a custom Gecko runtime). Its simplest use is then to invoke the ‘run’ command with a URL:
qbrt run https://eggtimer.org/
Which will start a process and load the URL into a native window:
URLs loaded in this way don’t have privileged access to the system. They’re treated as web content, not application chrome.
To load a desktop application with system privileges, point qbrt at a local directory containing a package.json file and main entry script:
qbrt run path/to/my/app/
For example, clone qbrt’s repo and try its example/ app:
git clone https://github.com/mozilla/qbrt.git qbrt run qbrt/example/
This will start a process and load the app into a privileged context, giving it access to Gecko’s APIs for opening windows and loading web content along with system integration APIs for file manipulation, networking, process management, etc.
(Another good example is the “shell” app that qbrt uses to load URLs.)
To package an app for distribution, invoke the ‘package’ command, which creates a platform-specific package containing both the app’s resources and the Gecko runtime:
qbrt package path/to/my/app/
Note that while qbrt is written in Node.js, it doesn’t provide Node.js APIs to apps. It might be useful to do so, using SpiderNode, as we did with Positron, although Gecko’s existing APIs expose equivalent functionality.
Also, qbrt doesn’t yet support runtime version management (i.e. being able to specify which version of Gecko to use, and to switch between them). At the time you install it, it downloads the latest nightly build of Firefox. (You can update that nightly build by reinstalling qbrt.)
And the packaging support is primitive. qbrt creates a shell script (batch script on Windows) to launch your app, and it packages your app using a platform-specific format (ZIP on Windows, DMG on Mac, and tar/gzip on Linux). But it doesn’t set icons nor most other package meta-data, and it doesn’t create auto-installers nor support signing the package.
In general, qbrt is immature and unstable! It’s appropriate for testing, but it isn’t yet ready for you to ship apps with it.
Nevertheless, I’m keen to hear how it works for you, and whether it supports your use cases. What would you want to do with it, and what additional features would you need from it?
18 thoughts on “Introducing qbrt”
This is awesome 🙂
* How does chrome://app/content/index.html get mapped? How does qbrt know to load index.html in the example app?
* Could I create additional chrome:// pages?
* Is it possible to open a window in full screen?
* What does the qb in qbrt stand for?
qbrt maps the chrome://app/content/ prefix to the directory containing the main entry script in Runtime.registerChromePrefix. There are several issues with the way it does that.
First, at the qbrt API level, the main entry script’s directory isn’t necessarily the right directory to register. The directory containing the package.json file is probably better. They’re the same for simpler apps like qbrt’s own example/ and shell/, but they could be different for more complex apps, where the main entry script is in some subdirectory, and other subdirectories contain other files that you want to access with the prefix.
Second, at the Gecko API level, the API for registering an arbitrary chrome prefix (after initial chrome registration in early startup) requires writing a file to the filesystem and then reading it back in. It should be possible to register a chrome prefix without having to indirect through the filesystem.
Third, once the app is packaged, it could include a manifest that registers the prefix during initial chrome registration. So qbrt should write that manifest when packaging the app instead of continuing to register the chrome URL via Runtime.registerChromePrefix.
Since chrome://app/content/ maps to the directory containing the main entry script, you can load any other page in that directory (or its subdirectories) by referencing it via a chrome://app/content/ URL, i.e. a page at foo/bar.html relative to the main entry script can be referenced at chrome://app/content/foo/bar.html.
You could also create additional prefixes (f.e. chrome://mycustomprefix/content/) by doing the same thing that registerChromePrefix does. Or we could generalize that method to register any prefix you specify (currently it’s hardcoded to only register chrome://app/content/).
However, assuming that all your app’s files are in the same directory as the main entry script (or a subdirectory of it), it should be possible to reference them all from chrome://app/content/.
There isn’t a way to specify that a window should open in fullscreen mode in the call to nsIWindowWatcher.openWindow (i.e. Services.ww.openWindow) itself, but you can enable fullscreen mode right afterward by setting window.fullScreen to
It stands for Quails Bark (Really Terribly). Or possibly Queens Boldly (Rule Territories). It definitely doesn’t stand for Quantum Browser (RunTime). 😉
Hah. These answers make me ridiculously happy. You’ve unblocked me on something I’ve been stuck on for over a year. See you on GitHub 😉
For the Thunderbird case, I could see that this approach would make a lot of sense.
For Thunderbird to transition off of being a binary rebuild of Firefox, there are three big conversions that we need: C++->JS, XUL->HTML, and XPCOM->Something else.
Is it fair to assume that your current approach supports the existing XUL and XPCOM technology in Firefox? It seems like me that it has to, as “Gecko’s existing APIs expose equivalent functionality” is mostly XPCOM. If so, qbrt would be an interesting intermediate target for Thunderbird that would allow focusing on the C++->JS conversion piece, which I estimate is 10 person-years of effort.
Yes, qbrt exposes the existing XPCOM APIs, and it allows an app’s main entry script to load XUL windows.
However, note this discussion about removing some XPCOM APIs once WebExtensions are the only supported extension model:
Ooo this looks interesting.
Hey if you intend to rewrite a full blown mail client (a’ka Thunderbird sans XUL) as a Q-bertable JS files, I would love to know that there will be a way to build addons for it. Obviously I would have to rewrite the XUL part, but the XPCOM is the biggest headache. It would be great if we could just write a very primitive wrapper that gives access to the internal namespace but obivously everything will be gone if you start writing from scratch.
Hi looks very interesting. At this stage do you have any idea about the bundle size for a packaged version of a distrubutable app?
I just packaged a simple “Hello, World!” app on Mac, and it’s 89MB. Package sizes will vary by app and platform, of course, so the sizes could be different on Windows and Linux, and with more complex apps.
I recently ran across a similar project on itch.io called HTMLE — perhaps you two should chat and compare notes: https://gritfish.itch.io/htmle
Hey Andrew, thanks for the tip, I’ll take a look!
What is the difference between qbrt and Electron?
The primary difference is that Electron embeds Blink, while qbrt reuses a distribution of Firefox as a runtime. A secondary difference is that Electron embeds Node, while qbrt exposes Firefox’s internal APIs to apps.
The application package use with Electron is very large(more than 100M). What about qbrt ?
Very interesting, are you see CRIAX-SDK, no nodejs all portable and with the Mozilla tecnologies?
No, I haven’t seen that. Unfortunately, I don’t have the language skills to be able to understand the documentation. Is there English-language documentation available, or is the source code available online?