Class: Rich::Segment

Inherits:
Object
  • Object
show all
Defined in:
lib/rich/segment.rb

Overview

A piece of text with associated style. Segments are the fundamental unit produced by the rendering process and are ultimately converted to strings for terminal output.

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(text = "", style: nil, control: nil) ⇒ Segment

Create a new segment

Parameters:

  • text (String) (defaults to: "")

    Text content

  • style (Style, nil) (defaults to: nil)

    Style to apply

  • control (Array, nil) (defaults to: nil)

    Control codes



25
26
27
28
29
30
# File 'lib/rich/segment.rb', line 25

def initialize(text = "", style: nil, control: nil)
  @text = text.freeze
  @style = style
  @control = control&.freeze
  freeze
end

Instance Attribute Details

#controlArray? (readonly)

Returns Control codes (non-printable).

Returns:

  • (Array, nil)

    Control codes (non-printable)



19
20
21
# File 'lib/rich/segment.rb', line 19

def control
  @control
end

#styleStyle? (readonly)

Returns Style for the text.

Returns:

  • (Style, nil)

    Style for the text



16
17
18
# File 'lib/rich/segment.rb', line 16

def style
  @style
end

#textString (readonly)

Returns Text content.

Returns:

  • (String)

    Text content



13
14
15
# File 'lib/rich/segment.rb', line 13

def text
  @text
end

Class Method Details

.adjust_line_length(line, length, style: nil, pad: true) ⇒ Array<Segment>

Adjust line length by cropping or padding

Parameters:

  • line (Array<Segment>)

    Line segments

  • length (Integer)

    Target length

  • style (Style, nil) (defaults to: nil)

    Style for padding

  • pad (Boolean) (defaults to: true)

    Whether to pad short lines

Returns:



207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
# File 'lib/rich/segment.rb', line 207

def adjust_line_length(line, length, style: nil, pad: true)
  current_length = get_line_length(line)

  if current_length < length
    # Pad if needed
    if pad
      pad_size = length - current_length
      line + [blank(pad_size, style: style)]
    else
      line
    end
  elsif current_length > length
    # Crop
    crop_line(line, length)
  else
    line
  end
end

.apply_style(segments, style: nil, post_style: nil) ⇒ Enumerable<Segment>

Apply style to an iterable of segments

Parameters:

  • segments (Enumerable<Segment>)

    Segments to style

  • style (Style, nil) (defaults to: nil)

    Style to apply

  • post_style (Style, nil) (defaults to: nil)

    Style to apply after segment style

Returns:



124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
# File 'lib/rich/segment.rb', line 124

def apply_style(segments, style: nil, post_style: nil)
  return segments if style.nil? && post_style.nil?

  segments.map do |segment|
    next segment if segment.control?

    new_style = if segment.style
                  if style && post_style
                    style + segment.style + post_style
                  elsif style
                    style + segment.style
                  else
                    segment.style + post_style
                  end
                else
                  style || post_style
                end

    new(segment.text, style: new_style, control: segment.control)
  end
end

.blank(cell_count, style: nil) ⇒ Segment

Create a blank segment with specified cell width

Parameters:

  • cell_count (Integer)

    Width in cells

  • style (Style, nil) (defaults to: nil)

    Optional style

Returns:



108
109
110
# File 'lib/rich/segment.rb', line 108

def blank(cell_count, style: nil)
  new(" " * cell_count, style: style)
end

.control(control_codes) ⇒ Segment

Create a control segment

Parameters:

  • control (Array)

    Control codes

Returns:



115
116
117
# File 'lib/rich/segment.rb', line 115

def control(control_codes)
  new("", control: control_codes)
end

.crop_line(line, max_width) ⇒ Array<Segment>

Crop a line to a maximum width

Parameters:

  • line (Array<Segment>)

    Line segments

  • max_width (Integer)

    Maximum width

Returns:



246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
# File 'lib/rich/segment.rb', line 246

def crop_line(line, max_width)
  result = []
  remaining = max_width

  line.each do |segment|
    break if remaining <= 0

    segment_width = segment.cell_length

    if segment_width <= remaining
      result << segment
      remaining -= segment_width
    else
      # Need to split segment
      before, _after = segment.split_cells(remaining)
      result << before
      remaining = 0
    end
  end

  result
end

