Class: Tuile::StyledString::Style

Inherits:
Object
  • Object
show all
Defined in:
lib/tuile/styled_string.rb,
sig/tuile.rbs

Overview

A frozen value type describing the visual style of a Span. Colors are stored as Color instances (or nil for the terminal default); inputs to Style.new and #merge are coerced via Color.coerce, so the four accepted color forms — nil, Symbol, Integer 0..255, RGB Array — work transparently.

Constant Summary collapse

DEFAULT =

The style with no color and no attributes — what the terminal shows without any SGR applied.

Returns:

new

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Instance Attribute Details

#bgColor? (readonly)

Returns:



68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
# File 'lib/tuile/styled_string.rb', line 68

class Style < Data.define(:fg, :bg, :bold, :italic, :underline, :strikethrough, :inverse)
  # @param fg [Color, Symbol, Integer, Array<Integer>, nil] coerced via {Color.coerce}.
  # @param bg [Color, Symbol, Integer, Array<Integer>, nil] coerced via {Color.coerce}.
  # @param bold [Boolean]
  # @param italic [Boolean]
  # @param underline [Boolean]
  # @param strikethrough [Boolean]
  # @param inverse [Boolean]
  # @return [Style]
  # @raise [ArgumentError] when a color is not one of the accepted forms.
  def self.new(fg: nil, bg: nil, bold: false, italic: false, underline: false, strikethrough: false,
               inverse: false)
    super(fg: Color.coerce(fg), bg: Color.coerce(bg), bold:, italic:, underline:, strikethrough:, inverse:)
  end

  # The style with no color and no attributes — what the terminal shows
  # without any SGR applied.
  # @return [Style]
  DEFAULT = new

  # @return [Boolean]
  def default? = self == DEFAULT

  # Returns a new {Style} with the given attributes overridden.
  # @param overrides [Hash{Symbol => Object}]
  # @return [Style]
  def merge(**overrides) = self.class.new(**to_h.merge(overrides))

  # Minimal SGR escape that transitions a terminal already showing `self`
  # into `other`: only the attributes that differ are emitted. Returns
  # `""` when the styles are identical (nothing to do), and {Ansi::RESET}
  # (`\e[0m`, one code) when `other` is the default style — shorter than
  # turning each attribute off individually.
  # @param other [Style] the style to transition to.
  # @return [String]
  def sgr_to(other)
    return "" if self == other
    return Ansi::RESET if other.default?

    codes = []
    codes << (other.bold ? 1 : 22) if bold != other.bold
    codes << (other.italic ? 3 : 23) if italic != other.italic
    codes << (other.underline ? 4 : 24) if underline != other.underline
    codes << (other.strikethrough ? 9 : 29) if strikethrough != other.strikethrough
    codes << (other.inverse ? 7 : 27) if inverse != other.inverse
    codes.concat(color_codes(other.fg, target: :fg)) if fg != other.fg
    codes.concat(color_codes(other.bg, target: :bg)) if bg != other.bg
    return "" if codes.empty?

    "\e[#{codes.join(";")}m"
  end

  private

  # @param color [Color, nil]
  # @param target [Symbol] either `:fg` or `:bg`.
  # @return [Array<Integer>] SGR codes; `[39]` / `[49]` for the "default"
  #   reset when `color` is `nil`, otherwise delegated to {Color#sgr_codes}.
  def color_codes(color, target:)
    return [target == :fg ? 39 : 49] if color.nil?

    color.sgr_codes(target)
  end
end

#boldBoolean (readonly)

Returns:

  • (Boolean)


68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
# File 'lib/tuile/styled_string.rb', line 68

