Base16 Theme Library - UPDATE

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, scheme and, optionally, scheme-dark, which set arbitrary themes for light and dark mode.
  • Automatic discovery of inverse theme. If you specify scheme only, 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
}
2 Likes

Thanks for taking the time to build this. Having easy access to the Base16 themes is very cool. Quick question on the config settings. Should it looks something like this?

config.set {
  base16 = {
    scheme = "solarized-light",
    scheme-dark = "solarized-dark"
  }
}

If so, it doesn’t seem to be working for me. It does result in an update to the Base16 Stylesheet, but no theme change.

1 Like

I’m using the following: config.set("base16.scheme","solarized-light")

If you use the version I updated to this morning (I know it’s sloppy to update inline, but, again, proof of concept more than prod code), that one line should do the following:

  1. Pull ayu-light from the repo
  2. Check if solarized-dark exists and pull it, taking alternate action if it doesn’t
  3. Update /Library/User/Styles/Base16 Stylesheet with the correct values.

You can add config.set("base16.scheme-dark","solarized-dark") as well if you want to be specific or pull a different alternate theme. (I just tried, and it works fine here, though the color assignments in the stylesheet still need some work. I’m not looking forward to that project.)

My questions would be:

A. Do you have another space-style block somewhere that might be overriding or conflicting with the generated one?
B. The version I updated to today has a bunch of troubleshooting alerts commented out. You might want to uncomment some of them (one or two at a time, or you’ll be overwhelmed) to see if you’re getting an error I’m not.
C. Before this morning, I had a pretty dumb issue with variable naming that was preventing most colors from being applied, so I’ve since fixed that.
D. Sanity check: Are you doing a CTRL-SHIFT-R to reload the site fully?

I’m going to put this in a repo properly as soon as I get a minute.

Thanks very much flkiwi. I’ve added the following directly to Library/User/Styles/base16:

config.set("base16.scheme","atelier-lakeside-light")
config.set("base16.scheme-dark","atelier-lakeside")

It’s working! :slight_smile: Well, the light theme is. Dark is not getting applied, but let me play around with it - it’s probably something I’ve done. The library is already super helpful - easier than pulling the base values into vi and reformatting (which I had been doing). Thanks again.

Interestingly, the style sheet does pick up the dark theme, it just not applying it:

/* Base16 Stylesheet: atelier-lakeside-light / atelier-lakeside */
/* Generated by Base16 Loader */
/* priority: 99 */

Oh, interesting. I add the config settings in a space-lua in a file called CONFIG, but putting it in the base16 file should get you to the same point.

Out of curiosity are the base00-base0A codes for atelier-lakeside-light and atelier-lakeside correct? (Specifically, is base00 dark-ish for one and light-ish for the other)? I was having some issues with how the theme hex values were parsed into the alternate variants, so you might have bumped into yet another parsing issue.

Yes, both set of base codes are getting set with the correct values. One thing that looks slightly odd: Both the light and dark blocks have their color-scheme value set at the bottom, for example:

html {
  --base00: #ebf8ff;
  --base01: #c1e4f6;
  --base02: #7ea2b4;
  --base03: #7195a8;
  --base04: #5a7b8c;
  --base05: #516d7b;
  --base06: #1f292e;
  --base07: #161b1d;
  --base08: #d22d72;
  --base09: #935c25;
  --base0a: #8a8a0f;
  --base0b: #568c3b;
  --base0c: #2d8f6f;
  --base0d: #257fad;
  --base0e: #6b6bb8;
  --base0f: #b72dd2;
  color-scheme: light;

But as well as this, there’s a standalone color-scheme set for light, but not dark.

html[data-theme="light"] {
  color-scheme: light;
}

I believe that has to do with establishing the default behavior (light), but I haven’t tried removing the second definition of “light” to see if anything bad happens. I’m getting the correct switching, but I did have some odd behavior when I started this that looked like a cache issue. What happens if you clear cache or, alternately, visit from another device?

No luck. FWIW, the SilverBullet version I’m running is 2.0.0-74-g77fb5d0d, just in case we’re on different versions and that’s related (though it seems unlikely).

Let me see if the issue is my side - I had been using the Base16 code that you’d uploaded in the past and though I think I’ve removed it all, there may be something hanging around. I may even try spinning up a new instance of SilverBullet.

Cheers.

I’d just delete the whole space-lua and paste in the update from today. See if that works. And, again, sorry that’s not very convenient (yet!)

I really like this! However, selected text in code blocks looks off, which makes it hard for me to use. I imagine it should be an easy fix.

Yep, the CSS has a number of silly and superficial mapping errors that I haven’t taken the time to fix. Maybe I’ll give it a go today.

1 Like

Ok, here we go. Pushed a new tag 0.2 (or just track the default branch, or edge if you’re feeling spicy) with the following changes:

  • Many, many stylesheet improvements. So many.
  • Added optional config settings for editor-width, ui-font, and editor-font to override SB defaults (instead of my hardcoded overrides before) if desired.
  • Added a check to stop processing if the config options haven’t changed (vs. what’s in the generated stylesheet).
1 Like

Well done, looks good now :slight_smile:

1 Like

Thank you! And thank you for testing this out. I think there’s room for some additional stylesheet optimization (and note I’m doing a little bit of an experiment making code blocks have SLIGHTLY more contrast than the theme), but it’s definitely an improvement.

Next up: user base16 themes.

You can put it in here, probably replace mine simple solutions:

Quick question - I see this is meant to be applied on every system:ready event, but for some reason, that’s not triggering when I terminate and then restart silverbullet.

Is there a special method that will reliably trigger system:ready or is there a way I can change the event to make this run, say, whenever I reload my space config?

(To clarify, the theme applied perfectly the first time I added the configuration…and now that theme’s just ‘stuck’. I’m confident it’s not doing the callback at all, as I cant get it to print a notification from that method either! Really weird!)

EDIT: Nevermind! I had accidentally wrapped the config.set() calls in a space-config block, rather than space-lua, which seemed to cause the issue. Swapping back to a space-lua block fixed the issue.

1 Like