.filter_control(segments, is_control: false) ⇒ Enumerable<Segment>

Filter segments by control status

Parameters:

  • segments (Enumerable<Segment>)

    Segments to filter

  • is_control (Boolean) (defaults to: false)

    Filter for control segments

Returns:



150
151
152
# File 'lib/rich/segment.rb', line 150

def filter_control(segments, is_control: false)
  segments.select { |s| s.control? == is_control }
end

.get_line_length(line) ⇒ Integer

Get total cell length of a line

Parameters:

  • line (Array<Segment>)

    Line segments

Returns:

  • (Integer)


229
230
231
# File 'lib/rich/segment.rb', line 229

def get_line_length(line)
  line.sum(&:cell_length)
end

.get_shape(lines) ⇒ Array<Integer>

Get dimensions of lines

Parameters:

  • lines (Array<Array<Segment>>)

    Lines

Returns:

  • (Array<Integer>)
    width, height


236
237
238
239
240
# File 'lib/rich/segment.rb', line 236

def get_shape(lines)
  height = lines.length
  width = lines.map { |line| get_line_length(line) }.max || 0
  [width, height]
end

.lineSegment

Create a newline segment

Returns:



100
101
102
# File 'lib/rich/segment.rb', line 100

def line
  @line ||= new("\n")
end

.render(segments, color_system: ColorSystem::TRUECOLOR) ⇒ String

Render segments to string with ANSI codes

Parameters:

  • segments (Enumerable<Segment>)

    Segments to render

  • color_system (Symbol) (defaults to: ColorSystem::TRUECOLOR)

    Color system to use

Returns:

  • (String)


311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
# File 'lib/rich/segment.rb', line 311

def render(segments, color_system: ColorSystem::TRUECOLOR)
  output = +""
  last_style = nil

  segments.each do |segment|
    if segment.control?
      # Handle control codes
      segment.control.each do |control_code|
        output << Control.generate(*control_code)
      end
    else
      style = segment.style

      if style != last_style
        # Reset if needed
        output << "\e[0m" if last_style
        # Apply new style
        output << style.render(color_system: color_system) if style
        last_style = style
      end

      output << segment.text
    end
  end

  # Reset at end if we had any style
  output << "\e[0m" if last_style

  output
end

.simplify(segments) ⇒ Array<Segment>

Simplify consecutive segments with the same style

Parameters:

  • segments (Array<Segment>)

    Segments to simplify

Returns:



272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
# File 'lib/rich/segment.rb', line 272

def simplify(segments)
  return segments if segments.empty?

  result = []
  current_text = +""
  current_style = nil
  current_control = nil

  segments.each do |segment|
    if segment.control?
      # Flush text if any
      unless current_text.empty?
        result << new(current_text, style: current_style, control: current_control)
        current_text = +""
      end
      result << segment
      current_style = nil
      current_control = nil
    elsif segment.style == current_style
      current_text << segment.text
    else
      unless current_text.empty?
        result << new(current_text, style: current_style, control: current_control)
      end
      current_text = +segment.text
      current_style = segment.style
      current_control = nil
    end
  end

  result << new(current_text, style: current_style) unless current_text.empty?

  result
end

.split_and_crop_lines(segments, width, style: nil, pad: true, include_new_lines: true) ⇒ Array<Array<Segment>>

Split and crop segments to a given width

Parameters:

  • segments (Enumerable<Segment>)

    Segments to process

  • width (Integer)

    Maximum width

  • style (Style, nil) (defaults to: nil)

    Fill style

  • pad (Boolean) (defaults to: true)

    Pad lines to width

  • include_new_lines (Boolean) (defaults to: true)

    Include newline segments

Returns:



188
189
190
191
192
193
194
195
196
197
198
199
# File 'lib/rich/segment.rb', line 188

def split_and_crop_lines(segments, width, style: nil, pad: true, include_new_lines: true)
  lines = split_lines(segments)

  lines.map do |line|
    cropped = adjust_line_length(line, width, style: style, pad: pad)
    if include_new_lines
      cropped + [new("\n")]
    else
      cropped
    end
  end
end

.split_at_cell(text, cut, style) ⇒ Array<Segment>

Split text at a cell position

Parameters:

  • text (String)

    Text to split

  • cut (Integer)

    Cell position

  • style (Style, nil)

    Style to apply

Returns:

  • (Array<Segment>)

    Two segments



347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
# File 'lib/rich/segment.rb', line 347

def split_at_cell(text, cut, style)
  position = 0
  cell_count = 0
  insert_spaces = 0

  text.each_char do |char|
    char_width = Cells.char_width(char)
    new_cell_count = cell_count + char_width

    if new_cell_count > cut
      # Split happens in the middle of a wide character
      if char_width == 2 && cell_count + 1 == cut
        insert_spaces = 2
      end
      break
    end

    cell_count = new_cell_count
    position += 1
    break if cell_count == cut
  end

  if insert_spaces > 0
    [
      new(text[0...position] + " " * (cut - cell_count), style: style),
      new(" " * (insert_spaces - (cut - cell_count)) + text[position..], style: style)
    ]
  else
    [
      new(text[0...position], style: style),
      new(text[position..] || "", style: style)
    ]
  end
end

.split_lines(segments) ⇒ Array<Array<Segment>>

Split segments into lines

Parameters:

  • segments (Enumerable<Segment>)

    Segments to split

Returns:

  • (Array<Array<Segment>>)

    Array of lines



157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
# File 'lib/rich/segment.rb', line 157

def split_lines(segments)
  lines = []
  current_line = []

  segments.each do |segment|
    if segment.text.include?("\n")
      parts = segment.text.split("\n", -1)
      parts.each_with_index do |part, index|
        current_line << new(part, style: segment.style) unless part.empty?

        if index < parts.length - 1
          lines << current_line
          current_line = []
        end
      end
    else
      current_line << segment
    end
  end

  lines << current_line unless current_line.empty?
  lines
end

Instance Method Details

#==(other) ⇒ Object Also known as: eql?



85
86
87
88
89
# File 'lib/rich/segment.rb', line 85

def ==(other)
  return false unless other.is_a?(Segment)

  @text == other.text && @style == other.style && @control == other.control
end

#cell_lengthInteger

Returns Display width in terminal cells.

Returns:

  • (Integer)

    Display width in terminal cells



33
34
35
36
37
# File 'lib/rich/segment.rb', line 33

def cell_length
  return 0 if control?

  Cells.cached_cell_len(@text)
end

#control?Boolean

Returns True if this is a control segment.

Returns:

  • (Boolean)

    True if this is a control segment



50
51
52
# File 'lib/rich/segment.rb', line 50

def control?
  !@control.nil?
end

#empty?Boolean

Returns True if segment is empty.

Returns:

  • (Boolean)

    True if segment is empty



45
46
47
# File 'lib/rich/segment.rb', line 45

def empty?
  @text.empty?
end

#hashObject



93
94
95
# File 'lib/rich/segment.rb', line 93

def hash
  [@text, @style, @control].hash
end

#inspectObject



75
76
77
78
79
80
81
82
83
# File 'lib/rich/segment.rb', line 75

def inspect
  if control?
    "#<Rich::Segment control=#{@control.inspect}>"
  elsif @style
    "#<Rich::Segment #{@text.inspect} style=#{@style}>"
  else
    "#<Rich::Segment #{@text.inspect}>"
  end
end

#present?Boolean

Returns True if segment has text content.

Returns:

  • (Boolean)

    True if segment has text content



40
41
42
# File 'lib/rich/segment.rb', line 40

def present?
  !@text.empty?
end

#split_cells(cut) ⇒ Array<Segment>

Split segment at a cell position

Parameters:

  • cut (Integer)

    Cell position to split at

Returns:

  • (Array<Segment>)

    Two segments [before, after]



62
63
64
65
66
67
# File 'lib/rich/segment.rb', line 62

def split_cells(cut)
  return [self.class.new("", style: @style), self] if cut <= 0
  return [self, self.class.new("", style: @style)] if cut >= cell_length

  self.class.split_at_cell(@text, cut, @style)
end

#to_boolBoolean

Returns True if segment has text (used for truthiness).

Returns:

  • (Boolean)

    True if segment has text (used for truthiness)



55
56
57
# File 'lib/rich/segment.rb', line 55

def to_bool
  present?
end

#to_sString

Get segment text with ANSI reset if needed

Returns:

  • (String)


71
72
73
# File 'lib/rich/segment.rb', line 71

def to_s
  @text
end