UPDATE (2025-09-15) - I’ve put this in a proper repo, so you can import this directly with URL > Import via the command palette.
This is an early preview, one might even say proof of concept, of a base16 theming library for you to enjoy. Whether you enjoy it because it is useful or because it is a bit hacked together is up to you. This is the first project of any substance I’ve created with Lua, and I had to rely on Claude to help me navigate a few issues. I don’t expect this code is well optimized. But it works well enough that I wanted to present it so cleverer folks than I am can have a go at it.
UPDATE
Originally posted version had old tone mapping logic. Various subsequent updates to fix dumb issues, including a theme parsing issue.
GOAL
Specify a theme from the tinted-themes base16 repository by name and, transparently, generate appropriate stylesheet for both light and dark modes.
FEATURES
- Two config.set options,
schemeand, optionally,scheme-dark, which set arbitrary themes for light and dark mode. - Automatic discovery of inverse theme. If you specify
schemeonly, and you specify a light variant, the script will attempt to find the dark variant of the same theme. - If there is no inverse variant (e.g., ‘dracula’), the script will try to fake an inverse theme by remapping the neutral values in reverse (base00 effectively becomes base07). So, in the case of dracula, it will try to create “dracula-light” by inverting the neutrals.
TODO
- Intelligent identification of light and dark themes based on YAML contents and base00 mapping (to avoid the situation where a dark theme is assigned to light mode)
- Identify inefficiencies and fix
- A favorite of one of my professional colleagues, “make better throughout”
- Handle user-defined “local” base16 themes
- base24 support?
- Fix pretty much all of the stylesheet mappings for tonexx and basexx, because there are some awkward pre-alpha issues with things like selection and selected text being the same color.
- Fix what might either be some strange caching issue or flagrant user error.
TO USE
Drop this space-lua in a file. I’m using Library/User/Styles/base16.
-- ==============================================================================
-- BASE16 THEME LOADER FOR SILVERBULLET
-- ==============================================================================
-- Automatically generates Base16 color schemes for SilverBullet
-- Fetches themes from the official Base16 repository and applies them
--
-- Usage:
-- 1. Drop this file into your SilverBullet space
-- 2. Configure theme in SETTINGS with: base16.scheme: "theme-name"
-- 3. Optionally set base16.scheme-dark: "dark-theme-name" for explicit dark theme
-- 4. Reload SilverBullet - theme will auto-apply
--
-- Available themes: https://github.com/tinted-theming/schemes/tree/spec-0.11/base16
-- ==============================================================================
-- priority: 95
theme = theme or {}
-- ==============================================================================
-- CONFIGURATION SCHEMA
-- ==============================================================================
if config and type(config.define) == "function" then
config.define("base16", {
type = "object",
properties = {
scheme = {
type = "string",
description = "Base16 theme slug for light mode (e.g., 'monokai', 'solarized-light')"
},
["scheme-dark"] = {
type = "string",
description = "Base16 theme slug for dark mode (optional - if not set, will try to find inverse or generate from light theme)"
}
},
additionalProperties = false
})
end
-- ==============================================================================
-- CONSTANTS
-- ==============================================================================
local BASE_URL = "https://raw.githubusercontent.com/tinted-theming/schemes/spec-0.11/base16/"
-- ==============================================================================
-- UI STYLESHEET TEMPLATES
-- ==============================================================================
local UI_TEMPLATE = [[
color-scheme: light;
/* Typography */
--ui-font: "Open Sans","Helvetica",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";
--editor-font: "0xProto","iA-Mono" !important;
--editor-width: 960px !important;
/* Root */
--root-background-color: var(--base00);
--root-color: var(--base07);
/* Panels */
--panel-background-color: var(--base00);
--panel-border-color: var(--base01);
/* Top bar */
--top-color: var(--base07);
--top-background-color: var(--base01);
--top-border-color: var(--base03);
--top-sync-error-color: var(--base07);
--top-sync-error-background-color: var(--base0a);
--top-saved-color: var(--base07);
--top-unsaved-color: var(--base07);
--top-loading-color: var(--base05);
/* Bottom bar */
--bhs-background-color: var(--base00);
--bhs-border-color: var(--base01);
/* Notifications */
--notifications-background-color: var(--base01);
--notifications-border-color: var(--base03);
/* Buttons */
--button-background-color: var(--base01);
--button-hover-background-color: var(--base02);
--button-color: var(--base07);
--button-border-color: var(--base05);
--primary-button-background-color: var(--base0d);
--primary-button-hover-background-color: color-mix(in srgb, var(--base0d), var(--base00) 35%);
--primary-button-color: var(--base00);
--primary-button-border-color: transparent;
--action-button-background-color: transparent;
--action-button-color: var(--base07);
--action-button-hover-color: var(--base0c);
--action-button-active-color: var(--base0);
/* Inputs */
--text-field-background-color: var(--base01);
/* Editor */
--editor-caret-color: var(--base07);
--editor-selection-background-color: var(--base01);
--editor-panels-bottom-color: var(--base07);
--editor-panels-bottom-background-color: var(--base02);
--editor-panels-bottom-border-color: color-mix(in srgb, var(--base02), var(--base07) 20%);
/* Editor completion */
--editor-completion-detail-color: var(--base05);
--editor-completion-detail-selected-color: var(--base02);
/* Editor elements */
--editor-list-bullet-color: color-mix(in srgb, var(--base07), var(--base00) 50%);
--editor-heading-color: var(--base07);
--editor-heading-meta-color: color-mix(in srgb, var(--base07), var(--base00) 60%);
--editor-ruler-color: color-mix(in srgb, var(--base07), var(--base00) 45%);
--editor-code-color: color-mix(in srgb, var(--base07), var(--base00) 35%);
/* Links */
--link-color: var(--base0d);
--link-missing-color: var(--base09);
--editor-link-color: var(--link-color);
--editor-link-url-color: var(--link-color);
--editor-link-meta-color: color-mix(in srgb, var(--base07), var(--base00) 60%);
--editor-naked-url-color: var(--link-color);
/* Wiki links */
--editor-wiki-link-page-background-color: color-mix(in srgb, var(--base0c), transparent 93%);
--editor-wiki-link-page-color: var(--link-color);
--editor-wiki-link-page-missing-color: var(--link-missing-color);
--editor-wiki-link-color: color-mix(in srgb, var(--base0d), var(--base00) 35%);
--editor-named-anchor-color: color-mix(in srgb, var(--base07), var(--base00) 60%);
/* Command buttons */
--editor-command-button-color: var(--base07);
--editor-command-button-background-color: var(--base01);
--editor-command-button-hover-background-color: var(--base02);
--editor-command-button-meta-color: color-mix(in srgb, var(--base07), var(--base00) 60%);
--editor-command-button-border-color: var(--base04);
/* Meta colors */
--editor-line-meta-color: color-mix(in srgb, var(--base07), var(--base00) 60%);
--editor-meta-color: var(--base0f);
/* Tables */
--editor-table-head-background-color: var(--base05);
--editor-table-head-color: var(--base00);
--editor-table-even-background-color: var(--base02);
/* Blockquotes */
--editor-blockquote-background-color: color-mix(in srgb, var(--base07), var(--base00) 90%);
--editor-blockquote-color: var(--base04);
--editor-blockquote-border-color: var(--base05);
/* Code blocks */
--editor-code-background-color: color-mix(in srgb, var(--base07), var(--base00) 90%);
--editor-struct-color: var(--base0f);
--editor-highlight-background-color: color-mix(in srgb, var(--base0a), transparent 50%);
/* Syntax highlighting */
--editor-code-comment-color: color-mix(in srgb, var(--base07), var(--base00) 60%);
--editor-code-variable-color: var(--base0c);
--editor-code-typename-color: var(--base0b);
--editor-code-string-color: var(--base0d);
--editor-code-number-color: var(--base0b);
--editor-code-operator-color: color-mix(in srgb, var(--base07), var(--base00) 50%);
--editor-code-info-color: color-mix(in srgb, var(--base07), var(--base00) 55%);
--editor-code-atom-color: var(--base0f);
/* Frontmatter */
--editor-frontmatter-background-color: color-mix(in srgb, var(--base0a), transparent 70%);
--editor-frontmatter-color: color-mix(in srgb, var(--base07), var(--base00) 55%);
--editor-frontmatter-marker-color: var(--base07);
/* Widgets */
--editor-widget-background-color: var(--base01);
--editor-task-marker-color: color-mix(in srgb, var(--base07), var(--base00) 55%);
--editor-task-state-color: color-mix(in srgb, var(--base07), var(--base00) 55%);
/* UI accents */
--ui-accent-color: var(--base0d);
--ui-accent-text-color: var(--base0d);
--ui-accent-contrast-color: var(--base00);
/* Subtle elements */
--meta-subtle-color: color-mix(in srgb, var(--base07), var(--base00) 55%);
--subtle-color: color-mix(in srgb, var(--base07), var(--base00) 55%);
--subtle-background-color: color-mix(in srgb, var(--base07), var(--base00) 90%);
/* Hashtags */
--editor-hashtag-background-color: var(--base0c);
--editor-hashtag-color: var(--base07);
--editor-hashtag-border-color: color-mix(in srgb, var(--base0c), transparent 58%);
/* Admonitions */
--admonition-width: 1.25rem;]]
local DARK_UI_TEMPLATE = [[
color-scheme: dark;]]
-- ==============================================================================
-- HELPER FUNCTIONS
-- ==============================================================================
function fetchTheme(scheme_name)
--editor.flashNotification("Fetching theme: " .. scheme_name)
--local url = BASE_URL .. scheme_name .. ".yaml"
--editor.flashNotification("Fetching URL: " .. url)
local success, result = pcall(function()
return http.request(BASE_URL .. scheme_name .. ".yaml")
end)
--editor.flashNotification("HTTP success: " .. tostring(success))
if not success then
--editor.flashNotification("HTTP error: " .. tostring(result))
return false, nil
end
if not result or not result.body then
--editor.flashNotification("No response body")
return false, nil
end
--editor.flashNotification("Body length: " .. string.len(result.body))
--editor.flashNotification("First 100 chars: " .. string.sub(result.body, 1, 100))
local palette = {}
local match_count = 0
for key, val in result.body:gmatch("base(%x%x)%s*:%s*['\"]?([#]?%x%x%x%x%x%x)['\"]?") do
match_count = match_count + 1
local clean = string.gsub(val, "#", "")
local lower = string.lower(clean)
if string.len(lower) == 6 and string.match(lower, "^%x+$") then
palette["base" .. string.lower(key)] = "#" .. lower
end
end
--editor.flashNotification("Regex matches: " .. match_count .. " | Palette size: " .. tostring(#palette))
if match_count == 0 then
--editor.flashNotification("YAML sample: " .. string.sub(result.body, 1, 200))
end
return true, palette
end
function findInverseTheme(scheme)
local inverse_candidates = {}
--editor.flashNotification("Input scheme: '" .. tostring(scheme) .. "' (type: " .. type(scheme) .. ")")
if string.find(scheme, "light") then
local result1 = string.gsub(scheme, "light", "dark")
local result2 = string.gsub(scheme, "-light", "-dark")
local result3 = scheme:gsub("light", "dark")
table.insert(inverse_candidates, result1)
table.insert(inverse_candidates, result2)
table.insert(inverse_candidates, result3)
end
for _, candidate in ipairs(inverse_candidates) do
local success, palette = fetchTheme(candidate)
if success then
return candidate, palette
end
end
return nil, nil
end
function generateThemeVars(palette, swap_neutrals)
local rules = {}
-- DEBUG: Check what we're getting
--editor.flashNotification("generateThemeVars called with swap_neutrals: " .. tostring(swap_neutrals))
if not palette then
--editor.flashNotification("ERROR: base16 palette is nil!")
return ""
end
-- Count palette entries
local count = 0
for k, v in pairs(palette) do
count = count + 1
--editor.flashNotification("Palette: " .. k .. " = " .. v)
end
--editor.flashNotification("Palette has " .. count .. " entries")
-- Rest of function unchanged...
if swap_neutrals then
for i = 0, 7 do
local from_key = string.format("base%02x", i)
local to_key = string.format("base%02x", 7-i)
if palette[from_key] then
rules[#rules + 1] = string.format(" --%s: %s;", to_key, palette[from_key])
end
end
else
for i = 0, 7 do
local key = string.format("base%02x", i)
if palette[key] then
rules[#rules + 1] = string.format(" --%s: %s;", key, palette[key])
end
end
end
for i = 8, 15 do
local key = string.format("base%02x", i)
if palette[key] then
rules[#rules + 1] = string.format(" --%s: %s;", key, palette[key])
end
end
--editor.flashNotification("Generated " .. #rules .. " CSS rules")
return table.concat(rules, "\n")
end
-- ==============================================================================
-- MAIN THEME APPLICATION FUNCTION
-- ==============================================================================
function theme.applyFromConfig()
local light_scheme = "default-light"
local dark_scheme = nil
local light_success, config_result = pcall(function() return config.get("base16.scheme") end)
--editor.flashNotification("Config read success: " .. tostring(light_success))
--editor.flashNotification("Config result: " .. tostring(config_result) .. " (type: " .. type(config_result) .. ")")
if light_success and type(config_result) == "string" and config_result ~= "" then
light_scheme = config_result
--editor.flashNotification("Using scheme: " .. light_scheme)
else
--editor.flashNotification("Falling back to default-light")
end
local dark_success, dark_config_result = pcall(function() return config.get("base16.scheme-dark") end)
if dark_success and type(dark_config_result) == "string" and dark_config_result ~= "" then
dark_scheme = dark_config_result
end
-- Fetch light theme
local light_success, light_palette = fetchTheme(light_scheme)
if not light_success then
error("Failed to fetch light theme: " .. light_scheme)
end
-- Determine dark theme
local dark_palette = nil
local used_dark_scheme = light_scheme
if dark_scheme then
-- 1. Explicit dark theme specified
local dark_success, explicit_dark_palette = fetchTheme(dark_scheme)
if dark_success then
dark_palette = explicit_dark_palette
used_dark_scheme = dark_scheme
end
elseif string.find(light_scheme, "light") or string.find(light_scheme, "dark") then
-- 4. Scheme has light/dark in name, try to find inverse
local inverse_scheme, inverse_palette = findInverseTheme(light_scheme)
if inverse_scheme then
dark_palette = inverse_palette
used_dark_scheme = inverse_scheme
end
end
-- 3. If no explicit dark theme and scheme doesn't have light/dark in name,
-- we'll swap base00-07 below
-- Generate CSS variables
local light_vars = generateThemeVars(light_palette, false)
local dark_vars
if dark_palette then
-- Use separate dark theme palette
dark_vars = generateThemeVars(dark_palette, false)
else
-- Generate swapped colors from light theme
dark_vars = generateThemeVars(light_palette, true)
used_dark_scheme = light_scheme .. " (inverted)"
end
-- Generate CSS content
local css_content = string.format([[/* Base16 Stylesheet: %s / %s */
/* Generated by Base16 Loader */
/* priority: 99 */
html {
%s
%s
}
html[data-theme="light"] {
color-scheme: light;
}
html[data-theme="dark"] {
%s
%s
}
/* Admonition styling */
.sb-admonition[admonition="note"] .sb-admonition-type::before { width: var(--admonition-width) !important; }
.sb-admonition[admonition="note"] .sb-admonition-type * { display: none; }
.sb-admonition[admonition="note"] {
--admonition-icon: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="16" x2="12" y2="12"></line><line x1="12" y1="8" x2="12.01" y2="8"></line></svg>');
--admonition-color: #00b8d4;
}
.sb-admonition[admonition="warning"] .sb-admonition-type::before { width: var(--admonition-width) !important; }
.sb-admonition[admonition="warning"] .sb-admonition-type * { display: none; }
.sb-admonition[admonition="warning"] {
--admonition-icon: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"></path><line x1="12" y1="9" x2="12" y2="13"></line><line x1="12" y1="17" x2="12.01" y2="17"></line></svg>');
--admonition-color: #ff9100;
}]], light_scheme, used_dark_scheme, light_vars, UI_TEMPLATE, dark_vars, DARK_UI_TEMPLATE)
-- Write stylesheet
local page_content = string.format("```space-style\n%s\n```", css_content)
space.writePage("Library/User/Styles/Base16 Stylesheet", page_content)
return true
end
-- ==============================================================================
-- AUTOMATIC APPLICATION ON STARTUP
-- ==============================================================================
event.listen{
name = "system:ready",
run = function()
local success, result = pcall(theme.applyFromConfig)
if success then
--editor.flashNotification("Base16 stylesheet generated!")
else
--editor.flashNotification("Base16 error: " .. tostring(result))
end
end
}