Chat about this codebase

AI-powered code exploration

Online

Project Overview

Custom Theme Studio is a desktop-only Obsidian plugin that enables users to create, customize, and export themes through an integrated CSS editor. It leverages Obsidian’s built-in CSS variables (obsidian.css), provides live previews, and packages themes for sharing.

Main Capabilities

  • Interactive CSS editor with live preview and variable management
  • Built-in support for light and dark modes
  • One-click export of complete theme packages (CSS + manifest)
  • Import and refine existing themes

Problems It Solves

  • Eliminates manual theme file editing and reloads
  • Ensures consistent styling across modes and UI components
  • Simplifies versioning and distribution of themes
  • Lowers barrier for new theme authors

Core Components

  • manifest.json: Plugin metadata (id: custom-theme-studio, version: 1.0.1, desktop-only)
  • obsidian.css: Reference for Obsidian’s CSS variables, layout tokens, and UI components
  • package.json: Development scripts, dependencies, and build configuration

Sample manifest.json Snippet

{
  "id": "custom-theme-studio",
  "name": "Custom Theme Studio",
  "version": "1.0.1",
  "minAppVersion": "0.12.0",
  "author": "Your Name",
  "description": "Create, customize, and export Obsidian themes via CSS editor.",
  "isDesktopOnly": true
}

Example CSS Customization

/* Change primary accent color */
:root {
  --text-accent: #ff75a0;
}

/* Dark-mode background override */
.theme-custom.theme-dark {
  --background-primary: #1e1e2e;
}
## Getting Started

This guide covers installing Custom Theme Studio in Obsidian—either from the Community Plugin directory or by building from source—and walking through your first theme tweak.

### Prerequisites

- Obsidian v1.4+  
- Node.js v14+ (for local builds)  
- A vault with Community Plugins enabled

### Installation via Obsidian Community Plugins

1. In Obsidian, open Settings → Community Plugins.  
2. Turn off Safe Mode if enabled.  
3. Browse Community Plugins → Search for “Custom Theme Studio”.  
4. Click “Install” → “Enable”.  

Custom Theme Studio now appears in the command palette and as a sidebar icon.

### Building from Source

Follow these steps to customize or contribute to the plugin.

#### 1. Clone and Install Dependencies

