Module: Canon::DiffFormatter::Theme

Defined in:
lib/canon/diff_formatter/theme.rb

Overview

Theme definitions for diff display.

Theme is a nested hash structure:

  • diff: removed/added/changed/unchanged/formatting/informative

  • xml: tag/attribute_name/attribute_value/text/comment/cdata

  • html: same as xml

  • structure: line_number/pipe/context

  • visualization: space/tab/newline/nbsp

  • display_mode: :separate/:inline/:mixed

Each styled element has: color, bg, bold, underline, strikethrough, italic

Defined Under Namespace

Classes: Resolver, ThemeInheritance, ValidationResult

Constant Summary collapse

VALID_COLORS =

Valid ANSI color values (standard 16 + common extended colors) Standard: 8 colors + 8 bright variants Extended: light_ variants (for backgrounds), amber (retro terminal)

%i[
  default black red green yellow blue magenta cyan white
  bright_black bright_red bright_green bright_yellow bright_blue bright_magenta bright_cyan bright_white
  light_red light_green light_blue light_cyan light_magenta light_yellow light_black light_white
  amber
].freeze
VALID_DISPLAY_MODES =

Valid display modes

%i[separate inline mixed].freeze
STYLING_PROPERTIES =

Base properties for any styled element

%i[color bg bold underline strikethrough
italic].freeze
LIGHT =

LIGHT THEME - Light terminal backgrounds, professional use

{
  name: "Light",
  description: "Light terminal backgrounds - professional, high contrast",

  diff: {
    removed: {
      marker: { color: :red, bg: :light_red, bold: false },
      content: { color: :red, bg: nil, bold: false, underline: false,
                 strikethrough: true },
    },
    added: {
      marker: { color: :green, bg: :light_green, bold: false },
      content: { color: :green, bg: nil, bold: false, underline: false,
                 strikethrough: false },
    },
    changed: {
      marker: { color: :bright_red, bg: nil, bold: true },
      content_old: { color: :bright_red,   bg: nil, bold: true,
                     underline: false, strikethrough: true },
      content_new: { color: :bright_green, bg: nil, bold: true,
                     underline: true, strikethrough: false },
    },
    unchanged: {
      content: { color: :default, bg: nil, bold: false, underline: false,
                 strikethrough: false },
    },
    formatting: {
      marker: { color: :bright_blue, bg: nil, bold: false },
      content: { color: :bright_blue, bg: nil, bold: false,
                 underline: false, strikethrough: false },
    },
    informative: {
      marker: { color: :bright_magenta, bg: nil, bold: false },
      content: { color: :bright_magenta, bg: nil, bold: false,
                 underline: false, strikethrough: false },
    },
  },

  xml: {
    tag: { color: :bright_blue, bg: nil, bold: true, italic: false },
    attribute_name: { color: :magenta, bg: nil, bold: false,
                      italic: false },
    attribute_value: { color: :green, bg: nil, bold: false,
                       italic: false },
    text: { color: :default, bg: nil, bold: false, italic: false },
    comment: { color: :magenta, bg: nil, bold: false, italic: true },
    cdata: { color: :yellow, bg: nil, bold: false, italic: false },
  },

  html: {
    tag: { color: :bright_blue, bg: nil, bold: true, italic: false },
    attribute_name: { color: :magenta, bg: nil, bold: false,
                      italic: false },
    attribute_value: { color: :green, bg: nil, bold: false,
                       italic: false },
    text: { color: :default, bg: nil, bold: false, italic: false },
    comment: { color: :magenta, bg: nil, bold: false, italic: true },
    cdata: { color: :yellow, bg: nil, bold: false, italic: false },
  },

  structure: {
    line_number: { color: :black },
    pipe: { color: :black },
    context: { color: :default },
  },

  visualization: {
    space: "",
    tab: "",
    newline: "",
    nbsp: "",
  },

  display_mode: :separate,
}.freeze
DARK =

DARK THEME - Dark terminal backgrounds, developer favorite

{
  name: "Dark",
  description: "Dark terminal backgrounds - saturated colors, no backgrounds",

  diff: {
    removed: {
      marker: { color: :red, bg: nil, bold: false },
      content: { color: :red, bg: nil, bold: false, underline: false,
                 strikethrough: true },
    },
    added: {
      marker: { color: :green, bg: nil, bold: false },
      content: { color: :green,       bg: nil, bold: false,
                 underline: false, strikethrough: false },
    },
    changed: {
      marker: { color: :yellow,       bg: nil, bold: true },
      content_old: { color: :bright_red,   bg: nil, bold: false,
                     underline: false, strikethrough: true },
      content_new: { color: :bright_green, bg: nil, bold: false,
                     underline: true, strikethrough: false },
    },
    unchanged: {
      content: { color: :default, bg: nil, bold: false, underline: false,
                 strikethrough: false },
    },
    formatting: {
      marker: { color: :bright_blue, bg: nil, bold: false },
      content: { color: :bright_blue, bg: nil, bold: false,
                 underline: false, strikethrough: false },
    },
    informative: {
      marker: { color: :cyan, bg: nil, bold: false },
      content: { color: :cyan, bg: nil, bold: false, underline: false,
                 strikethrough: false },
    },
  },

  xml: {
    tag: { color: :bright_blue, bg: nil, bold: true, italic: false },
    attribute_name: { color: :magenta, bg: nil, bold: false,
                      italic: false },
    attribute_value: { color: :green, bg: nil, bold: false,
                       italic: false },
    text: { color: :default, bg: nil, bold: false, italic: false },
    comment: { color: :cyan, bg: nil, bold: false, italic: true },
    cdata: { color: :yellow, bg: nil, bold: false, italic: false },
  },

  html: {
    tag: { color: :bright_blue, bg: nil, bold: true, italic: false },
    attribute_name: { color: :magenta, bg: nil, bold: false,
                      italic: false },
    attribute_value: { color: :green, bg: nil, bold: false,
                       italic: false },
    text: { color: :default, bg: nil, bold: false, italic: false },
    comment: { color: :cyan, bg: nil, bold: false, italic: true },
    cdata: { color: :yellow, bg: nil, bold: false, italic: false },
  },

  structure: {
    line_number: { color: :white },
    pipe: { color: :white },
    context: { color: :default },
  },

  visualization: {
    space: "",
    tab: "",
    newline: "",
    nbsp: "",
  },

  display_mode: :separate,
}.freeze
RETRO =

RETRO THEME - Amber CRT, low blue light, accessibility

{
  name: "Retro",
  description: "Amber CRT phosphor - monochromatic amber, low blue light, high accessibility",

  diff: {
    removed: {
      # Bright amber on amber background = inverse video, highest emphasis
      marker: { color: :bright_yellow, bg: :yellow, bold: true },
      content: { color: :bright_yellow, bg: :yellow, bold: true,
                 underline: false, strikethrough: false },
    },
    added: {
      # Bright white = less emphasis than removed, but distinct from normal text
      marker: { color: :bright_white, bg: nil, bold: true },
      content: { color: :bright_white, bg: nil, bold: false,
                 underline: false, strikethrough: false },
    },
    changed: {
      marker: { color: :bright_yellow, bg: :yellow, bold: true },
      content_old: { color: :bright_yellow, bg: :yellow, bold: true,
                     underline: false, strikethrough: true },
      content_new: { color: :bright_white, bg: nil, bold: false,
                     underline: true, strikethrough: false },
    },
    unchanged: {
      content: { color: :yellow, bg: nil, bold: false, underline: false,
                 strikethrough: false },
    },
    formatting: {
      # Dimmer amber + strikethrough = clearly different from normal text
      marker: { color: :yellow, bg: nil, bold: false,
                strikethrough: true },
      content: { color: :yellow, bg: nil, bold: false, underline: false,
                 strikethrough: true },
    },
    informative: {
      # Bright amber + underline = distinct from formatting and normal
      marker: { color: :bright_yellow, bg: nil, bold: true,
                underline: true },
      content: { color: :bright_yellow, bg: nil, bold: true,
                 underline: true, strikethrough: false },
    },
  },

  xml: {
    # Amber monochrome for all XML elements
    tag: { color: :bright_yellow, bg: nil, bold: true, italic: false },
    attribute_name: { color: :bright_yellow, bg: nil, bold: false,
                      italic: false },
    attribute_value: { color: :bright_yellow, bg: nil, bold: false,
                       italic: false },
    text: { color: :yellow, bg: nil, bold: false, italic: false },
    comment: { color: :yellow, bg: nil, bold: false, italic: true },
    cdata: { color: :bright_yellow, bg: nil, bold: false, italic: false },
  },

  html: {
    tag: { color: :bright_yellow, bg: nil, bold: true, italic: false },
    attribute_name: { color: :bright_yellow, bg: nil, bold: false,
                      italic: false },
    attribute_value: { color: :bright_yellow, bg: nil, bold: false,
                       italic: false },
    text: { color: :yellow, bg: nil, bold: false, italic: false },
    comment: { color: :yellow, bg: nil, bold: false, italic: true },
    cdata: { color: :bright_yellow, bg: nil, bold: false, italic: false },
  },

  structure: {
    line_number: { color: :yellow },
    pipe: { color: :yellow },
    context: { color: :yellow },
  },

  visualization: {
    space: "",
    tab: "",
    newline: "",
    nbsp: "",
  },

  display_mode: :separate,
}.freeze
CLAUDE =

CLAUDE THEME - Claude Code diff style, high contrast HUD

{
  name: "Claude",
  description: "Claude Code diff style - red/green backgrounds, maximum visual pop",

  diff: {
    removed: {
      # Red background + white text = immediate visual pop
      marker: { color: :white, bg: :red, bold: true },
      content: { color: :white, bg: :red, bold: false, underline: false,
                 strikethrough: false },
    },
    added: {
      # Green background + white text (black invisible on dark terminals)
      marker: { color: :white, bg: :green, bold: true },
      content: { color: :white, bg: :green, bold: false,
                 underline: false, strikethrough: false },
    },
    changed: {
      marker: { color: :white, bg: :magenta, bold: true },
      content_old: { color: :bright_red,   bg: nil, bold: false,
                     underline: false, strikethrough: true },
      content_new: { color: :bright_green, bg: nil, bold: false,
                     underline: true, strikethrough: false },
    },
    unchanged: {
      content: { color: :default, bg: nil, bold: false, underline: false,
                 strikethrough: false },
    },
    formatting: {
      marker: { color: :yellow, bg: nil, bold: false },
      content: { color: :yellow, bg: nil, bold: false, underline: false,
                 strikethrough: false },
    },
    informative: {
      marker: { color: :bright_cyan, bg: nil, bold: false },
      content: { color: :bright_cyan, bg: nil, bold: false,
                 underline: false, strikethrough: false },
    },
  },

  xml: {
    tag: { color: :bright_blue, bg: nil, bold: true, italic: false },
    attribute_name: { color: :magenta, bg: nil, bold: false,
                      italic: false },
    attribute_value: { color: :green, bg: nil, bold: false,
                       italic: false },
    text: { color: :default, bg: nil, bold: false, italic: false },
    comment: { color: :cyan, bg: nil, bold: false, italic: true },
    cdata: { color: :yellow, bg: nil, bold: false, italic: false },
  },

  html: {
    tag: { color: :bright_blue, bg: nil, bold: true, italic: false },
    attribute_name: { color: :magenta, bg: nil, bold: false,
                      italic: false },
    attribute_value: { color: :green, bg: nil, bold: false,
                       italic: false },
    text: { color: :default, bg: nil, bold: false, italic: false },
    comment: { color: :cyan, bg: nil, bold: false, italic: true },
    cdata: { color: :yellow, bg: nil, bold: false, italic: false },
  },

  structure: {
    line_number: { color: :yellow },
    pipe: { color: :yellow },
    context: { color: :default },
  },

  visualization: {
    space: "",
    tab: "",
    newline: "",
    nbsp: "",
  },

  display_mode: :separate,
}.freeze
CYBERPUNK =

CYBERPUNK THEME - Neon on black, high contrast, futuristic

{
  name: "Cyberpunk",
  description: "Neon on black - high contrast, futuristic, electric",

  diff: {
    removed: {
      # Hot pink/magenta neon for deletions
      marker: { color: :bright_magenta, bg: nil, bold: true },
      content: { color: :bright_magenta, bg: nil, bold: true,
                 underline: false, strikethrough: true },
    },
    added: {
      # Electric cyan neon for additions
      marker: { color: :bright_cyan, bg: nil, bold: true },
      content: { color: :bright_cyan, bg: nil, bold: true,
                 underline: false, strikethrough: false },
    },
    changed: {
      # Yellow warning neon for change markers
      marker: { color: :bright_yellow, bg: nil, bold: true },
      content_old: { color: :bright_magenta, bg: nil, bold: false,
                     underline: false, strikethrough: true },
      content_new: { color: :bright_cyan,    bg: nil, bold: false,
                     underline: true, strikethrough: false },
    },
    unchanged: {
      content: { color: :default, bg: nil, bold: false, underline: false,
                 strikethrough: false },
    },
    formatting: {
      # Dim green for low-priority formatting
      marker: { color: :green, bg: nil, bold: false },
      content: { color: :green, bg: nil, bold: false, underline: false,
                 strikethrough: false },
    },
    informative: {
      # Bright yellow neon for informative
      marker: { color: :bright_yellow, bg: nil, bold: true },
      content: { color: :bright_yellow, bg: nil, bold: false,
                 underline: false, strikethrough: false },
    },
  },

  xml: {
    # Tags in bright cyan, attributes in hot magenta
    tag: { color: :bright_cyan, bg: nil, bold: true, italic: false },
    attribute_name: { color: :bright_magenta, bg: nil, bold: false,
                      italic: false },
    attribute_value: { color: :bright_green, bg: nil, bold: false,
                       italic: false },
    text: { color: :default, bg: nil, bold: false, italic: false },
    comment: { color: :green, bg: nil, bold: false, italic: true },
    cdata: { color: :bright_yellow, bg: nil, bold: false, italic: false },
  },

  html: {
    tag: { color: :bright_cyan, bg: nil, bold: true, italic: false },
    attribute_name: { color: :bright_magenta, bg: nil, bold: false,
                      italic: false },
    attribute_value: { color: :bright_green, bg: nil, bold: false,
                       italic: false },
    text: { color: :default, bg: nil, bold: false, italic: false },
    comment: { color: :green, bg: nil, bold: false, italic: true },
    cdata: { color: :bright_yellow, bg: nil, bold: false, italic: false },
  },

  structure: {
    line_number: { color: :bright_cyan },
    pipe: { color: :bright_cyan },
    context: { color: :default },
  },

  visualization: {
    space: "",
    tab: "",
    newline: "",
    nbsp: "",
  },

  display_mode: :separate,
}.freeze
THEMES =

Registry of all themes

{
  light: LIGHT,
  dark: DARK,
  retro: RETRO,
  claude: CLAUDE,
  cyberpunk: CYBERPUNK,
}.freeze

Class Method Summary collapse

Class Method Details

.[](name) ⇒ Object



614
615
616
617
618
619
# File 'lib/canon/diff_formatter/theme.rb', line 614

def self.[](name)
  theme = THEMES[name] || raise(ArgumentError,
                                "Unknown theme: #{name}. Valid: #{THEMES.keys}")
  # Return a deep copy to prevent mutation of theme constants
  deep_dup(theme)
end

