Running DOOM in my taskbar
So I got DOOM running inside my taskbar. The actual game, not a screenshot. A 64x40 pixel window sitting in my Zebar bar, playing the attract mode demo on loop. Click it and it blows up to 640x400 for real gameplay.

It’s built on js-dos, a WebAssembly port of DOSBox. The whole thing runs in the browser engine inside a Tauri webview. No native binaries, no shell commands, just WASM grinding through 1993 x86 code.
I built this with Claude Code, mostly because the debugging cycle on this was miserable and I needed something to absorb the frustration of staring at grey rectangles.
The js-dos version rabbit hole
This is the part that ate most of my time. js-dos has two major versions: v8 and 6.22. v8 is current. 6.22 is older and lighter. Obviously I went with v8.
v8 is a full React app. Redux store, play button, sidebar, toolbar, the works. It renders this whole player chrome around the actual DOSBox canvas. If you’re making a game page where someone lands and clicks play, great. If you’re trying to cram it into a 64-pixel-wide Tauri widget window, absolutely not.
But I didn’t find that out gracefully. First thing that happened: Identifier 'gc' has already been declared. v8’s bundle declares a global gc that apparently collides with something in the Tauri webview environment. The script refuses to load. DOSBox never initializes. You get a grey rectangle and nothing in the console that tells you what went wrong except that one error about gc.
I spent a while trying to work around this before accepting that v8 was just wrong for the job entirely. Even if the collision didn’t exist, I’d still be fighting to hide all the player UI. I don’t want a play button. I want raw DOSBox frames on a canvas.
Falling back to 6.22
6.22 is much simpler. Canvas in, promise out:
Dos(document.getElementById('canvas'), {
wdosboxUrl: 'https://js-dos.com/6.22/current/wdosbox.js',
}).ready(function(fs, main) {
fs.extract('./doom.jsdos').then(function() {
main(['-c', 'DOOM.EXE']).then(function(ci) {
console.log('DOOM ready');
});
});
});
No player chrome. Just DOSBox rendering onto a canvas. That’s all I wanted.
The bundle format matters
A .jsdos bundle is a ZIP file. The internal structure has to match the js-dos version you’re targeting, and I learned this the hard way.
First attempt, I structured it for v8:
.jsdos/dosbox.conf
DOOM/DOOM.EXE
DOOM/DOOM1.WAD
6.22 didn’t like this at all. The WASM module crashed on startup with an Uncaught Object pointing at Emscripten’s quit_ function. Not exactly a helpful error message. What was actually happening: DOSBox started, couldn’t find game files at the paths it expected, and called exit().
Flat bundle fixed it:
DOOM.EXE
DOOM1.WAD
No config file, no subdirectories. 6.22 auto-mounts the extracted files and you tell it what to run via command-line args.
Scaling a 320x200 canvas to 64x40
DOOM renders at 320x200. My widget window is 64x40. That’s roughly a 5x scale-down.
I tried object-fit: cover on the canvas first. Turns out object-fit only works on replaced elements like <img> and <video>. Canvas ignores it. You just see the top-left corner of the frame, cropped to the window size.
What actually works is CSS transform: scale():
function applyCompactScale() {
const canvas = document.getElementById('canvas');
const cw = canvas.width || 320;
const ch = canvas.height || 200;
const winW = presetSize ? presetSize.width : 64;
const winH = presetSize ? presetSize.height : 40;
const scale = Math.min(winW / cw, winH / ch);
canvas.style.transformOrigin = 'top left';
canvas.style.transform = 'scale(' + scale + ')';
}
The scale factor is computed from the actual canvas size rather than hardcoded, so if something changes the DOSBox resolution it still works. When expanding to 640x400 the transform gets cleared and the canvas fills the window normally.
DOSBox eats your keyboard
Original plan: press Escape to collapse the expanded window back to bar size. Obvious choice.
DOSBox captures all keyboard input. The browser never sees the keypress. And DOOM uses Escape for its own menu, obviously. So pressing Escape opens the DOOM pause menu instead of collapsing the widget. I sat there for a second watching the DOOM menu pop up thinking “right, of course.”
F9 works because DOSBox doesn’t care about it. There’s also an X button in the top-right for clicking.
Tauri permissions and window resizing
Small thing that wasted 10 minutes: I tried calling tauriWindow.setFocus() after expanding, thinking the game needed explicit focus to receive keyboard input. Zebar’s ACL doesn’t allow window:set_focus for widgets. Fails silently. Clicking the widget gives focus anyway, so the call was never needed.
The expand/collapse resizing reuses the pattern from my app launcher widget. Grab the preset geometry at startup:
const [size, pos] = await Promise.all([
tauriWindow.outerSize(),
tauriWindow.outerPosition(),
]);
const scale = window.devicePixelRatio;
presetSize = size.toLogical(scale);
presetPos = pos.toLogical(scale);
When expanding, check if there’s room below the widget. If not (bar is at the bottom of the screen), shift the window up and expand in that direction instead. Collapsing snaps everything back to the original size and position.
Rough edges
The initial compact scale runs on a 2-second setTimeout. It’s waiting for DOSBox to initialize and give the canvas real dimensions before calculating the scale factor. A ResizeObserver would be cleaner. The timeout works, but I wouldn’t bet on it surviving a slow CDN day.
Audio muting also didn’t make the cut. v8 had ci.mute() and ci.unmute() on the command interface, which was nice. 6.22’s command interface is less cooperative about this. The attract mode demos are silent by default so it doesn’t matter much, but it would be nice to toggle audio when expanding.
The result
Tiny DOOM window in my taskbar, attract mode looping away. Click to expand, F9 or X to collapse. About 150 lines of JS, a CSS file, and a shareware WAD running through WebAssembly. No build step.
No real reason for it to exist. It’s staying.