```bash
git clone https://github.com/gapmiss/custom-theme-studio.git
cd custom-theme-studio
npm install

2. Development Mode (Live Rebuild)

npm run dev
  • Runs esbuild in watch mode.
  • On each save, esbuild rebuilds main.js.
  • Reload Obsidian or use the Reload command (Ctrl/Cmd+R) to pick up changes.

3. Production Build

npm run build
  • Outputs a production bundle in dist/.
  • Exits on completion.

4. Install Locally in Your Vault

  1. Create a plugin folder in your vault:

    • macOS/Linux:
      mkdir -p ~/YourVault/.obsidian/plugins/custom-theme-studio
      
    • Windows (PowerShell):
      New-Item -ItemType Directory "$env:USERPROFILE\YourVault\.obsidian\plugins\custom-theme-studio"
      
  2. Copy build artifacts:

    cp dist/main.js manifest.json ~/YourVault/.obsidian/plugins/custom-theme-studio/
    
  3. In Obsidian, open Settings → Community Plugins → Disabled Plugins → Enable “Custom Theme Studio”.

First Theme Tweak

  1. Open the command palette (Ctrl/Cmd+P), run Open Custom Theme Studio.

  2. In the CSS editor panel, add a rule. For example, to change heading colors:

    /* Change H1 color in preview */
    .markdown-preview-view h1 {
      color: #e91e63;
    }
    
  3. Click Save & Apply. Observe the preview update immediately.

  4. When satisfied, export your theme via the “Export Theme” button.

You’re now ready to explore advanced customization and share your themes!

Using Custom Theme Studio

Customize your Obsidian theme day-to-day by editing variables, adding CSS rules, picking UI elements by clicking, live-previewing styles, and exporting or importing your theme bundle.


Managing Custom CSS Variables (CSSVariableManager.updateVariable)

Handle adding, updating, or removing CSS variables in plugin settings and persist changes.

Signature
updateVariable(name: string, value: string, parent: string): void

Parameters

  • name – CSS variable name (e.g. "--accent-h").
  • value – New value (e.g. "300"), or "" to remove override.
  • parent – Variable category (e.g. "colors").

Behavior

  1. Reads plugin.settings.customVariables: CSSVariable[].
  2. Finds matching entry by variable === name and parent === parent.
    • If found & value !== '': updates entry.value.
    • If found & value === '': removes entry.
  3. If not found & value !== '': pushes new { parent, variable: name, value }.
  4. Calls plugin.saveSettings().

Code Example

import { CSSVariableManager } from 'src/managers/cssVariableManager';

const cssVarManager = new CSSVariableManager(plugin);
const input = document.createElement('input');
input.type = 'text';
input.placeholder = 'Enter --accent-h value';

input.addEventListener('input', (e) => {
  const newValue = (e.target as HTMLInputElement).value;
  cssVarManager.updateVariable('--accent-h', newValue, 'colors');
  // Live-apply if theme is enabled
  if (plugin.settings.themeEnabled) {
    plugin.themeManager.applyCustomTheme();
  }
});
containerEl.appendChild(input);

Usage Patterns

  • Add/Change: pass non-empty value.
  • Reset: pass "" to remove override.
  • All calls persist automatically; theme updates via themeManager.applyCustomTheme().

Editing Custom CSS Elements (CSSEditor)

Create and manage CSS rules for specific selectors, live-preview them, and save to settings.

Core Methods

  • createEditorSection(container: HTMLElement): void
  • setSelector(selector: string, focus?: boolean): void
  • applyChanges(): void
  • saveElement(element: CSSElement): void

Code Example

import CSSEditor, { CSSElement } from 'src/managers/cssEditor';

// Initialize editor in your view
const cssEditor = new CSSEditor(plugin, view);
cssEditor.createEditorSection(plugin.settingsContainerEl);

// Pre-fill selector (e.g. from last pick)
cssEditor.setSelector(plugin.settings.lastSelectedSelector, true);

// Programmatically add a new rule
const newRule: CSSElement = {
  id: 'rule-1',
  selector: '.modal-dialog',
  css: 'background-color: var(--background-primary); padding: 8px;',
  enabled: true
};
cssEditor.saveElement(newRule);

// Persist and live-preview changes
cssEditor.applyChanges();

Inline Editing
The editor integrates CodeMirror. User edits CSS in the UI, and changes apply on each keystroke. Calling applyChanges() writes plugin.settings.customCSSElements and re-renders theme.


Interactive Element Picker (ElementSelector)

Point-and-click any Obsidian UI element to auto-populate the CSS editor with a robust selector.

Flow

  1. User clicks “Select element” button in your view.
  2. elementSelector.startElementSelection() attaches global mouse handlers, shows tooltip, highlights on hover.
  3. On click:
    • generateSelector(el, evt.altKey) returns default or specific selector.
    • Saves to plugin.settings.lastSelectedSelector, calls plugin.saveSettings().
    • Injects into cssEditor via setSelector() and showEditorSection(true).

Key Snippets

// Start selection
selectBtn.addEventListener('click', () => {
  cssEditor.resetEditor();
  cssEditor.showEditorSection(false);
  elementSelector.startElementSelection();
});

// Generate selector
generateSelector(el: HTMLElement, useSpecific = false): string {
  const tag = el.tagName.toLowerCase();
  if (el.id) return `${tag}#${el.id}`;
  if (!useSpecific && el.hasAttribute('aria-label')) {
    const v = this.escapeAttributeValue(el.getAttribute('aria-label')!);
    return `${tag}[aria-label="${v}"]`;
  }
  // fallback: tag + classes, data-*, etc.
  return `${tag}.${[...el.classList].join('.')}`;
}

// On element click
selectElement(el: HTMLElement, evt: MouseEvent) {
  const selector = this.generateSelector(el, evt.altKey);
  this.plugin.settings.lastSelectedSelector = selector;
  this.plugin.saveSettings();
  this.view.cssEditor.setSelector(selector, false);
  this.view.cssEditor.showEditorSection(true);
}

Tips

  • Hold Alt for full-specific selector (classes, data-attributes).
  • Default picks use aria-label or shortest data-*.
  • Styles:
    • .cts-element-picker-active on <body>
    • .cts-element-picker-hover on hovered element
    • .cts-element-picker-tooltip for info box