class Style < Data.define(:fg, :bg, :bold, :italic, :underline, :strikethrough, :inverse)
  # @param fg [Color, Symbol, Integer, Array<Integer>, nil] coerced via {Color.coerce}.
  # @param bg [Color, Symbol, Integer, Array<Integer>, nil] coerced via {Color.coerce}.
  # @param bold [Boolean]
  # @param italic [Boolean]
  # @param underline [Boolean]
  # @param strikethrough [Boolean]
  # @param inverse [Boolean]
  # @return [Style]
  # @raise [ArgumentError] when a color is not one of the accepted forms.
  def self.new(fg: nil, bg: nil, bold: false, italic: false, underline: false, strikethrough: false,
               inverse: false)
    super(fg: Color.coerce(fg), bg: Color.coerce(bg), bold:, italic:, underline:, strikethrough:, inverse:)
  end

  # The style with no color and no attributes — what the terminal shows
  # without any SGR applied.
  # @return [Style]
  DEFAULT = new

  # @return [Boolean]
  def default? = self == DEFAULT

  # Returns a new {Style} with the given attributes overridden.
  # @param overrides [Hash{Symbol => Object}]
  # @return [Style]
  def merge(**overrides) = self.class.new(**to_h.merge(overrides))

  # Minimal SGR escape that transitions a terminal already showing `self`
  # into `other`: only the attributes that differ are emitted. Returns
  # `""` when the styles are identical (nothing to do), and {Ansi::RESET}
  # (`\e[0m`, one code) when `other` is the default style — shorter than
  # turning each attribute off individually.
  # @param other [Style] the style to transition to.
  # @return [String]
  def sgr_to(other)
    return "" if self == other
    return Ansi::RESET if other.default?

    codes = []
    codes << (other.bold ? 1 : 22) if bold != other.bold
    codes << (other.italic ? 3 : 23) if italic != other.italic
    codes << (other.underline ? 4 : 24) if underline != other.underline
    codes << (other.strikethrough ? 9 : 29) if strikethrough != other.strikethrough
    codes << (other.inverse ? 7 : 27) if inverse != other.inverse
    codes.concat(color_codes(other.fg, target: :fg)) if fg != other.fg
    codes.concat(color_codes(other.bg, target: :bg)) if bg != other.bg
    return "" if codes.empty?

    "\e[#{codes.join(";")}m"
  end

  private

  # @param color [Color, nil]
  # @param target [Symbol] either `:fg` or `:bg`.
  # @return [Array<Integer>] SGR codes; `[39]` / `[49]` for the "default"
  #   reset when `color` is `nil`, otherwise delegated to {Color#sgr_codes}.
  def color_codes(color, target:)
    return [target == :fg ? 39 : 49] if color.nil?

    color.sgr_codes(target)
  end
end

#fgColor? (readonly)

Returns:



68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
# File 'lib/tuile/styled_string.rb', line 68

class Style < Data.define(:fg, :bg, :bold, :italic, :underline, :strikethrough, :inverse)
  # @param fg [Color, Symbol, Integer, Array<Integer>, nil] coerced via {Color.coerce}.
  # @param bg [Color, Symbol, Integer, Array<Integer>, nil] coerced via {Color.coerce}.
  # @param bold [Boolean]
  # @param italic [Boolean]
  # @param underline [Boolean]
  # @param strikethrough [Boolean]
  # @param inverse [Boolean]
  # @return [Style]
  # @raise [ArgumentError] when a color is not one of the accepted forms.
  def self.new(fg: nil, bg: nil, bold: false, italic: false, underline: false, strikethrough: false,
               inverse: false)
    super(fg: Color.coerce(fg), bg: Color.coerce(bg), bold:, italic:, underline:, strikethrough:, inverse:)
  end

  # The style with no color and no attributes — what the terminal shows
  # without any SGR applied.
  # @return [Style]
  DEFAULT = new

  # @return [Boolean]
  def default? = self == DEFAULT

  # Returns a new {Style} with the given attributes overridden.
  # @param overrides [Hash{Symbol => Object}]
  # @return [Style]
  def merge(**overrides) = self.class.new(**to_h.merge(overrides))

  # Minimal SGR escape that transitions a terminal already showing `self`
  # into `other`: only the attributes that differ are emitted. Returns
  # `""` when the styles are identical (nothing to do), and {Ansi::RESET}
  # (`\e[0m`, one code) when `other` is the default style — shorter than
  # turning each attribute off individually.
  # @param other [Style] the style to transition to.
  # @return [String]
  def sgr_to(other)
    return "" if self == other
    return Ansi::RESET if other.default?

    codes = []
    codes << (other.bold ? 1 : 22) if bold != other.bold
    codes << (other.italic ? 3 : 23) if italic != other.italic
    codes << (other.underline ? 4 : 24) if underline != other.underline
    codes << (other.strikethrough ? 9 : 29) if strikethrough != other.strikethrough
    codes << (other.inverse ? 7 : 27) if inverse != other.inverse
    codes.concat(color_codes(other.fg, target: :fg)) if fg != other.fg
    codes.concat(color_codes(other.bg, target: :bg)) if bg != other.bg
    return "" if codes.empty?

    "\e[#{codes.join(";")}m"
  end

  private

  # @param color [Color, nil]
  # @param target [Symbol] either `:fg` or `:bg`.
  # @return [Array<Integer>] SGR codes; `[39]` / `[49]` for the "default"
  #   reset when `color` is `nil`, otherwise delegated to {Color#sgr_codes}.
  def color_codes(color, target:)
    return [target == :fg ? 39 : 49] if color.nil?

    color.sgr_codes(target)
  end
