How the Telegram Desktop Theme System Works: Loading Custom Themes and Color Schemes

Telegram Desktop loads custom themes from .tdesktop-theme files by parsing color schemes and optional background images into a style::palette, applying them through a preview-then-persist workflow while supporting real-time file watching and system accent color adjustments.

The Telegram Desktop theme system manages both built-in embedded themes and user-provided custom themes through a comprehensive C++ implementation. Located in the telegramdesktop/tdesktop repository, this system handles theme discovery, color adjustment, dynamic loading, and runtime switching including night mode transitions.

Theme Architecture Overview

The implementation centers around three distinct concepts that work together to provide flexible theming:

  • Embedded themes – Built-in themes shipped with the application (Default, Day-Blue, Night, Night-Green) stored as Qt resources in window/themes/window_themes_embedded.*
  • User-provided themes – .tdesktop-theme files containing a color scheme (colors.tdesktop-theme or colors.tdesktop-palette) and optional background images handled by window/themes/window_theme.*
  • Theme preview and editor – A temporary testing state that renders new palettes without persistence, managed by window/themes/window_theme_editor.* and window/themes/window_theme_preview.*

Theme Discovery and Color Adjustment

When Telegram Desktop encounters a theme file, it first determines whether it is an embedded resource via IsEmbeddedTheme(path). Embedded themes reside in Qt resources (:/gui/...) with fixed color schemes defined in EmbeddedThemes().

For accent color customization, the system checks if the user has enabled system accent colors or selected a custom accent in Settings. The ColorizerForTheme(path) function creates a style::colorizer that remaps the theme's primary color while preserving a whitelist of keys (kColorizeIgnoredKeys). This colorizer passes through to the theme parser in window/themes/window_theme.cpp.

The relevant implementations reside in:

Loading Custom Themes from Files

The core loading mechanism resides in Window::Theme::LoadFromFile, which validates and parses theme archives:

bool Window::Theme::LoadFromFile(
    const QString &path,
    not_null<Instance*> out,
    Cached *outCache,
    QByteArray *outContent) {
  const auto colorizer = ColorizerForTheme(path);
  return LoadFromFile(path, out, outCache, outContent, colorizer);
}

The loading process follows these steps:

  1. File validation – readThemeContent reads the file and checks against kThemeFileSizeLimit (5 MiB)
  2. Archive extraction – LoadTheme unpacks the .zip (or plain text) theme, extracting the color scheme (colors.tdesktop-theme or colors.tdesktop-palette) and optional background image (background.jpg/png)
  3. Color scheme parsing – loadColorScheme parses the scheme line-by-line using ReadPaletteValues and populates a style::palette, optionally applying the style::colorizer
  4. Background processing – If present, loadBackground extracts the image, validates dimensions against kBackgroundSizeLimit, and stores it in the Instance representing the parsed theme

Applying and Persisting Themes

The public API exposes several entry points for theme application:

  • Apply(const QString &filepath, const Data::CloudTheme &cloud) – Loads a theme file and initiates a testing session for preview
  • ApplyDefaultWithPath(const QString &themePath) – Used for built-in default or night themes, creating a Preview from embedded resources
  • KeepApplied() – Persists the temporary theme data when the user confirms a preview, writing to disk via Local::writeTheme and committing the background

During application, ChatBackground::setTestingTheme swaps the current palette through style::main_palette::apply and stores background images via setThemeData for UI rendering.

File Watching and Live Reloading

For themes loaded from regular files (non-embedded), Telegram Desktop establishes a QFileSystemWatcher in ChatBackground::refreshThemeWatcher. When the file changes on disk, the watcher triggers:

Apply(path);
KeepApplied();

This enables real-time theme development, allowing users to edit .tdesktop-theme files in external editors and see changes instantly without restarting the application.

Night Mode Implementation

Night mode operates as a thin wrapper around the standard theme logic:

  • ToggleNightMode() flips the boolean flag in ChatBackground
  • reapplyWithNightMode determines whether to load the night embedded theme (NightThemePath()) or a user-provided variant
  • The UI reacts to IsNightModeValue() (an rpl::producer) to update widgets automatically when the mode changes

Theme Editor Integration

The theme editor (window/themes/window_theme_editor_box.cpp/h) constructs temporary Preview objects via PreviewFromFile and hands them to Apply. While the editor remains open, the theme operates in testing mode. KeepFromEditor writes the final theme to disk when the user saves, while Revert restores the previous palette if the user cancels the operation.

Summary

  • Telegram Desktop distinguishes between embedded themes (Qt resources) and user-provided themes (.tdesktop-theme files) through IsEmbeddedTheme()
  • The LoadFromFile function enforces a 5 MiB size limit and extracts color schemes and backgrounds from ZIP archives
  • Testing mode allows previewing themes before persistence via Apply() and KeepApplied()
  • Real-time reloading works through QFileSystemWatcher for external file changes
  • Accent color adjustments apply via style::colorizer while protecting whitelisted keys
  • Night mode toggles between embedded themes using ToggleNightMode() and reapplyWithNightMode

Frequently Asked Questions

How does Telegram Desktop handle large theme files?

The system enforces kThemeFileSizeLimit (5 MiB) in window/themes/window_theme.cpp. Files exceeding this limit are rejected during the readThemeContent phase to prevent memory issues and ensure rapid parsing.

Can I modify a theme while Telegram Desktop is running and see changes immediately?

Yes. For non-embedded themes, the application creates a QFileSystemWatcher in ChatBackground::refreshThemeWatcher that monitors the theme file path. When changes are detected, it automatically calls Apply(path) followed by KeepApplied() to reload and persist the updated theme without requiring a restart.

What is the difference between Apply() and KeepApplied() in the theme system?

Apply() initiates a testing session that temporarily swaps the current palette and background for preview purposes without writing to persistent storage. KeepApplied() commits these changes permanently by writing the theme data to disk via Local::writeTheme and clearing the temporary state. This two-phase approach prevents incomplete or broken themes from corrupting the user's settings.

How does the accent color customization work with existing themes?

When loading any theme, ColorizerForTheme(path) checks for system or user-defined accent colors. If present, it creates a style::colorizer through ColorizerFrom that remaps specific color values while respecting kColorizeIgnoredKeys (a whitelist of colors that should not be modified). This colorizer passes into loadColorScheme to adjust the palette during parsing, allowing themes like Day-Blue to adapt to user preferences while maintaining visual consistency for critical UI elements.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →