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:



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) ⇒ Object

@param fg — coerced via Color.coerce.

@param bg — coerced via Color.coerce.

@param bold

@param italic

@param underline

@param strikethrough



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

#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])


142
143
144
145
146
# File 'lib/tuile/styled_string.rb', line 142

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

  color.sgr_codes(target)
end

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

@param overrides

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.

@param other — the style to transition to.

Parameters:

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