Class: Tuile::StyledString::Style

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

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:



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
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
# File 'lib/tuile/styled_string.rb', line 83

class Style < Data.define(:fg, :bg, :bold, :italic, :underline, :strikethrough)
  # @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]
  # @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)
    super(fg: Color.coerce(fg), bg: Color.coerce(bg), bold:, italic:, underline:, strikethrough:)
  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.
  #
  # Shared by {StyledString#to_ansi} (diffing span-to-span from the default
  # style) and {Buffer}'s flush (diffing cell-to-cell against the style the
  # terminal currently holds), so both emit identical minimal sequences.
  # @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.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)


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
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
# File 'lib/tuile/styled_string.rb', line 83

class Style < Data.define(:fg, :bg, :bold, :italic, :underline, :strikethrough)
  # @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]
  # @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)
    super(fg: Color.coerce(fg), bg: Color.coerce(bg), bold:, italic:, underline:, strikethrough:)
  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.
  #
  # Shared by {StyledString#to_ansi} (diffing span-to-span from the default
  # style) and {Buffer}'s flush (diffing cell-to-cell against the style the
  # terminal currently holds), so both emit identical minimal sequences.
  # @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.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:



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
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
# File 'lib/tuile/styled_string.rb', line 83

class Style < Data.define(:fg, :bg, :bold, :italic, :underline, :strikethrough)
  # @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]
  # @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)
    super(fg: Color.coerce(fg), bg: Color.coerce(bg), bold:, italic:, underline:, strikethrough:)
  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.
  #
  # Shared by {StyledString#to_ansi} (diffing span-to-span from the default
  # style) and {Buffer}'s flush (diffing cell-to-cell against the style the
  # terminal currently holds), so both emit identical minimal sequences.
  # @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.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)


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
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
# File 'lib/tuile/styled_string.rb', line 83

class Style < Data.define(:fg, :bg, :bold, :italic, :underline, :strikethrough)
  # @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]
  # @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)
    super(fg: Color.coerce(fg), bg: Color.coerce(bg), bold:, italic:, underline:, strikethrough:)
  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.
  #
  # Shared by {StyledString#to_ansi} (diffing span-to-span from the default
  # style) and {Buffer}'s flush (diffing cell-to-cell against the style the
  # terminal currently holds), so both emit identical minimal sequences.
  # @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.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)


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
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
# File 'lib/tuile/styled_string.rb', line 83

class Style < Data.define(:fg, :bg, :bold, :italic, :underline, :strikethrough)
  # @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]
  # @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)
    super(fg: Color.coerce(fg), bg: Color.coerce(bg), bold:, italic:, underline:, strikethrough:)
  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.
  #
  # Shared by {StyledString#to_ansi} (diffing span-to-span from the default
  # style) and {Buffer}'s flush (diffing cell-to-cell against the style the
  # terminal currently holds), so both emit identical minimal sequences.
  # @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.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)


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
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
# File 'lib/tuile/styled_string.rb', line 83

class Style < Data.define(:fg, :bg, :bold, :italic, :underline, :strikethrough)
  # @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]
  # @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)
    super(fg: Color.coerce(fg), bg: Color.coerce(bg), bold:, italic:, underline:, strikethrough:)
  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.
  #
  # Shared by {StyledString#to_ansi} (diffing span-to-span from the default
  # style) and {Buffer}'s flush (diffing cell-to-cell against the style the
  # terminal currently holds), so both emit identical minimal sequences.
  # @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.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) ⇒ Style

Parameters:

  • fg (Color, Symbol, Integer, Array<Integer>, nil) (defaults to: nil)

    coerced via Color.coerce.

  • bg (Color, Symbol, Integer, Array<Integer>, nil) (defaults to: nil)

    coerced via Color.coerce.

  • bold (Boolean) (defaults to: false)
  • italic (Boolean) (defaults to: false)
  • underline (Boolean) (defaults to: false)
  • strikethrough (Boolean) (defaults to: false)

Returns:

Raises:

  • (ArgumentError)

    when a color is not one of the accepted forms.



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

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

Instance Method Details

#default?Boolean

Returns:

  • (Boolean)


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

def default? = self == DEFAULT

#merge(**overrides) ⇒ Style

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

Parameters:

  • overrides (Hash{Symbol => Object})

Returns:



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

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.

Shared by Tuile::StyledString#to_ansi (diffing span-to-span from the default style) and Buffer‘s flush (diffing cell-to-cell against the style the terminal currently holds), so both emit identical minimal sequences.

Parameters:

  • other (Style)

    the style to transition to.

Returns:

  • (String)


120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
# File 'lib/tuile/styled_string.rb', line 120

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.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