end

#inverseBoolean (readonly)

@return — swap the fg/bg in effect at the cell (SGR 7) — including terminal defaults, which fg/bg can't name.

Returns:

  • (Boolean)


68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
# File 'lib/tuile/styled_string.rb', line 68

class Style < Data.define(:fg, :bg, :bold, :italic, :underline, :strikethrough, :inverse)
  # @param fg [Color, Symbol, Integer, Array<Integer>, nil] coerced via {Color.coerce}.
  # @param bg [Color, Symbol, Integer, Array<Integer>, nil] coerced via {Color.coerce}.
  # @param bold [Boolean]
  # @param italic [Boolean]
  # @param underline [Boolean]
  # @param strikethrough [Boolean]
  # @param inverse [Boolean]
  # @return [Style]
  # @raise [ArgumentError] when a color is not one of the accepted forms.
  def self.new(fg: nil, bg: nil, bold: false, italic: false, underline: false, strikethrough: false,
               inverse: false)
    super(fg: Color.coerce(fg), bg: Color.coerce(bg), bold:, italic:, underline:, strikethrough:, inverse:)
  end

  # The style with no color and no attributes — what the terminal shows
  # without any SGR applied.
  # @return [Style]
  DEFAULT = new

  # @return [Boolean]
  def default? = self == DEFAULT

  # Returns a new {Style} with the given attributes overridden.
  # @param overrides [Hash{Symbol => Object}]
  # @return [Style]
  def merge(**overrides) = self.class.new(**to_h.merge(overrides))

  # Minimal SGR escape that transitions a terminal already showing `self`
  # into `other`: only the attributes that differ are emitted. Returns
  # `""` when the styles are identical (nothing to do), and {Ansi::RESET}
  # (`\e[0m`, one code) when `other` is the default style — shorter than
  # turning each attribute off individually.
  # @param other [Style] the style to transition to.
  # @return [String]
  def sgr_to(other)
    return "" if self == other
    return Ansi::RESET if other.default?

    codes = []
    codes << (other.bold ? 1 : 22) if bold != other.bold
    codes << (other.italic ? 3 : 23) if italic != other.italic
    codes << (other.underline ? 4 : 24) if underline != other.underline
    codes << (other.strikethrough ? 9 : 29) if strikethrough != other.strikethrough
    codes << (other.inverse ? 7 : 27) if inverse != other.inverse
    codes.concat(color_codes(other.fg, target: :fg)) if fg != other.fg
    codes.concat(color_codes(other.bg, target: :bg)) if bg != other.bg
    return "" if codes.empty?

    "\e[#{codes.join(";")}m"
  end

  private

  # @param color [Color, nil]
  # @param target [Symbol] either `:fg` or `:bg`.
  # @return [Array<Integer>] SGR codes; `[39]` / `[49]` for the "default"
  #   reset when `color` is `nil`, otherwise delegated to {Color#sgr_codes}.
  def color_codes(color, target:)
    return [target == :fg ? 39 : 49] if color.nil?

    color.sgr_codes(target)
  end