Live Previewing and Applying Changes

Both variable updates and CSS element edits apply instantly:

  • Variables: CSSVariableManager.updateVariable() calls plugin.saveSettings(). Then call themeManager.applyCustomTheme() to inject updated CSS variables.
  • Elements: CSSEditor applies inline styles as you type. Call applyChanges() to persist and rebuild the theme bundle.
// After multiple edits
await plugin.themeManager.applyCustomTheme();
showNotice('Theme updated', 'success');

Exporting and Importing Themes

Exporting

Use ThemeManager to bundle CSS and manifest for distribution:

  • exportThemeCSS(): Promise<void>
    • Gathers CSS variables & custom CSS.
    • Prepends header comments (exportThemeName, exportAuthor, exportURL).
    • Formats via Prettier if exportPrettierFormat is true.
    • Triggers download of theme.css.

  • exportThemeManifest(): void
    • Builds manifest.json with kebab-cased name, version, author, URLs.
    • Triggers download.

  • copyThemeToClipboard(): Promise<void>
    • Generates CSS bundle, formats, copies to clipboard.

  • copyManifestToClipboard(): void
    • Builds JSON, copies to clipboard.

Example

// In a button handler:
await plugin.themeManager.exportThemeCSS();
plugin.themeManager.exportThemeManifest();
await plugin.themeManager.copyThemeToClipboard();
plugin.themeManager.copyManifestToClipboard();

Importing Configuration

Restore your complete Custom Theme Studio settings:

  1. Open plugin Settings → Import Configuration.
  2. Select a JSON file exported earlier.
  3. Plugin loads settings, calls plugin.saveSettings(), and re-renders the view and theme.

You can also import programmatically:

// Pseudo-code
import { readFileSync } from 'fs';
const data = JSON.parse(readFileSync('cts-config.json', 'utf-8'));
plugin.settings = data;
await plugin.saveSettings();
await plugin.themeManager.applyCustomTheme();

Tips

  • Ensure exportThemeName, exportAuthor, exportURL, and formatting options are set before export.
  • Use import/export buttons in the Settings UI for one-click operations.

Configuration & Settings

This section lists every option exposed in the Settings tab, explains how they persist in Obsidian, and shows how to import/export and validate configurations.

Settings Schema

Import the core interface and defaults from the settings module:

import {
  CustomThemeStudioSettings,
  DEFAULT_SETTINGS
} from '../settings';
import { ICodeEditorConfig } from '../interfaces/types';

Properties on CustomThemeStudioSettings:

• enableLivePreview (boolean)
Toggle live CSS preview as you edit variables or custom selectors.

• darkModeOnly (boolean)
Apply your custom theme only when Obsidian is in dark mode.

• globalCSS (boolean)
Append generated CSS to the app’s global stylesheet vs. only the workspace pane.

• editorConfig (ICodeEditorConfig)
Configure the integrated Ace editor (theme, keyboard, font, line numbers, wrap).

• exportScope ('workspace' | 'vault')
Choose to export CSS to your workspace folder or vault root.

• includeComments (boolean)
Include variable names and section comments in the exported CSS file.

• cssVariables (Record<string, string>)
Map of CSS variable names to their current values.

• enabledElements (string[])
List of CSS selectors or component IDs you’ve toggled on/off for export.

Accessing and Updating Settings

Load your settings on plugin startup and register the Settings tab:

// main.ts
export default class CustomThemeStudio extends Plugin {
  settings: CustomThemeStudioSettings;

  async onload() {
    this.settings = Object.assign(
      {},
      DEFAULT_SETTINGS,
      await this.loadData()
    );
    this.addSettingTab(
      new CustomThemeStudioSettingTab(this.app, this)
    );
  }

  // Save whenever you mutate this.settings
  async saveSettings() {
    await this.saveData(this.settings);
  }
}

Update a setting programmatically:

// Disable live preview
this.settings.enableLivePreview = false;
await this.saveSettings();
// Refresh your UI or apply changes here...

Importing and Exporting Settings

Leverage settingsIO for cross-platform backup and restore:

import settingsIO from './settings/settingsIO';

// Export current settings
await settingsIO.exportSettings(this.settings, this.app);