.deep_dup(obj) ⇒ Hash

Get a theme by name Deep copy a value, handling nested hashes and arrays

Parameters:

  • name (Symbol)

    Theme name

Returns:

  • (Hash)

    Theme hash

Raises:

  • (ArgumentError)

    if theme not found



597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
# File 'lib/canon/diff_formatter/theme.rb', line 597

def self.deep_dup(obj)
  case obj
  when Hash
    obj.transform_values { |v| deep_dup(v) }
  when Array
    obj.map { |v| deep_dup(v) }
  when String, Symbol, Numeric, TrueClass, FalseClass, NilClass
    obj
  else
    begin
      obj.dup
    rescue StandardError
      obj
    end
  end
end

.include?(name) ⇒ Boolean

Check if theme name exists

Parameters:

  • name (Symbol)

Returns:

  • (Boolean)


630
631
632
# File 'lib/canon/diff_formatter/theme.rb', line 630

def self.include?(name)
  THEMES.key?(name)
end

.inherit_from(base_name) ⇒ ThemeInheritance

Create a new theme by inheriting from a base theme and merging overrides

Parameters:

  • base_name (Symbol)

    Name of base theme (:light, :dark, :retro, :claude)

Returns:



460
461
462
# File 'lib/canon/diff_formatter/theme.rb', line 460

def self.inherit_from(base_name)
  ThemeInheritance.new(base_name)
end

.namesArray<Symbol>

List available theme names

Returns:

  • (Array<Symbol>)


623
624
625
# File 'lib/canon/diff_formatter/theme.rb', line 623

def self.names
  THEMES.keys
end

.resolver(config = nil) ⇒ Object

Singleton instance for convenience



859
860
861
# File 'lib/canon/diff_formatter/theme.rb', line 859

def self.resolver(config = nil)
  Resolver.new(config)
end

.validate(theme) ⇒ ValidationResult

Validate a theme hash has all required keys and valid values

Parameters:

  • theme (Hash)

    Theme hash to validate

Returns:



527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
# File 'lib/canon/diff_formatter/theme.rb', line 527

def self.validate(theme)
  missing_keys = []
  extra_keys = []
  invalid_values = []

  # Check top-level keys
  required_toplevel = %i[name description diff xml html structure
                         visualization display_mode]
  required_toplevel.each do |key|
    missing_keys << "top-level.#{key}" unless theme.key?(key)
  end

  # Validate diff section
  if theme[:diff]
    validate_diff_section(theme[:diff], missing_keys, extra_keys,
                          invalid_values)
  end

  # Validate xml section
  if theme[:xml]
    validate_xml_section(theme[:xml], missing_keys, extra_keys,
                         invalid_values)
  end

  # Validate html section
  if theme[:html]
    validate_xml_section(theme[:html], missing_keys, extra_keys,
                         invalid_values)
  end

  # Validate structure
  if theme[:structure]
    validate_structure_section(theme[:structure], missing_keys,
                               extra_keys, invalid_values)
  end

  # Validate visualization
  if theme[:visualization]
    validate_visualization_section(theme[:visualization], missing_keys,
                                   extra_keys, invalid_values)
  end

  # Validate display_mode
  if theme[:display_mode]
    unless VALID_DISPLAY_MODES.include?(theme[:display_mode])
      invalid_values << "display_mode must be one of #{VALID_DISPLAY_MODES}, got #{theme[:display_mode]}"
    end
  else
    missing_keys << "display_mode"
  end

  ValidationResult.new(
    valid: missing_keys.empty? && extra_keys.empty? && invalid_values.empty?,
    missing_keys: missing_keys,
    extra_keys: extra_keys,
    invalid_values: invalid_values,
  )
end

.validate_allHash{Symbol => ValidationResult}

Validate all predefined themes

Returns:



588
589
590
# File 'lib/canon/diff_formatter/theme.rb', line 588

def self.validate_all
  THEMES.transform_values { |theme| validate(theme) }
end