end

#italicBoolean (readonly)

Returns:

  • (Boolean)


68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
# File 'lib/tuile/styled_string.rb', line 68

class Style < Data.define(:fg, :bg, :bold, :italic, :underline, :strikethrough, :inverse)
  # @param fg [Color, Symbol, Integer, Array<Integer>, nil] coerced via {Color.coerce}.
  # @param bg [Color, Symbol, Integer, Array<Integer>, nil] coerced via {Color.coerce}.
  # @param bold [Boolean]
  # @param italic [Boolean]
  # @param underline [Boolean]
  # @param strikethrough [Boolean]
  # @param inverse [Boolean]
  # @return [Style]
  # @raise [ArgumentError] when a color is not one of the accepted forms.
  def self.new(fg: nil, bg: nil, bold: false, italic: false, underline: false, strikethrough: false,
               inverse: false)
    super(fg: Color.coerce(fg), bg: Color.coerce(bg), bold:, italic:, underline:, strikethrough:, inverse:)
  end

  # The style with no color and no attributes — what the terminal shows
  # without any SGR applied.
  # @return [Style]
  DEFAULT = new

  # @return [Boolean]
  def default? = self == DEFAULT

  # Returns a new {Style} with the given attributes overridden.
  # @param overrides [Hash{Symbol => Object}]
  # @return [Style]
  def merge(**overrides) = self.class.new(**to_h.merge(overrides))

  # Minimal SGR escape that transitions a terminal already showing `self`
  # into `other`: only the attributes that differ are emitted. Returns
  # `""` when the styles are identical (nothing to do), and {Ansi::RESET}
  # (`\e[0m`, one code) when `other` is the default style — shorter than
  # turning each attribute off individually.
  # @param other [Style] the style to transition to.
  # @return [String]
  def sgr_to(other)
    return "" if self == other
    return Ansi::RESET if other.default?

    codes = []
    codes << (other.bold ? 1 : 22) if bold != other.bold
    codes << (other.italic ? 3 : 23) if italic != other.italic
    codes << (other.underline ? 4 : 24) if underline != other.underline
    codes << (other.strikethrough ? 9 : 29) if strikethrough != other.strikethrough
    codes << (other.inverse ? 7 : 27) if inverse != other.inverse
    codes.concat(color_codes(other.fg, target: :fg)) if fg != other.fg
    codes.concat(color_codes(other.bg, target: :bg)) if bg != other.bg
    return "" if codes.empty?

    "\e[#{codes.join(";")}m"
  end

  private

  # @param color [Color, nil]
  # @param target [Symbol] either `:fg` or `:bg`.
  # @return [Array<Integer>] SGR codes; `[39]` / `[49]` for the "default"
  #   reset when `color` is `nil`, otherwise delegated to {Color#sgr_codes}.
  def color_codes(color, target:)
    return [target == :fg ? 39 : 49] if color.nil?

    color.sgr_codes(target)
  end
end

#strikethroughBoolean (readonly)

Returns:

  • (Boolean)


68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
# File 'lib/tuile/styled_string.rb', line 68

class Style < Data.define(:fg, :bg, :bold, :italic, :underline, :strikethrough, :inverse)
  # @param fg [Color, Symbol, Integer, Array<Integer>, nil] coerced via {Color.coerce}.
  # @param bg [Color, Symbol, Integer, Array<Integer>, nil] coerced via {Color.coerce}.
  # @param bold [Boolean]
  # @param italic [Boolean]
  # @param underline [Boolean]
  # @param strikethrough [Boolean]
  # @param inverse [Boolean]
  # @return [Style]
  # @raise [ArgumentError] when a color is not one of the accepted forms.
  def self.new(fg: nil, bg: nil, bold: false, italic: false, underline: false, strikethrough: false,
               inverse: false)
    super(fg: Color.coerce(fg), bg: Color.coerce(bg), bold:, italic:, underline:, strikethrough:, inverse:)
  end

  # The style with no color and no attributes — what the terminal shows
  # without any SGR applied.
  # @return [Style]
  DEFAULT = new

  # @return [Boolean]
  def default? = self == DEFAULT

  # Returns a new {Style} with the given attributes overridden.
  # @param overrides [Hash{Symbol => Object}]
  # @return [Style]
  def merge(**overrides) = self.class.new(**to_h.merge(overrides))

  # Minimal SGR escape that transitions a terminal already showing `self`
  # into `other`: only the attributes that differ are emitted. Returns
  # `""` when the styles are identical (nothing to do), and {Ansi::RESET}
  # (`\e[0m`, one code) when `other` is the default style — shorter than
  # turning each attribute off individually.
  # @param other [Style] the style to transition to.
  # @return [String]
  def sgr_to(other)
    return "" if self == other
    return Ansi::RESET if other.default?

    codes = []
    codes << (other.bold ? 1 : 22) if bold != other.bold
    codes << (other.italic ? 3 : 23) if italic != other.italic
    codes << (other.underline ? 4 : 24) if underline != other.underline
    codes << (other.strikethrough ? 9 : 29) if strikethrough != other.strikethrough
    codes << (other.inverse ? 7 : 27) if inverse != other.inverse
    codes.concat(color_codes(other.fg, target: :fg)) if fg != other.fg
    codes.concat(color_codes(other.bg, target: :bg)) if bg != other.bg
    return "" if codes.empty?

    "\e[#{codes.join(";")}m"
  end

  private

  # @param color [Color, nil]
  # @param target [Symbol] either `:fg` or `:bg`.
  # @return [Array<Integer>] SGR codes; `[39]` / `[49]` for the "default"
  #   reset when `color` is `nil`, otherwise delegated to {Color#sgr_codes}.
  def color_codes(color, target:)
    return [target == :fg ? 39 : 49] if color.nil?

    color.sgr_codes(target)
  end
end

#underlineBoolean (readonly)

Returns:

  • (Boolean)


68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
# File 'lib/tuile/styled_string.rb', line 68

class Style < Data.define(:fg, :bg, :bold, :italic, :underline, :strikethrough, :inverse)
  # @param fg [Color, Symbol, Integer, Array<Integer>, nil] coerced via {Color.coerce}.
  # @param bg [Color, Symbol, Integer, Array<Integer>, nil] coerced via {Color.coerce}.
  # @param bold [Boolean]
  # @param italic [Boolean]
  # @param underline [Boolean]
  # @param strikethrough [Boolean]
  # @param inverse [Boolean]
  # @return [Style]
  # @raise [ArgumentError] when a color is not one of the accepted forms.
  def self.new(fg: nil, bg: nil, bold: false, italic: false, underline: false, strikethrough: false,
               inverse: false)
    super(fg: Color.coerce(fg), bg: Color.coerce(bg), bold:, italic:, underline:, strikethrough:, inverse:)
  end

  # The style with no color and no attributes — what the terminal shows
  # without any SGR applied.
  # @return [Style]
  DEFAULT = new

  # @return [Boolean]
  def default? = self == DEFAULT

  # Returns a new {Style} with the given attributes overridden.
  # @param overrides [Hash{Symbol => Object}]
  # @return [Style]
  def merge(**overrides) = self.class.new(**to_h.merge(overrides))

  # Minimal SGR escape that transitions a terminal already showing `self`
  # into `other`: only the attributes that differ are emitted. Returns
  # `""` when the styles are identical (nothing to do), and {Ansi::RESET}
  # (`\e[0m`, one code) when `other` is the default style — shorter than
  # turning each attribute off individually.
  # @param other [Style] the style to transition to.
  # @return [String]
  def sgr_to(other)
    return "" if self == other
    return Ansi::RESET if other.default?

    codes = []
    codes << (other.bold ? 1 : 22) if bold != other.bold
    codes << (other.italic ? 3 : 23) if italic != other.italic
    codes << (other.underline ? 4 : 24) if underline != other.underline
    codes << (other.strikethrough ? 9 : 29) if strikethrough != other.strikethrough
    codes << (other.inverse ? 7 : 27) if inverse != other.inverse
    codes.concat(color_codes(other.fg, target: :fg)) if fg != other.fg
    codes.concat(color_codes(other.bg, target: :bg)) if bg != other.bg
    return "" if codes.empty?

    "\e[#{codes.join(";")}m"
  end

  private

  # @param color [Color, nil]
  # @param target [Symbol] either `:fg` or `:bg`.
  # @return [Array<Integer>] SGR codes; `[39]` / `[49]` for the "default"
  #   reset when `color` is `nil`, otherwise delegated to {Color#sgr_codes}.
  def color_codes(color, target:)
    return [target == :fg ? 39 : 49] if color.nil?

    color.sgr_codes(target)
  end