// Import and validate
const imported = await settingsIO.importSettings(this.app);
if (imported) {
  // Replace, persist, and refresh
  this.settings = imported;
  await this.saveData(imported);
  this.settingTab.reload();
}

Key points:

  • Electron builds prompt a native file-save/open dialog.
  • Mobile/web fall back to reading/writing CTS_settings.json in vault root.
  • settingsIO.validateSettings(obj) ensures structure before acceptance.
  • Success/failure notices surface automatically via Obsidian’s notice API.

Resetting to Defaults

Reset all customizations back to the shipped defaults:

import { DEFAULT_SETTINGS } from '../settings';

// Reset
this.settings = { ...DEFAULT_SETTINGS };
await this.saveSettings();
this.settingTab.reload();
new Notice('Custom Theme Studio settings reset.');

Runtime Validation

If you dynamically load external JSON, guard against invalid shapes:

import settingsIO from './settings/settingsIO';

async function loadExternalConfig(path: string) {
  const raw = await vaultAdapter.read(path);
  const data = JSON.parse(raw);
  if (settingsIO.validateSettings(data)) {
    this.settings = data;
    await this.saveSettings();
  } else {
    new Notice('Invalid Custom Theme Studio configuration.');
  }
}

Use these patterns to ensure your plugin always operates on a valid, up-to-date settings object.

Development & Contribution Guide

This guide covers cloning, environment setup, coding standards, building, testing, and releasing new versions to GitHub and the Obsidian Community Plugin registry.

Prerequisites

  • Node.js v16+ and npm
  • Git
  • Obsidian (for manual plugin testing)

1. Clone & Setup

git clone https://github.com/gapmiss/custom-theme-studio.git
cd custom-theme-studio
npm install

2. Coding Standards

EditorConfig

.editorconfig enforces UTF-8, LF line endings, final newline, tabs of width 4:

root = true

[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
indent_style = tab
indent_size = 4
tab_width = 4

ESLint

.eslintrc extends recommended rules for TypeScript; .eslintignore skips node_modules/ and main.js.

Install and configure:

npm install --save-dev eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin

Add to package.json:

"scripts": {
  "lint": "eslint \"src/**/*.ts\""
}

Run linting:

npm run lint

3. Building & Development

esbuild Configuration

esbuild.config.mjs bundles src/main.ts into main.js, excludes Obsidian/Electron APIs, supports watch mode in development and optimized one-time build in production.

npm Scripts

"scripts": {
  "dev": "node esbuild.config.mjs",
  "build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production"
}
  • npm run dev
    Starts a watcher; rebuilds main.js on any src/**/*.ts change.
  • npm run build
    Runs type-check then bundles optimized output.

To add new peer-dependencies or APIs (e.g. Obsidian/Electron) to the bundle exclusion, update the external array in esbuild.config.mjs.


4. Type Checking

tsconfig.json enables strict mode, ESNext modules, source maps.
Run TypeScript checking separately:

"scripts": {
  "typecheck": "tsc -noEmit"
}
npm run typecheck

5. Release Workflow

Scripts in package.json

"scripts": {
  "release": "node release.mjs",
  "release:minor": "node release.mjs minor",
  "release:major": "node release.mjs major",
  "version": "node version-bump.mjs"
}

Creating a Release

# Patch release
npm run release

# Minor release
npm run release:minor

# Major release
npm run release:major

This performs:

  1. Git cleanliness check
  2. Bumps package.json version
  3. Syncs manifest.json and versions.json
  4. Runs build and type-check
  5. Commits artifacts (main.js, manifest.json, styles.css)
  6. Tags commit (vX.Y.Z) and pushes
  7. (Optional) Creates GitHub Release via gh release create

On failure, it rolls back version changes and PRs. For manual version sync without full release, use:

npm run version

6. Publishing to Obsidian Community Plugins

  1. Confirm manifest.json fields: id, name, version, minAppVersion.
  2. Push tagged release to GitHub.
  3. Open a pull request against the Obsidian Community Plugins repo, updating the plugin entry with the new manifest.json.
  4. Once merged, your version appears in Obsidian’s Community Plugins browser.

By following these steps, contributors maintain consistent code style, streamline development, and automate releases for seamless updates.