Module: AskTTY::Internal::Rendering

Defined in:
lib/asktty/internal/rendering.rb

Class Method Summary collapse

Class Method Details

.chop_grapheme(text) ⇒ Object



10
11
12
# File 'lib/asktty/internal/rendering.rb', line 10

def chop_grapheme(text)
  graphemes(text.to_s)[0...-1].join
end

.content_width(width) ⇒ Object



105
106
107
# File 'lib/asktty/internal/rendering.rb', line 105

def content_width(width)
  [width.to_i - 2, 1].max
end

.display_width(text) ⇒ Object



14
15
16
# File 'lib/asktty/internal/rendering.rb', line 14

def display_width(text)
  Unicode::DisplayWidth.of(text.to_s.gsub(/\e\[[\d;]*m/, ""), ambiguous: 1)
end


64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
# File 'lib/asktty/internal/rendering.rb', line 64

def footer_lines(error_message:, help_line:, width:)
  lines = []

  if error_message && !error_message.empty?
    lines.concat(wrap(error_message, width).map do |line|
      ANSIStyle.error(line)
    end)
  end

  lines << help_line if help_line && !help_line.empty?

  return [] if lines.empty?

  [""] + lines
end

.frame(lines) ⇒ Object



109
110
111
112
113
114
# File 'lib/asktty/internal/rendering.rb', line 109

def frame(lines)
  content_lines = Array(lines).flat_map { |line| split_lines(line.to_s) }
  border = ANSIStyle.muted("")

  content_lines.map { |line| "#{border} #{line}" }.join("\n")
end

.graphemes(text) ⇒ Object



116
117
118
# File 'lib/asktty/internal/rendering.rb', line 116

def graphemes(text)
  text.scan(/\X/)
end

.header_lines(title, details, width:) ⇒ Object



56
57
58
59
60
61
62
# File 'lib/asktty/internal/rendering.rb', line 56

def header_lines(title, details, width:)
  lines = wrap(title.to_s, width).map { |line| ANSIStyle.title(line) }

  return lines unless details

  lines + wrap(details.to_s, width).map { |line| ANSIStyle.muted(line) }
end

.help_line(items, width:) ⇒ Object



80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
# File 'lib/asktty/internal/rendering.rb', line 80

def help_line(items, width:)
  items = Array(items).map(&:to_s).reject(&:empty?)
  return "" if items.empty?

  line = +""
  total_width = 0

  items.each do |item|
    segment = total_width.zero? ? item : "#{item}"
    segment_width = display_width(segment)

    if width.to_i.positive? && total_width + segment_width > width
      line << "" if total_width + display_width("") <= width
      break
    end

    line << segment
    total_width += segment_width
  end

  return "" if line.empty?

  ANSIStyle.muted(line)
end

.placeholder_with_cursor(text) ⇒ Object



18
19
20
21
22
23
# File 'lib/asktty/internal/rendering.rb', line 18

def placeholder_with_cursor(text)
  first_grapheme, *rest = graphemes(text.to_s)
  return ANSIStyle.cursor unless first_grapheme

  ANSIStyle.style(first_grapheme, foreground: 8, background: 2) + ANSIStyle.muted(rest.join)
end

.prompt_frame(title:, details:, help_items:, error_message:, width:, gap_before_body: false) ⇒ Object



25
26
27
28
29
30
31
32
33
34
35
36
37
38
# File 'lib/asktty/internal/rendering.rb', line 25

def prompt_frame(title:, details:, help_items:, error_message:, width:, gap_before_body: false)
  content_width = self.content_width(width)

  lines = header_lines(title, details, width: content_width)
  lines << "" if gap_before_body && !lines.empty?
  lines.concat(Array(yield(content_width)))
  lines.concat(
    footer_lines(
      error_message: error_message, help_line: help_line(help_items, width: content_width), width: content_width
    )
  )

  frame(lines)
end

.split_lines(text) ⇒ Object



120
121
122
123
124
# File 'lib/asktty/internal/rendering.rb', line 120

def split_lines(text)
  return [""] if text.empty?

  text.split("\n", -1)
end

.style_submitted_lines(lines, title_length:) ⇒ Object



126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
# File 'lib/asktty/internal/rendering.rb', line 126

def (lines, title_length:)
  remaining_title_length = title_length

  lines.map do |line|
    line_graphemes = graphemes(line)

    if remaining_title_length >= line_graphemes.length
      remaining_title_length -= line_graphemes.length
      ANSIStyle.title(line)
    elsif remaining_title_length.positive?
      title_text = line_graphemes[0, remaining_title_length].join
      value_text = line_graphemes[remaining_title_length..].to_a.join
      remaining_title_length = 0

      ANSIStyle.title(title_text) + ANSIStyle.text(value_text)
    else
      ANSIStyle.text(line)
    end
  end
end

.submitted_frame(title, value, width:) ⇒ Object



40
41
42
43
44
45
46
# File 'lib/asktty/internal/rendering.rb', line 40

def (title, value, width:)
  prefix = "#{title}:"
  summary = value.to_s.empty? ? prefix : "#{prefix} #{value}"
  lines = wrap(summary, content_width(width))

  frame((lines, title_length: graphemes(prefix).length))
end

.take_segment(graphemes, width) ⇒ Object



189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
# File 'lib/asktty/internal/rendering.rb', line 189

def take_segment(graphemes, width)
  segment = []
  segment_width = 0
  last_whitespace_index = nil

  graphemes.each_with_index do |grapheme, index|
    grapheme_width = display_width(grapheme)

    if segment_width.zero? && grapheme_width > width
      return [[grapheme], grapheme.match?(/\s/) ? 0 : nil]
    end

    break if segment_width.positive? && segment_width + grapheme_width > width

    segment << grapheme
    segment_width += grapheme_width
    last_whitespace_index = index if grapheme.match?(/\s/)

    break if segment_width >= width
  end

  [segment, last_whitespace_index]
end

.wrap(text, width) ⇒ Object



48
49
50
# File 'lib/asktty/internal/rendering.rb', line 48

def wrap(text, width)
  split_lines(text.to_s).flat_map { |line| wrap_line(line, width) }
end

.wrap_exact(text, width) ⇒ Object



52
53
54
# File 'lib/asktty/internal/rendering.rb', line 52

def wrap_exact(text, width)
  split_lines(text.to_s).flat_map { |line| wrap_exact_line(line, width) }
end

.wrap_exact_line(line, width) ⇒ Object



174
175
176
177
178
179
180
181
182
183
184
185
186
187
# File 'lib/asktty/internal/rendering.rb', line 174

def wrap_exact_line(line, width)
  return [""] if line.empty?

  lines = []
  remaining = graphemes(line)

  until remaining.empty?
    segment, = take_segment(remaining, width)
    lines << segment.join
    remaining = remaining[segment.length..] || []
  end

  lines
end

.wrap_line(line, width) ⇒ Object



147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
# File 'lib/asktty/internal/rendering.rb', line 147

def wrap_line(line, width)
  return [""] if line.empty?

  lines = []
  remaining = graphemes(line)

  until remaining.empty?
    segment, last_whitespace_index = take_segment(remaining, width)

    if segment.length == remaining.length
      lines << segment.join
      break
    end

    if last_whitespace_index&.positive?
      lines << segment[0...last_whitespace_index].join.rstrip
      remaining = remaining[(last_whitespace_index + 1)..] || []
      remaining = remaining.drop_while { |grapheme| grapheme.match?(/\s/) }
    else
      lines << segment.join
      remaining = remaining[segment.length..] || []
    end
  end

  lines
end