end

Class Method Details

.new(fg: nil, bg: nil, bold: false, italic: false, underline: false, strikethrough: false, inverse: false) ⇒ Object

@param fg — coerced via Color.coerce.

@param bg — coerced via Color.coerce.

@param bold

@param italic

@param underline

@param strikethrough

@param inverse



78
79
80
81
# File 'lib/tuile/styled_string.rb', line 78

def self.new(fg: nil, bg: nil, bold: false, italic: false, underline: false, strikethrough: false,
             inverse: false)
  super(fg: Color.coerce(fg), bg: Color.coerce(bg), bold:, italic:, underline:, strikethrough:, inverse:)
end

Instance Method Details

#color_codes(color, target:) ⇒ ::Array[Integer]

@param color

@param target — either :fg or :bg.

@return — SGR codes; [39] / [49] for the "default" reset when color is nil, otherwise delegated to Color#sgr_codes.

Parameters:

  • color (Color, nil)
  • target: (Symbol)

Returns:

  • (::Array[Integer])


126
127
128
129
130
# File 'lib/tuile/styled_string.rb', line 126

def color_codes(color, target:)
  return [target == :fg ? 39 : 49] if color.nil?

  color.sgr_codes(target)
end

#default?Boolean

Returns:

  • (Boolean)


89
# File 'lib/tuile/styled_string.rb', line 89

def default? = self == DEFAULT

#merge(**overrides) ⇒ Style

Returns a new Tuile::StyledString::Style with the given attributes overridden.

@param overrides

Parameters:

  • overrides (::Hash[Symbol, Object])

Returns:



94
# File 'lib/tuile/styled_string.rb', line 94

def merge(**overrides) = self.class.new(**to_h.merge(overrides))

#sgr_to(other) ⇒ String

Minimal SGR escape that transitions a terminal already showing self into other: only the attributes that differ are emitted. Returns "" when the styles are identical (nothing to do), and Ansi::RESET (\e[0m, one code) when other is the default style — shorter than turning each attribute off individually.

@param other — the style to transition to.

Parameters:

Returns:

  • (String)


103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
# File 'lib/tuile/styled_string.rb', line 103

def sgr_to(other)
  return "" if self == other
  return Ansi::RESET if other.default?

  codes = []
  codes << (other.bold ? 1 : 22) if bold != other.bold
  codes << (other.italic ? 3 : 23) if italic != other.italic
  codes << (other.underline ? 4 : 24) if underline != other.underline
  codes << (other.strikethrough ? 9 : 29) if strikethrough != other.strikethrough
  codes << (other.inverse ? 7 : 27) if inverse != other.inverse
  codes.concat(color_codes(other.fg, target: :fg)) if fg != other.fg
  codes.concat(color_codes(other.bg, target: :bg)) if bg != other.bg
  return "" if codes.empty?

  "\e[#{codes.join(";")}m"
end