Repository navigation
Themes
Themes are data, not code. Every color, size, type size and timing of the window's chrome comes from one JSON file, themes/default.json. Edit it while the window is open and the chrome follows.
-
themes/default.jsonbeside the executable, else in its parent directory -
--theme <file.json>on the command line - The theme last chosen from the command palette, kept as
settings.jsonin the profile folder ({"theme": "<file>"}, see Running Sashfold) and read before the window opens - Every token has a built-in default; a missing or malformed token falls back to it, and each problem is named
Four more themes ship beside the default, each a plain theme file that overrides only the colors it changes: light.json (Sashfold Light), nord.json (Nord), solarized-dark.json (Solarized Dark) and high-contrast.json (High Contrast, with a two-pixel border). Every .json in the folder beside the shipped theme that parses clean and names itself is offered by the command palette (Ctrl+Shift+P, then Theme:) and by the main menu's Themes, where the one in use is checked — drop your own file in, and it is on the list under the name it gives itself. Choosing one puts it on at once, the window follows that file from then on (edit it and the chrome follows), and the choice is kept for the next start. A theme is one file: post it anywhere, and anyone can drop it beside theirs.
The profile folder (see Running Sashfold) has a themes folder of its own. A theme file of ours put there is offered beside the shipped ones. So is a Firefox or a Chrome theme: put its .xpi, its .crx, a .zip of it or its unpacked folder there and it is converted into a theme of ours — into themes/converted/<its name>/, a theme.json with its pictures beside it and an import-notes.txt — and is on the list under its own name, from the palette and from the main menu's Themes. The folder is looked at when the window starts and every two seconds while it is open, so a theme dropped in is there the next time the list is opened. None of that folder needs knowing, though: themes are found and put on inside the window. about:themes — the main menu's Themes → More themes…, or the palette — lists the reader's themes, each a click to put on, and under them a gallery of Firefox's: names, previews and how many use each, read from the add-ons site's public catalogue when the page is opened and at no other time, with a search box, three orders (most used, top rated, newest) and pages. Put on is one click: the theme's .xpi is fetched and converted from the bytes that arrive, it is put on at once, it joins the reader's themes (and the Themes menu) from then on, the page stays where it was, and no file is left in the downloads folder. The Chrome Web Store publishes no catalogue to read, so its themes cannot be listed; the page has a box for a store theme's address (or its id) instead, and the theme's .crx is fetched by that id. Both stores are linked from the page. The same holds wherever a theme arrives from: a link to an .xpi or a .crx on any page puts the theme on in the same way, and what arrives and is no theme — an extension, a zip of something else — is the download it would have been. sashfold --import-theme <file or folder> [-o <folder>] does the same from a terminal, prints the theme file's path, and lists what it could not carry over.
It is a converter, not a runtime: only the theme part of the manifest is read, and nothing of theirs runs. What a theme says lands token for token — the frame and its color when the window is not in front, the toolbar, the tabs' text, the front tab, its line, the icons, a button pointed at and one held down, the fields at rest and with the focus with their selection, the menus, the new-tab page, the header's pictures with their alignment and tiling (theme_frame, additional_backgrounds; Chrome's theme_frame, theme_frame_overlay, theme_toolbar, theme_tab_background, theme_ntp_background). Colors are read in every form those manifests use: [r, g, b] with an optional alpha from 0 to 1, and for Firefox any CSS color — hex, a name, rgb(), hsl(). A name written as __MSG_key__ is looked up in the theme's own _locales. Chrome's tints are worked into the colors and pictures written, by that browser's own rule: a hue to take, a saturation and a lightness moved towards, 0.5 leaving each as it is and −1 leaving it alone.
What a theme leaves unsaid is filled in as its browser would fill it: from a light or a dark palette chosen by the theme's own words (its color_scheme, else whether its text is bright), the fallbacks between its keys (the front tab's text from the toolbar's, the icons' from the text's, the front tab's color from the toolbar's), background tabs that are the frame seen through, a toolbar that veils the header's picture when the theme gives it no color, and for Chrome the frame of a window not in front under that browser's default tint for one. Every color token is written out, so a converted theme does not change when our defaults do. What a theme says that has no surface here yet — a sidebar's colors, a tab's loading color, Chrome's incognito variants — is listed by key in import-notes.txt, never dropped in silence.
Colors — chrome-background, chrome-text, chrome-text-muted, chrome-border, tab-active-background, tab-inactive-background, tab-hover-background, address-background, address-text, address-border, accent, selection, find-highlight, find-current, hint-background, hint-text, button-hover-background, button-disabled-text, status-background, status-text, content-background, secure-indicator, insecure-indicator
Colors of a surface's own — whatever pops up over the window, a menu or the command palette, draws from popup-background, popup-border, popup-text, popup-text-muted (a shortcut, a row that is not highlighted), popup-disabled-text, popup-highlight and popup-highlight-text. A theme need not name them: one it leaves out takes that theme's own value of another token — chrome-background, address-border, chrome-text, chrome-text-muted, button-disabled-text, button-hover-background and chrome-text, in that order — never the built-in default, so a light theme written before menus existed has light menus. Every surface added from here on gets tokens of its own this way, each the counterpart of a key that Firefox and Chrome themes carry (popup, popup_border, popup_text, popup_highlight, popup_highlight_text for these), so that such a theme's colors have somewhere to land one to one.
The same goes for what the older tokens used to cover between them. The toolbar has toolbar-background, apart from the tab in front (from tab-active-background), its buttons' icons toolbar-icon (from chrome-text) and the words on its own surface — the bookmarks bar's titles — toolbar-text (from chrome-text; Firefox's toolbar_text and bookmark_text, Chrome's bookmark_text) — the toolbar and icons of a browser theme. The icons are line drawings of the shell's own — back, forward, reload, reader mode, the main menu, a new tab, a tab's close, the window's three controls — drawn at four sevenths of button-size by a rasterizer that works in whole numbers only, so that the chrome's pictures are the same bytes on every system (see Testing). The tab in front has tab-text for its title (from chrome-text; the other tabs' stays chrome-text-muted) and tab-line, a line along its top edge, which is transparent unless a theme names it and sits under a container's stripe. A field with the focus — the address bar, the find box, the palette's — has address-background-focus, address-text-focus and address-border-focus (from address-background, address-text and accent), and the band over its selected text is address-selection (from selection): toolbar_field_focus, toolbar_field_text_focus, toolbar_field_border_focus and toolbar_field_highlight. None of the shipped themes names any of these, and each paints exactly as it did.
A window that is not the one in front wears another frame: chrome-background-inactive is what shows behind the tabs then — a browser theme's frame_inactive. The system says when the window is in front (the activated state of an xdg_toplevel on Wayland, WM_ACTIVATE on Windows). A theme that does not name the token gets its own frame dimmed by the rule Chrome dims its frame by — a light frame 28% of the way to white, a dark one a little more saturated and 13% of the way — so that where the shell draws its own title bar it can be seen which window has the keyboard; this is the one derived token that follows its source through a rule and not as it stands. A theme that wants its frame the same either way names the two alike, as a converted Firefox theme does, that browser leaving such a frame as it is.
Three more of the same kind: button-active-background (from button-hover-background) is a toolbar button while it is held down — button_background_active; toolbar-top-separator is a line along the toolbar's top, broken where the tab in front runs into the toolbar, which is transparent unless a theme names it — toolbar_top_separator; and address-selection-text (from address-text-focus) is a field's text over its selection band — toolbar_field_highlight_text.
Everything over the frame composites. Give toolbar-background, tab-active-background, tab-inactive-background, tab-hover-background or address-background an alpha (#rrggbbaa) and the frame — its color, and its pictures below — shows through. A background tab whose color is the frame's, or is fully transparent, shows the frame; the pointer's color, tab-hover-background, goes over a background tab's color and pictures both, so a translucent white lightens whatever the tab wears. A box with a border — a field, a menu, the palette — draws the border around its inside and never under it, so a translucent inside shows what is behind the box, not the border's color.
The shape of the tabs — "tab-shape", beside the theme's name: "attached" (as unsaid) or "floating". Attached, a tab's foot runs into the toolbar and only its top corners are round, the tab in front and the toolbar one surface, as Chrome draws them. Floating, a tab is a rounded box of its own in the middle of the strip, with the strip showing above and below it, as Firefox draws them — and tab-line is then an outline all round the tab in front, where attached it is a line along its top. A converted theme sits its tabs as its own browser does: a Firefox theme's white front tab over a blue toolbar is a pill that was meant, not a tab that does not match.
Metrics — tab-strip-height, tab-height, tab-max-width, tab-min-width, tab-corner-radius, tab-gap, toolbar-height, bookmarks-bar-height, bookmark-max-width, address-height, address-corner-radius, button-size, button-corner-radius, padding, border-width, status-height, find-height, devtools-height, scroll-step, tab-icon-size
Type — font-size, tab-font-size, status-font-size, and font-family: the face the chrome's words are set in, a list of families as CSS writes one ("Inter, 'Noto Sans', sans-serif"), the first the machine has being the one used. Unsaid, it is the machine's own interface face, then its sans-serif. The words are measured and kerned by that face — a title is cut where it no longer fits, a caret stands where the words before it end — and whatever the face lacks comes from the next family, then from the built-in face. The fonts a page brings along are never used, whatever name they go by.
Timings — tab-hover, tab-switch
New tab ("new-tab": { … }) — backgrounds, a folder of pictures (PNG, JPEG, GIF or BMP) written relative to the theme file, which the new-tab page rotates through in name order; rotate, how long each picture stays before the next fades in, in milliseconds (0 never rotates; 20000 by default). A theme that names no folder gets a gradient of its own colors. The page's colors are the chrome's unless the theme names them here: background and text — a browser theme's ntp_background and ntp_text — with text-muted for the date and background-end for the gradient's far end. Unnamed, the page runs from chrome-background towards the accent in chrome-text and chrome-text-muted, as it always has; a background named alone is that color flat, as those browsers show theirs, and naming both ends gives the gradient between them. A new-tab page that is open when another theme is put on is made again in that theme's colors.
Pictures ("images": { … }) — frame, toolbar and tab-background: pictures laid over the chrome (PNG, JPEG, GIF, BMP or SVG), each written relative to the theme file. A surface takes one picture or a list of them, front to back, and a picture is its path alone or an object:
"images": {
"frame": [
"header.png",
{ "picture": "weave.png", "align": "left top", "tile": "repeat" }
],
"toolbar": { "picture": "brushed.png", "align": "left top", "tile": "repeat-x" }
}A picture may move. An animated PNG or an animated GIF plays, which is what an animated Firefox theme is made of — "Hacker (Animated)" is a 39-frame animated PNG tiled across the header, "Black Rain (Animated)" a 72-frame GIF as its frame — and an imported one plays as it did there. The frames are composed the way each format says (a GIF's frame left, cleared or put back; an animated PNG's dispose and blend operations; a delay of a hundredth of a second or less played as a tenth; a file that plays a number of times stopping on its last frame), and a picture is kept as its file and one frame, decoded as each comes due, not as every frame it has. Only the header is painted again for a frame — the page under it is not — so a moving theme costs about two milliseconds a frame in the real window, and a window without one still does nothing while it idles. A window nobody can see — minimized, wholly covered, on another desktop, as the compositor says through xdg_toplevel's suspended state, or minimized on Windows — stops its pictures and is not woken for them; shown again, it goes on from the frame it stopped on.
align is one or two of left, center, right, top and bottom, a side named alone leaving the other axis centered; tile is no-repeat, repeat, repeat-x or repeat-y. Unsaid, a picture is held at the top right and shown once, as a Firefox theme's is. All three surfaces are placed against one area — the tab strip and the toolbar together, from the window's top left — so that a picture runs on unbroken from the tab in front into the toolbar; they differ in where they show. frame shows behind everything in that area, over the frame's color: a browser theme's theme_frame, and behind it a Firefox theme's additional_backgrounds with their alignment and tiling. toolbar shows over the toolbar's color and the front tab's (theme_toolbar), and tab-background over every other tab (theme_tab_background). A picture's size is in CSS px, so at two device px per CSS px it is drawn twice the size, like the chrome around it. A picture that cannot be read, or is in no format decoded here, is left out and named on stderr, and the rest of the theme is put on. A surface takes sixteen pictures at most, and a picture 16 MB as a file and sixteen million pixels.
Colors are written as #rrggbb or #rrggbbaa.
A unit test asserts that the shipped themes/default.json equals the built-in defaults token for token, so the file and the code cannot drift apart. The chrome goldens in CI (see Testing) are rendered from the same file. tests/shell/frame.script puts on a theme of translucent colors over held and tiled pictures and asserts pixels worked out by hand — the toolbar over the frame, a field over the toolbar, a picture's corners at one and at two device px per CSS px, the frame of a window not in front — and each rule it checks has been seen to fail with its fault planted.
Getting started
What works today
Measured
Design decisions
Testing and tools