r2ui 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (133) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +64 -0
  3. data/README.md +79 -0
  4. data/lib/r2ui/app.rb +311 -34
  5. data/lib/r2ui/canvas.rb +135 -14
  6. data/lib/r2ui/cli/builder.rb +72 -0
  7. data/lib/r2ui/cli/context.rb +45 -0
  8. data/lib/r2ui/cli/definition.rb +174 -0
  9. data/lib/r2ui/cli/ext/ask.rb +133 -0
  10. data/lib/r2ui/cli/ext/box.rb +112 -0
  11. data/lib/r2ui/cli/ext/choose.rb +162 -0
  12. data/lib/r2ui/cli/ext/choose_many.rb +241 -0
  13. data/lib/r2ui/cli/ext/color_option.rb +64 -0
  14. data/lib/r2ui/cli/ext/completion.rb +520 -0
  15. data/lib/r2ui/cli/ext/config.rb +179 -0
  16. data/lib/r2ui/cli/ext/confirm.rb +110 -0
  17. data/lib/r2ui/cli/ext/dashboard.rb +105 -0
  18. data/lib/r2ui/cli/ext/diff.rb +195 -0
  19. data/lib/r2ui/cli/ext/editor.rb +94 -0
  20. data/lib/r2ui/cli/ext/env.rb +108 -0
  21. data/lib/r2ui/cli/ext/examples.rb +62 -0
  22. data/lib/r2ui/cli/ext/filter.rb +269 -0
  23. data/lib/r2ui/cli/ext/format.rb +104 -0
  24. data/lib/r2ui/cli/ext/list.rb +117 -0
  25. data/lib/r2ui/cli/ext/log.rb +89 -0
  26. data/lib/r2ui/cli/ext/pager.rb +97 -0
  27. data/lib/r2ui/cli/ext/pairs.rb +62 -0
  28. data/lib/r2ui/cli/ext/parallel.rb +274 -0
  29. data/lib/r2ui/cli/ext/password.rb +158 -0
  30. data/lib/r2ui/cli/ext/progress.rb +330 -0
  31. data/lib/r2ui/cli/ext/reference.rb +129 -0
  32. data/lib/r2ui/cli/ext/spinner.rb +165 -0
  33. data/lib/r2ui/cli/ext/summary.rb +63 -0
  34. data/lib/r2ui/cli/ext/table.rb +141 -0
  35. data/lib/r2ui/cli/ext/tasks.rb +216 -0
  36. data/lib/r2ui/cli/ext/trace.rb +64 -0
  37. data/lib/r2ui/cli/ext/tree.rb +114 -0
  38. data/lib/r2ui/cli/ext/version.rb +67 -0
  39. data/lib/r2ui/cli/extension.rb +129 -0
  40. data/lib/r2ui/cli/help.rb +115 -0
  41. data/lib/r2ui/cli/helpers.rb +20 -0
  42. data/lib/r2ui/cli/live.rb +150 -0
  43. data/lib/r2ui/cli/parser.rb +165 -0
  44. data/lib/r2ui/cli/prompt.rb +133 -0
  45. data/lib/r2ui/cli/runner.rb +86 -0
  46. data/lib/r2ui/cli/shell.rb +185 -0
  47. data/lib/r2ui/cli/testing.rb +49 -0
  48. data/lib/r2ui/cli.rb +129 -0
  49. data/lib/r2ui/compat/bubbletea/LICENSE-bubbletea.txt +21 -0
  50. data/lib/r2ui/compat/bubbletea/ansi.rb +424 -0
  51. data/lib/r2ui/compat/bubbletea/commands.rb +143 -0
  52. data/lib/r2ui/compat/bubbletea/input.rb +269 -0
  53. data/lib/r2ui/compat/bubbletea/input_reader.rb +66 -0
  54. data/lib/r2ui/compat/bubbletea/messages.rb +251 -0
  55. data/lib/r2ui/compat/bubbletea/model.rb +65 -0
  56. data/lib/r2ui/compat/bubbletea/native.rb +254 -0
  57. data/lib/r2ui/compat/bubbletea/renderer.rb +192 -0
  58. data/lib/r2ui/compat/bubbletea/runner.rb +425 -0
  59. data/lib/r2ui/compat/bubbletea/terminal.rb +230 -0
  60. data/lib/r2ui/compat/bubbletea/version.rb +8 -0
  61. data/lib/r2ui/compat/bubbletea/width_table.rb +228 -0
  62. data/lib/r2ui/compat/bubbletea.rb +16 -0
  63. data/lib/r2ui/compat/lipgloss/border.rb +17 -0
  64. data/lib/r2ui/compat/lipgloss/color.rb +78 -0
  65. data/lib/r2ui/compat/lipgloss/color_blend.rb +166 -0
  66. data/lib/r2ui/compat/lipgloss/colorful.rb +585 -0
  67. data/lib/r2ui/compat/lipgloss/layout.rb +461 -0
  68. data/lib/r2ui/compat/lipgloss/list.rb +85 -0
  69. data/lib/r2ui/compat/lipgloss/position.rb +30 -0
  70. data/lib/r2ui/compat/lipgloss/style/border.rb +134 -0
  71. data/lib/r2ui/compat/lipgloss/style.rb +1062 -0
  72. data/lib/r2ui/compat/lipgloss/table/render.rb +518 -0
  73. data/lib/r2ui/compat/lipgloss/table.rb +185 -0
  74. data/lib/r2ui/compat/lipgloss/termenv.rb +768 -0
  75. data/lib/r2ui/compat/lipgloss/text/unicode_tables.rb +428 -0
  76. data/lib/r2ui/compat/lipgloss/text.rb +1555 -0
  77. data/lib/r2ui/compat/lipgloss/tree/go_tree.rb +243 -0
  78. data/lib/r2ui/compat/lipgloss/tree.rb +105 -0
  79. data/lib/r2ui/compat/lipgloss/version.rb +15 -0
  80. data/lib/r2ui/compat/lipgloss.rb +64 -0
  81. data/lib/r2ui/compat/load_path/bubbletea.rb +4 -0
  82. data/lib/r2ui/compat/load_path/lipgloss.rb +4 -0
  83. data/lib/r2ui/component.rb +67 -0
  84. data/lib/r2ui/context.rb +87 -0
  85. data/lib/r2ui/drop_in.rb +16 -0
  86. data/lib/r2ui/dsl/column.rb +11 -3
  87. data/lib/r2ui/dsl/dashboard.rb +45 -9
  88. data/lib/r2ui/dsl/resource.rb +12 -0
  89. data/lib/r2ui/ext/after.rb +54 -0
  90. data/lib/r2ui/ext/async.rb +81 -0
  91. data/lib/r2ui/ext/batch_sequence.rb +54 -0
  92. data/lib/r2ui/ext/countdown.rb +52 -0
  93. data/lib/r2ui/ext/data_table.rb +71 -0
  94. data/lib/r2ui/ext/every.rb +66 -0
  95. data/lib/r2ui/ext/exec.rb +116 -0
  96. data/lib/r2ui/ext/file_picker.rb +78 -0
  97. data/lib/r2ui/ext/focus_report.rb +69 -0
  98. data/lib/r2ui/ext/help.rb +62 -0
  99. data/lib/r2ui/ext/keys.rb +64 -0
  100. data/lib/r2ui/ext/list.rb +145 -0
  101. data/lib/r2ui/ext/messages.rb +87 -0
  102. data/lib/r2ui/ext/mouse.rb +134 -0
  103. data/lib/r2ui/ext/paginator.rb +86 -0
  104. data/lib/r2ui/ext/paste.rb +86 -0
  105. data/lib/r2ui/ext/println.rb +21 -0
  106. data/lib/r2ui/ext/progress.rb +85 -0
  107. data/lib/r2ui/ext/refresh.rb +56 -0
  108. data/lib/r2ui/ext/resize.rb +62 -0
  109. data/lib/r2ui/ext/screen.rb +47 -0
  110. data/lib/r2ui/ext/screen_mode.rb +63 -0
  111. data/lib/r2ui/ext/spinner.rb +59 -0
  112. data/lib/r2ui/ext/state.rb +45 -0
  113. data/lib/r2ui/ext/stopwatch.rb +67 -0
  114. data/lib/r2ui/ext/suspend.rb +56 -0
  115. data/lib/r2ui/ext/text_area.rb +73 -0
  116. data/lib/r2ui/ext/text_input.rb +83 -0
  117. data/lib/r2ui/ext/theme.rb +112 -0
  118. data/lib/r2ui/ext/view.rb +37 -0
  119. data/lib/r2ui/ext/viewport.rb +92 -0
  120. data/lib/r2ui/ext/window_title.rb +51 -0
  121. data/lib/r2ui/extension.rb +199 -0
  122. data/lib/r2ui/history.rb +4 -1
  123. data/lib/r2ui/keys.rb +35 -8
  124. data/lib/r2ui/motion.rb +169 -0
  125. data/lib/r2ui/panel_state.rb +46 -2
  126. data/lib/r2ui/renderer.rb +87 -22
  127. data/lib/r2ui/version.rb +1 -1
  128. data/lib/r2ui/widgets/box.rb +6 -3
  129. data/lib/r2ui/widgets/glyphs.rb +149 -0
  130. data/lib/r2ui/widgets/table.rb +109 -25
  131. data/lib/r2ui.rb +12 -1
  132. metadata +119 -4
  133. data/lib/r2ui/terminal.rb +0 -53
@@ -0,0 +1,1062 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Lipgloss::Style: a port of lipgloss v1.1.0 style.go, set.go, get.go, unset.go, borders.go and
4
+ # align.go, behind the same Ruby-facing argument handling as the lipgloss gem 0.2.2 C extension
5
+ # (ext/lipgloss/style*.c, go/style*.go) and its lib/lipgloss/style.rb (align wrappers).
6
+ #
7
+ # Styles are immutable: every setter returns a new Style, like the gem (which allocates a new Go
8
+ # style per call). Like the gem, #dup and #clone give an empty style (the C extension allocates a
9
+ # fresh Go style and copies nothing).
10
+ #
11
+ # Private API for the table / list / tree ports (call with #send):
12
+ # get_padding / get_margin -> [top, right, bottom, left]
13
+ # get_horizontal_padding, get_vertical_padding, get_horizontal_margins, get_vertical_margins,
14
+ # get_horizontal_frame_size, get_vertical_frame_size, get_horizontal_border_size,
15
+ # get_vertical_border_size, get_border_style -> BorderDef, get_align_horizontal -> Float,
16
+ # get_align_vertical -> Float, get_max_width, get_max_height, get_inline,
17
+ # get_border_top_size / _right_size / _bottom_size / _left_size, get_border -> [def, t, r, b, l]
18
+ # set_border_def(border_def, *sides) Go Border(b, sides...)
19
+ # set_border_style_def(border_def) Go BorderStyle(b)
20
+ # set_foreground_color(terminal_color) / set_background_color(terminal_color)
21
+ # go_render(*strs) Go Render(strs...), no Ruby argument checks
22
+ require_relative "style/border"
23
+
24
+ module R2UI
25
+ module Compat
26
+ module Gloss
27
+ # The gem's Style is a T_DATA object with no instance variables, so it inspects as
28
+ # #<Lipgloss::Style:0x...>. Kept in a module so Style.public_instance_methods(false) stays
29
+ # exactly the gem's list.
30
+ module StyleInspect
31
+ def inspect = Object.instance_method(:to_s).bind_call(self)
32
+
33
+ def pretty_print(q)
34
+ q.text(inspect)
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
40
+
41
+ module Lipgloss
42
+ class Style
43
+ include R2UI::Compat::Gloss::StyleInspect
44
+ # propKey values (style.go), in Go's order.
45
+ module Keys
46
+ names = %i[
47
+ bold italic underline strikethrough reverse blink faint underline_spaces strikethrough_spaces
48
+ color_whitespace
49
+ foreground background width height align_horizontal align_vertical
50
+ padding_top padding_right padding_bottom padding_left
51
+ margin_top margin_right margin_bottom margin_left margin_background
52
+ border_style
53
+ border_top border_right border_bottom border_left
54
+ border_top_foreground border_right_foreground border_bottom_foreground border_left_foreground
55
+ border_top_background border_right_background border_bottom_background border_left_background
56
+ inline max_width max_height tab_width
57
+ transform
58
+ ]
59
+ names.each_with_index { |name, i| const_set(name.to_s.upcase, 1 << i) }
60
+ FIRST = BOLD
61
+ LAST = TRANSFORM
62
+
63
+ COLOR = [
64
+ FOREGROUND, BACKGROUND, MARGIN_BACKGROUND,
65
+ BORDER_TOP_FOREGROUND, BORDER_RIGHT_FOREGROUND, BORDER_BOTTOM_FOREGROUND, BORDER_LEFT_FOREGROUND,
66
+ BORDER_TOP_BACKGROUND, BORDER_RIGHT_BACKGROUND, BORDER_BOTTOM_BACKGROUND, BORDER_LEFT_BACKGROUND
67
+ ].freeze
68
+ NON_NEGATIVE = [
69
+ WIDTH, HEIGHT, PADDING_TOP, PADDING_RIGHT, PADDING_BOTTOM, PADDING_LEFT,
70
+ MARGIN_TOP, MARGIN_RIGHT, MARGIN_BOTTOM, MARGIN_LEFT, MAX_WIDTH, MAX_HEIGHT
71
+ ].freeze
72
+ VALUE = [ALIGN_HORIZONTAL, ALIGN_VERTICAL, BORDER_STYLE, TAB_WIDTH, TRANSFORM].freeze
73
+ STORED = (COLOR + NON_NEGATIVE + VALUE).to_h { |k| [k, true] }.freeze
74
+ NON_NEGATIVE_SET = NON_NEGATIVE.to_h { |k| [k, true] }.freeze
75
+ end
76
+ private_constant :Keys
77
+
78
+ Gloss = R2UI::Compat::Gloss
79
+ private_constant :Gloss
80
+
81
+ UTF_8 = Encoding::UTF_8
82
+ private_constant :UTF_8
83
+
84
+ TAB_WIDTH_DEFAULT = 4
85
+ private_constant :TAB_WIDTH_DEFAULT
86
+
87
+ def initialize
88
+ reset_state
89
+ end
90
+
91
+ def render(string)
92
+ Gloss::Layout.check_string(string)
93
+ c_result(go_render(c_str(string)))
94
+ end
95
+
96
+ # --- text attributes -------------------------------------------------------------------------
97
+
98
+ def bold(value) = with(Keys::BOLD, value ? true : false)
99
+ def italic(value) = with(Keys::ITALIC, value ? true : false)
100
+ def underline(value) = with(Keys::UNDERLINE, value ? true : false)
101
+ def strikethrough(value) = with(Keys::STRIKETHROUGH, value ? true : false)
102
+ def reverse(value) = with(Keys::REVERSE, value ? true : false)
103
+ def blink(value) = with(Keys::BLINK, value ? true : false)
104
+ def faint(value) = with(Keys::FAINT, value ? true : false)
105
+
106
+ # --- colors ----------------------------------------------------------------------------------
107
+
108
+ def foreground(color)
109
+ with(Keys::FOREGROUND, Gloss::Renderer.terminal_color(color))
110
+ end
111
+
112
+ def background(color)
113
+ with(Keys::BACKGROUND, Gloss::Renderer.terminal_color(color))
114
+ end
115
+
116
+ def margin_background(color)
117
+ with(Keys::MARGIN_BACKGROUND, Gloss::Renderer.terminal_color(color, adaptive: false, complete: false))
118
+ end
119
+
120
+ # --- size ------------------------------------------------------------------------------------
121
+
122
+ def width(width) = with(Keys::WIDTH, num2int(width))
123
+ def height(height) = with(Keys::HEIGHT, num2int(height))
124
+ def max_width(width) = with(Keys::MAX_WIDTH, num2int(width))
125
+ def max_height(height) = with(Keys::MAX_HEIGHT, num2int(height))
126
+
127
+ # --- alignment (lib/lipgloss/style.rb + style.c) ---------------------------------------------
128
+
129
+ def align(*positions)
130
+ _align(*positions.map { |p| Position.resolve(p) })
131
+ end
132
+
133
+ def align_horizontal(position)
134
+ _align_horizontal(Position.resolve(position))
135
+ end
136
+
137
+ def align_vertical(position)
138
+ _align_vertical(Position.resolve(position))
139
+ end
140
+
141
+ def _align(*positions)
142
+ if positions.empty? || positions.length > 2
143
+ raise ArgumentError, "wrong number of arguments (given #{positions.length}, expected 1..2)"
144
+ end
145
+
146
+ values = positions.map { |p| Gloss::Layout.num2dbl(p) }
147
+ derive do |s|
148
+ s.__send__(:set!, Keys::ALIGN_HORIZONTAL, values[0])
149
+ s.__send__(:set!, Keys::ALIGN_VERTICAL, values[1]) if values.length > 1
150
+ end
151
+ end
152
+
153
+ def _align_horizontal(position)
154
+ with(Keys::ALIGN_HORIZONTAL, Gloss::Layout.num2dbl(position))
155
+ end
156
+
157
+ def _align_vertical(position)
158
+ with(Keys::ALIGN_VERTICAL, Gloss::Layout.num2dbl(position))
159
+ end
160
+
161
+ # --- other -----------------------------------------------------------------------------------
162
+
163
+ def inline(value) = with(Keys::INLINE, value ? true : false)
164
+
165
+ def tab_width(width)
166
+ n = num2int(width)
167
+ n = -1 if n <= -1
168
+ with(Keys::TAB_WIDTH, n)
169
+ end
170
+
171
+ def underline_spaces(value) = with(Keys::UNDERLINE_SPACES, value ? true : false)
172
+ def strikethrough_spaces(value) = with(Keys::STRIKETHROUGH_SPACES, value ? true : false)
173
+
174
+ def set_string(string)
175
+ Gloss::Layout.check_string(string)
176
+ str = c_str(string)
177
+ derive { |s| s.instance_variable_set(:@value, str) }
178
+ end
179
+
180
+ def inherit(other)
181
+ unless other.is_a?(Style)
182
+ raise TypeError, "wrong argument type #{Gloss::Layout.type_name(other)} (expected Lipgloss::Style)"
183
+ end
184
+
185
+ derive { |s| s.__send__(:inherit!, other) }
186
+ end
187
+
188
+ def to_s
189
+ c_result(go_render)
190
+ end
191
+
192
+ # --- getters ---------------------------------------------------------------------------------
193
+
194
+ def bold? = get_as_bool(Keys::BOLD, false)
195
+ def italic? = get_as_bool(Keys::ITALIC, false)
196
+ def underline? = get_as_bool(Keys::UNDERLINE, false)
197
+ def strikethrough? = get_as_bool(Keys::STRIKETHROUGH, false)
198
+ def reverse? = get_as_bool(Keys::REVERSE, false)
199
+ def blink? = get_as_bool(Keys::BLINK, false)
200
+ def faint? = get_as_bool(Keys::FAINT, false)
201
+
202
+ def get_foreground = color_string(get_as_color(Keys::FOREGROUND))
203
+ def get_background = color_string(get_as_color(Keys::BACKGROUND))
204
+ def get_width = get_as_int(Keys::WIDTH)
205
+ def get_height = get_as_int(Keys::HEIGHT)
206
+
207
+ # --- padding / margin (style_spacing.c) ------------------------------------------------------
208
+
209
+ def padding(*values)
210
+ sides = spacing_args(values)
211
+ derive { |s| s.__send__(:set_sides!, :padding, sides) }
212
+ end
213
+
214
+ def padding_top(value) = with(Keys::PADDING_TOP, num2int(value))
215
+ def padding_right(value) = with(Keys::PADDING_RIGHT, num2int(value))
216
+ def padding_bottom(value) = with(Keys::PADDING_BOTTOM, num2int(value))
217
+ def padding_left(value) = with(Keys::PADDING_LEFT, num2int(value))
218
+
219
+ def margin(*values)
220
+ sides = spacing_args(values)
221
+ derive { |s| s.__send__(:set_sides!, :margin, sides) }
222
+ end
223
+
224
+ def margin_top(value) = with(Keys::MARGIN_TOP, num2int(value))
225
+ def margin_right(value) = with(Keys::MARGIN_RIGHT, num2int(value))
226
+ def margin_bottom(value) = with(Keys::MARGIN_BOTTOM, num2int(value))
227
+ def margin_left(value) = with(Keys::MARGIN_LEFT, num2int(value))
228
+
229
+ # --- borders (style_border.c) ----------------------------------------------------------------
230
+
231
+ def border(*args)
232
+ raise ArgumentError, "wrong number of arguments (given 0, expected 1+)" if args.empty?
233
+
234
+ border = Gloss::Borders.resolve(args[0])
235
+ sides = args[1, 4].map { |v| v ? true : false }
236
+ set_border_def(border, *sides)
237
+ end
238
+
239
+ def border_style(type)
240
+ set_border_style_def(Gloss::Borders.resolve(type))
241
+ end
242
+
243
+ def border_foreground(color)
244
+ c = Gloss::Renderer.terminal_color(color, complete: false)
245
+ derive { |s| s.__send__(:set_border_colors!, :foreground, [c]) }
246
+ end
247
+
248
+ def border_background(color)
249
+ c = Gloss::Renderer.terminal_color(color, complete: false)
250
+ derive { |s| s.__send__(:set_border_colors!, :background, [c]) }
251
+ end
252
+
253
+ def border_top(value) = with(Keys::BORDER_TOP, value ? true : false)
254
+ def border_right(value) = with(Keys::BORDER_RIGHT, value ? true : false)
255
+ def border_bottom(value) = with(Keys::BORDER_BOTTOM, value ? true : false)
256
+ def border_left(value) = with(Keys::BORDER_LEFT, value ? true : false)
257
+
258
+ def border_top_foreground(color) = with(Keys::BORDER_TOP_FOREGROUND, side_color(color))
259
+ def border_right_foreground(color) = with(Keys::BORDER_RIGHT_FOREGROUND, side_color(color))
260
+ def border_bottom_foreground(color) = with(Keys::BORDER_BOTTOM_FOREGROUND, side_color(color))
261
+ def border_left_foreground(color) = with(Keys::BORDER_LEFT_FOREGROUND, side_color(color))
262
+ def border_top_background(color) = with(Keys::BORDER_TOP_BACKGROUND, side_color(color))
263
+ def border_right_background(color) = with(Keys::BORDER_RIGHT_BACKGROUND, side_color(color))
264
+ def border_bottom_background(color) = with(Keys::BORDER_BOTTOM_BACKGROUND, side_color(color))
265
+ def border_left_background(color) = with(Keys::BORDER_LEFT_BACKGROUND, side_color(color))
266
+
267
+ BORDER_CUSTOM_KEYS = %i[
268
+ top bottom left right top_left top_right bottom_left bottom_right
269
+ middle_left middle_right middle middle_top middle_bottom
270
+ ].freeze
271
+ private_constant :BORDER_CUSTOM_KEYS
272
+
273
+ def border_custom(*args, **opts)
274
+ raise ArgumentError, "wrong number of arguments (given #{args.length}, expected 0)" unless args.empty?
275
+ raise ArgumentError, "keyword arguments required" if opts.empty?
276
+
277
+ fields = BORDER_CUSTOM_KEYS.map do |k|
278
+ v = opts[k]
279
+ v.nil? ? "" : c_str(v)
280
+ end
281
+ set_border_def(Gloss::BorderDef.new(*fields))
282
+ end
283
+
284
+ # --- unset (style_unset.c) -------------------------------------------------------------------
285
+
286
+ def unset_bold = without(Keys::BOLD)
287
+ def unset_italic = without(Keys::ITALIC)
288
+ def unset_underline = without(Keys::UNDERLINE)
289
+ def unset_strikethrough = without(Keys::STRIKETHROUGH)
290
+ def unset_reverse = without(Keys::REVERSE)
291
+ def unset_blink = without(Keys::BLINK)
292
+ def unset_faint = without(Keys::FAINT)
293
+ def unset_foreground = without(Keys::FOREGROUND)
294
+ def unset_background = without(Keys::BACKGROUND)
295
+ def unset_width = without(Keys::WIDTH)
296
+ def unset_height = without(Keys::HEIGHT)
297
+ def unset_padding_top = without(Keys::PADDING_TOP)
298
+ def unset_padding_right = without(Keys::PADDING_RIGHT)
299
+ def unset_padding_bottom = without(Keys::PADDING_BOTTOM)
300
+ def unset_padding_left = without(Keys::PADDING_LEFT)
301
+ def unset_margin_top = without(Keys::MARGIN_TOP)
302
+ def unset_margin_right = without(Keys::MARGIN_RIGHT)
303
+ def unset_margin_bottom = without(Keys::MARGIN_BOTTOM)
304
+ def unset_margin_left = without(Keys::MARGIN_LEFT)
305
+ def unset_border_style = without(Keys::BORDER_STYLE)
306
+ def unset_inline = without(Keys::INLINE)
307
+
308
+ private
309
+
310
+ # --- state -----------------------------------------------------------------------------------
311
+
312
+ def reset_state
313
+ @props = 0
314
+ @attrs = 0
315
+ @values = {}
316
+ @value = ""
317
+ end
318
+
319
+ # The gem's dup/clone allocate a fresh, empty Go style.
320
+ def initialize_copy(_other)
321
+ super
322
+ reset_state
323
+ end
324
+
325
+ def load_state(props, attrs, values, value)
326
+ @props = props
327
+ @attrs = attrs
328
+ @values = values
329
+ @value = value
330
+ end
331
+
332
+ def derive
333
+ s = self.class.allocate
334
+ s.__send__(:load_state, @props, @attrs, @values.dup, @value)
335
+ yield s
336
+ s
337
+ end
338
+
339
+ def with(key, value)
340
+ derive { |s| s.__send__(:set!, key, value) }
341
+ end
342
+
343
+ def without(key)
344
+ derive { |s| s.__send__(:unset!, key) }
345
+ end
346
+
347
+ # Style.set (set.go)
348
+ def set!(key, value)
349
+ if Keys::STORED.key?(key)
350
+ value = [0, value].max if Keys::NON_NEGATIVE_SET.key?(key)
351
+ @values[key] = value
352
+ elsif value == true || value == false
353
+ value ? (@attrs |= key) : (@attrs &= ~key)
354
+ elsif value.is_a?(Integer)
355
+ (value & key).zero? ? (@attrs &= ~key) : (@attrs |= key)
356
+ end
357
+ @props |= key
358
+ end
359
+
360
+ def unset!(key)
361
+ @props &= ~key
362
+ end
363
+
364
+ def set?(key) = (@props & key) != 0
365
+
366
+ # Style.setFrom
367
+ def set_from!(key, other)
368
+ if Keys::STORED.key?(key)
369
+ set!(key, other.__send__(:raw_value, key))
370
+ else
371
+ set!(key, other.__send__(:raw_attrs))
372
+ end
373
+ end
374
+
375
+ def raw_value(key) = @values[key]
376
+ def raw_attrs = @attrs
377
+
378
+ # Style.Inherit
379
+ def inherit!(other)
380
+ key = Keys::FIRST
381
+ while key <= Keys::LAST
382
+ if other.__send__(:set?, key)
383
+ case key
384
+ when Keys::MARGIN_TOP, Keys::MARGIN_RIGHT, Keys::MARGIN_BOTTOM, Keys::MARGIN_LEFT,
385
+ Keys::PADDING_TOP, Keys::PADDING_RIGHT, Keys::PADDING_BOTTOM, Keys::PADDING_LEFT
386
+ key <<= 1
387
+ next
388
+ when Keys::BACKGROUND
389
+ if !set?(Keys::MARGIN_BACKGROUND) && !other.__send__(:set?, Keys::MARGIN_BACKGROUND)
390
+ set!(Keys::MARGIN_BACKGROUND, other.__send__(:raw_value, Keys::BACKGROUND))
391
+ end
392
+ end
393
+ set_from!(key, other) unless set?(key)
394
+ end
395
+ key <<= 1
396
+ end
397
+ end
398
+
399
+ # whichSidesInt + Padding / Margin
400
+ def set_sides!(kind, sides)
401
+ return unless (1..4).cover?(sides.length)
402
+
403
+ top, right, bottom, left = which_sides(sides)
404
+
405
+ if kind == :padding
406
+ set!(Keys::PADDING_TOP, top)
407
+ set!(Keys::PADDING_RIGHT, right)
408
+ set!(Keys::PADDING_BOTTOM, bottom)
409
+ set!(Keys::PADDING_LEFT, left)
410
+ else
411
+ set!(Keys::MARGIN_TOP, top)
412
+ set!(Keys::MARGIN_RIGHT, right)
413
+ set!(Keys::MARGIN_BOTTOM, bottom)
414
+ set!(Keys::MARGIN_LEFT, left)
415
+ end
416
+ end
417
+
418
+ # BorderForeground(c...) / BorderBackground(c...)
419
+ def set_border_colors!(kind, colors)
420
+ return if colors.empty?
421
+ return unless (1..4).cover?(colors.length)
422
+
423
+ top, right, bottom, left = which_sides(colors)
424
+ if kind == :foreground
425
+ set!(Keys::BORDER_TOP_FOREGROUND, top)
426
+ set!(Keys::BORDER_RIGHT_FOREGROUND, right)
427
+ set!(Keys::BORDER_BOTTOM_FOREGROUND, bottom)
428
+ set!(Keys::BORDER_LEFT_FOREGROUND, left)
429
+ else
430
+ set!(Keys::BORDER_TOP_BACKGROUND, top)
431
+ set!(Keys::BORDER_RIGHT_BACKGROUND, right)
432
+ set!(Keys::BORDER_BOTTOM_BACKGROUND, bottom)
433
+ set!(Keys::BORDER_LEFT_BACKGROUND, left)
434
+ end
435
+ end
436
+
437
+ # whichSides*: [top, right, bottom, left], or [] when the count isn't 1..4.
438
+ def which_sides(i)
439
+ case i.length
440
+ when 1 then [i[0], i[0], i[0], i[0]]
441
+ when 2 then [i[0], i[1], i[0], i[1]]
442
+ when 3 then [i[0], i[1], i[2], i[1]]
443
+ when 4 then [i[0], i[1], i[2], i[3]]
444
+ else []
445
+ end
446
+ end
447
+
448
+ # --- Go getters ------------------------------------------------------------------------------
449
+
450
+ def get_as_bool(key, default)
451
+ return default unless set?(key)
452
+
453
+ (@attrs & key) != 0
454
+ end
455
+
456
+ def get_as_color(key)
457
+ return NO_COLOR unless set?(key)
458
+
459
+ @values[key] || NO_COLOR
460
+ end
461
+
462
+ def get_as_int(key)
463
+ return 0 unless set?(key)
464
+
465
+ @values[key] || 0
466
+ end
467
+
468
+ def get_as_position(key)
469
+ return 0.0 unless set?(key)
470
+
471
+ @values[key] || 0.0
472
+ end
473
+
474
+ def get_border_style
475
+ return Gloss::Borders::NO_BORDER unless set?(Keys::BORDER_STYLE)
476
+
477
+ @values[Keys::BORDER_STYLE]
478
+ end
479
+
480
+ def implicit_borders?
481
+ get_border_style != Gloss::Borders::NO_BORDER &&
482
+ !(set?(Keys::BORDER_TOP) || set?(Keys::BORDER_RIGHT) || set?(Keys::BORDER_BOTTOM) || set?(Keys::BORDER_LEFT))
483
+ end
484
+
485
+ def get_padding
486
+ [get_as_int(Keys::PADDING_TOP), get_as_int(Keys::PADDING_RIGHT),
487
+ get_as_int(Keys::PADDING_BOTTOM), get_as_int(Keys::PADDING_LEFT)]
488
+ end
489
+
490
+ def get_margin
491
+ [get_as_int(Keys::MARGIN_TOP), get_as_int(Keys::MARGIN_RIGHT),
492
+ get_as_int(Keys::MARGIN_BOTTOM), get_as_int(Keys::MARGIN_LEFT)]
493
+ end
494
+
495
+ def get_horizontal_padding = get_as_int(Keys::PADDING_LEFT) + get_as_int(Keys::PADDING_RIGHT)
496
+ def get_vertical_padding = get_as_int(Keys::PADDING_TOP) + get_as_int(Keys::PADDING_BOTTOM)
497
+ def get_horizontal_margins = get_as_int(Keys::MARGIN_LEFT) + get_as_int(Keys::MARGIN_RIGHT)
498
+ def get_vertical_margins = get_as_int(Keys::MARGIN_TOP) + get_as_int(Keys::MARGIN_BOTTOM)
499
+
500
+ def get_border
501
+ [get_border_style, get_as_bool(Keys::BORDER_TOP, false), get_as_bool(Keys::BORDER_RIGHT, false),
502
+ get_as_bool(Keys::BORDER_BOTTOM, false), get_as_bool(Keys::BORDER_LEFT, false)]
503
+ end
504
+
505
+ def get_border_top_size
506
+ return 0 if !get_as_bool(Keys::BORDER_TOP, false) && !implicit_borders?
507
+
508
+ get_border_style.top_size
509
+ end
510
+
511
+ def get_border_right_size
512
+ return 0 if !get_as_bool(Keys::BORDER_RIGHT, false) && !implicit_borders?
513
+
514
+ get_border_style.right_size
515
+ end
516
+
517
+ def get_border_bottom_size
518
+ return 0 if !get_as_bool(Keys::BORDER_BOTTOM, false) && !implicit_borders?
519
+
520
+ get_border_style.bottom_size
521
+ end
522
+
523
+ def get_border_left_size
524
+ return 0 if !get_as_bool(Keys::BORDER_LEFT, false) && !implicit_borders?
525
+
526
+ get_border_style.left_size
527
+ end
528
+
529
+ def get_horizontal_border_size = get_border_left_size + get_border_right_size
530
+ def get_vertical_border_size = get_border_top_size + get_border_bottom_size
531
+ def get_horizontal_frame_size = get_horizontal_margins + get_horizontal_padding + get_horizontal_border_size
532
+ def get_vertical_frame_size = get_vertical_margins + get_vertical_padding + get_vertical_border_size
533
+
534
+ def get_align_horizontal = get_as_position(Keys::ALIGN_HORIZONTAL)
535
+ def get_align_vertical = get_as_position(Keys::ALIGN_VERTICAL)
536
+ def get_max_width = get_as_int(Keys::MAX_WIDTH)
537
+ def get_max_height = get_as_int(Keys::MAX_HEIGHT)
538
+ def get_inline = get_as_bool(Keys::INLINE, false)
539
+
540
+ # --- Go setters for the table / list / tree ports -------------------------------------------
541
+
542
+ # Border(b, sides...)
543
+ def set_border_def(border_def, *sides)
544
+ b = border_def.normalized.freeze
545
+ derive do |s|
546
+ s.__send__(:set!, Keys::BORDER_STYLE, b)
547
+ top, right, bottom, left = (1..4).cover?(sides.length) ? s.__send__(:which_sides, sides) : [true] * 4
548
+ s.__send__(:set!, Keys::BORDER_TOP, top ? true : false)
549
+ s.__send__(:set!, Keys::BORDER_RIGHT, right ? true : false)
550
+ s.__send__(:set!, Keys::BORDER_BOTTOM, bottom ? true : false)
551
+ s.__send__(:set!, Keys::BORDER_LEFT, left ? true : false)
552
+ end
553
+ end
554
+
555
+ # BorderStyle(b)
556
+ def set_border_style_def(border_def)
557
+ with(Keys::BORDER_STYLE, border_def.normalized.freeze)
558
+ end
559
+
560
+ def set_foreground_color(terminal_color) = with(Keys::FOREGROUND, terminal_color)
561
+ def set_background_color(terminal_color) = with(Keys::BACKGROUND, terminal_color)
562
+
563
+ # --- Ruby argument conversions (C extension) -------------------------------------------------
564
+
565
+ def num2int(value) = Gloss::Layout.num2int(value)
566
+
567
+ def spacing_args(values)
568
+ if values.empty? || values.length > 4
569
+ raise ArgumentError, "wrong number of arguments (given #{values.length}, expected 1..4)"
570
+ end
571
+
572
+ values.map { |v| num2int(v) }
573
+ end
574
+
575
+ def side_color(color)
576
+ Gloss::Renderer.terminal_color(color, adaptive: false, complete: false)
577
+ end
578
+
579
+ # terminalColorToString in go/style.go: only a plain Color has a string.
580
+ def color_string(color)
581
+ return nil unless color.is_a?(Gloss::TerminalColor::Color)
582
+ return nil if color.value.nil? || color.value.empty?
583
+
584
+ String.new(color.value, encoding: UTF_8)
585
+ end
586
+
587
+ def utf8(str)
588
+ str.b.force_encoding(UTF_8)
589
+ end
590
+
591
+ # StringValueCStr, then the bytes C (and Go's C.GoString) sees, as a UTF-8 string. For an
592
+ # ASCII-incompatible encoding (UTF-16, UTF-32) Ruby only rejects a NUL character, and C stops
593
+ # at the first NUL byte.
594
+ def c_str(value)
595
+ str = String.try_convert(value)
596
+ return utf8(Gloss::Layout.c_string(value)) if str.nil? || str.encoding.ascii_compatible?
597
+
598
+ nul = begin
599
+ "\0".encode(str.encoding)
600
+ rescue EncodingError
601
+ nil
602
+ end
603
+ raise ArgumentError, "string contains null byte" if nul && str.b.include?(nul.b) && str.include?(nul)
604
+
605
+ b = str.b
606
+ cut = b.index("\0")
607
+ utf8(cut ? b.byteslice(0, cut) : b)
608
+ end
609
+
610
+ # rb_utf8_str_new_cstr: a fresh, unfrozen UTF-8 string.
611
+ def c_result(str)
612
+ String.new(str, encoding: UTF_8)
613
+ end
614
+
615
+ NO_COLOR = Gloss::TerminalColor::NoColor.new.freeze
616
+ RIGHT = 1.0
617
+ CENTER = 0.5
618
+ BOTTOM = 1.0
619
+ TOP = 0.0
620
+ private_constant :NO_COLOR, :RIGHT, :CENTER, :BOTTOM, :TOP
621
+
622
+ def no_color?(color) = color.is_a?(Gloss::TerminalColor::NoColor)
623
+
624
+ # --- Render (style.go) -----------------------------------------------------------------------
625
+
626
+ def go_render(*strs)
627
+ strs = [@value, *strs] unless @value.empty?
628
+ str = strs.map { |x| utf8(x) }.join(" ")
629
+
630
+ r = Gloss::Renderer.default
631
+ p = r.color_profile
632
+ te = p.string
633
+ te_space = p.string
634
+ te_whitespace = p.string
635
+
636
+ bold = get_as_bool(Keys::BOLD, false)
637
+ italic = get_as_bool(Keys::ITALIC, false)
638
+ underline = get_as_bool(Keys::UNDERLINE, false)
639
+ strikethrough = get_as_bool(Keys::STRIKETHROUGH, false)
640
+ reverse = get_as_bool(Keys::REVERSE, false)
641
+ blink = get_as_bool(Keys::BLINK, false)
642
+ faint = get_as_bool(Keys::FAINT, false)
643
+
644
+ fg = get_as_color(Keys::FOREGROUND)
645
+ bg = get_as_color(Keys::BACKGROUND)
646
+
647
+ width = get_as_int(Keys::WIDTH)
648
+ height = get_as_int(Keys::HEIGHT)
649
+ horizontal_align = get_as_position(Keys::ALIGN_HORIZONTAL)
650
+ vertical_align = get_as_position(Keys::ALIGN_VERTICAL)
651
+
652
+ top_padding = get_as_int(Keys::PADDING_TOP)
653
+ right_padding = get_as_int(Keys::PADDING_RIGHT)
654
+ bottom_padding = get_as_int(Keys::PADDING_BOTTOM)
655
+ left_padding = get_as_int(Keys::PADDING_LEFT)
656
+
657
+ color_whitespace = get_as_bool(Keys::COLOR_WHITESPACE, true)
658
+ inline = get_as_bool(Keys::INLINE, false)
659
+ max_width = get_as_int(Keys::MAX_WIDTH)
660
+ max_height = get_as_int(Keys::MAX_HEIGHT)
661
+
662
+ underline_spaces = get_as_bool(Keys::UNDERLINE_SPACES, false) ||
663
+ (underline && get_as_bool(Keys::UNDERLINE_SPACES, true))
664
+ strikethrough_spaces = get_as_bool(Keys::STRIKETHROUGH_SPACES, false) ||
665
+ (strikethrough && get_as_bool(Keys::STRIKETHROUGH_SPACES, true))
666
+
667
+ style_whitespace = reverse
668
+ use_space_styler = (underline && !underline_spaces) || (strikethrough && !strikethrough_spaces) ||
669
+ underline_spaces || strikethrough_spaces
670
+
671
+ return maybe_convert_tabs(str) if @props.zero?
672
+
673
+ te = te.bold if bold
674
+ te = te.italic if italic
675
+ te = te.underline if underline
676
+ if reverse
677
+ te_whitespace = te_whitespace.reverse
678
+ te = te.reverse
679
+ end
680
+ te = te.blink if blink
681
+ te = te.faint if faint
682
+
683
+ unless no_color?(fg)
684
+ te = te.foreground(fg.color(r))
685
+ te_whitespace = te_whitespace.foreground(fg.color(r)) if style_whitespace
686
+ te_space = te_space.foreground(fg.color(r)) if use_space_styler
687
+ end
688
+
689
+ unless no_color?(bg)
690
+ te = te.background(bg.color(r))
691
+ te_whitespace = te_whitespace.background(bg.color(r)) if color_whitespace
692
+ te_space = te_space.background(bg.color(r)) if use_space_styler
693
+ end
694
+
695
+ te = te.underline if underline
696
+ te = te.cross_out if strikethrough
697
+
698
+ te_space = te_space.underline if underline_spaces
699
+ te_space = te_space.cross_out if strikethrough_spaces
700
+
701
+ str = maybe_convert_tabs(str)
702
+ str = replace_all(str, "\r\n", "\n")
703
+ str = replace_all(str, "\n", "") if inline
704
+
705
+ if !inline && width.positive?
706
+ wrap_at = width - left_padding - right_padding
707
+ str = Gloss::Text.cellbuf_wrap(str, wrap_at, "")
708
+ end
709
+
710
+ # Render core text
711
+ lines = go_split(str)
712
+ out = +""
713
+ lines.each_with_index do |line, i|
714
+ if use_space_styler
715
+ each_rune(line) do |rune, ch|
716
+ out << (Gloss::Text.space?(rune) ? te_space.styled(ch) : te.styled(ch))
717
+ end
718
+ else
719
+ out << te.styled(line)
720
+ end
721
+ out << "\n" if i != lines.length - 1
722
+ end
723
+ str = out
724
+
725
+ ws_style = color_whitespace || style_whitespace ? te_whitespace : nil
726
+
727
+ unless inline
728
+ str = pad(str, -left_padding, ws_style) if left_padding.positive?
729
+ str = pad(str, right_padding, ws_style) if right_padding.positive?
730
+ str = ("\n" * top_padding) + str if top_padding.positive?
731
+ str += "\n" * bottom_padding if bottom_padding.positive?
732
+ end
733
+
734
+ str = align_text_vertical(str, vertical_align, height) if height.positive?
735
+
736
+ num_lines = str.b.count("\n")
737
+ str = align_text_horizontal(str, horizontal_align, width, ws_style) if num_lines != 0 || width != 0
738
+
739
+ unless inline
740
+ str = apply_border(str)
741
+ str = apply_margins(str, inline)
742
+ end
743
+
744
+ if max_width.positive?
745
+ str = go_split(str).map { |l| Gloss::Text.truncate(l, max_width, "") }.join("\n")
746
+ end
747
+
748
+ if max_height.positive?
749
+ lines = go_split(str)
750
+ h = [max_height, lines.length].min
751
+ str = lines[0, h].join("\n") unless lines.empty?
752
+ end
753
+
754
+ str
755
+ end
756
+
757
+ def maybe_convert_tabs(str)
758
+ tw = set?(Keys::TAB_WIDTH) ? get_as_int(Keys::TAB_WIDTH) : TAB_WIDTH_DEFAULT
759
+ case tw
760
+ when -1 then str
761
+ when 0 then replace_all(str, "\t", "")
762
+ else replace_all(str, "\t", " " * tw)
763
+ end
764
+ end
765
+
766
+ def apply_margins(str, inline)
767
+ top_margin = get_as_int(Keys::MARGIN_TOP)
768
+ right_margin = get_as_int(Keys::MARGIN_RIGHT)
769
+ bottom_margin = get_as_int(Keys::MARGIN_BOTTOM)
770
+ left_margin = get_as_int(Keys::MARGIN_LEFT)
771
+
772
+ styler = Gloss::Termenv::Style.new
773
+ bgc = get_as_color(Keys::MARGIN_BACKGROUND)
774
+ styler = styler.background(bgc.color(Gloss::Renderer.default)) unless no_color?(bgc)
775
+
776
+ str = pad(str, -left_margin, styler)
777
+ str = pad(str, right_margin, styler)
778
+
779
+ unless inline
780
+ _lines, width = get_lines(str)
781
+ spaces = " " * width
782
+ str = styler.styled("#{spaces}\n" * top_margin) + str if top_margin.positive?
783
+ str += styler.styled("\n#{spaces}" * bottom_margin) if bottom_margin.positive?
784
+ end
785
+
786
+ str
787
+ end
788
+
789
+ def pad(str, n, style)
790
+ return str if n.zero?
791
+
792
+ sp = " " * n.abs
793
+ sp = style.styled(sp) if style
794
+
795
+ lines = go_split(str)
796
+ out = +""
797
+ lines.each_with_index do |l, i|
798
+ if n.positive?
799
+ out << l << sp
800
+ else
801
+ out << sp << l
802
+ end
803
+ out << "\n" if i != lines.length - 1
804
+ end
805
+ out
806
+ end
807
+
808
+ # --- align.go --------------------------------------------------------------------------------
809
+
810
+ def align_text_horizontal(str, pos, width, style)
811
+ lines, widest = get_lines(str)
812
+ out = +""
813
+ lines.each_with_index do |l, i|
814
+ line_width = Gloss::Text.string_width(l)
815
+ short = widest - line_width
816
+ short += [0, width - (short + line_width)].max
817
+ if short.positive?
818
+ if pos == RIGHT
819
+ s = " " * short
820
+ s = style.styled(s) if style
821
+ l = s + l
822
+ elsif pos == CENTER
823
+ left = short / 2
824
+ right = left + (short % 2)
825
+ left_spaces = " " * left
826
+ right_spaces = " " * right
827
+ if style
828
+ left_spaces = style.styled(left_spaces)
829
+ right_spaces = style.styled(right_spaces)
830
+ end
831
+ l = left_spaces + l + right_spaces
832
+ else
833
+ s = " " * short
834
+ s = style.styled(s) if style
835
+ l += s
836
+ end
837
+ end
838
+ out << l
839
+ out << "\n" if i < lines.length - 1
840
+ end
841
+ out
842
+ end
843
+
844
+ def align_text_vertical(str, pos, height)
845
+ str_height = str.b.count("\n") + 1
846
+ return str if height < str_height
847
+
848
+ if pos == TOP
849
+ str + ("\n" * (height - str_height))
850
+ elsif pos == CENTER
851
+ top_padding = (height - str_height) / 2
852
+ bottom_padding = top_padding
853
+ if str_height + top_padding + bottom_padding > height
854
+ top_padding -= 1
855
+ elsif str_height + top_padding + bottom_padding < height
856
+ bottom_padding += 1
857
+ end
858
+ ("\n" * top_padding) + str + ("\n" * bottom_padding)
859
+ elsif pos == BOTTOM
860
+ ("\n" * (height - str_height)) + str
861
+ else
862
+ str
863
+ end
864
+ end
865
+
866
+ # --- borders.go ------------------------------------------------------------------------------
867
+
868
+ def apply_border(str)
869
+ border = get_border_style.dup
870
+ has_top = get_as_bool(Keys::BORDER_TOP, false)
871
+ has_right = get_as_bool(Keys::BORDER_RIGHT, false)
872
+ has_bottom = get_as_bool(Keys::BORDER_BOTTOM, false)
873
+ has_left = get_as_bool(Keys::BORDER_LEFT, false)
874
+
875
+ top_fg = get_as_color(Keys::BORDER_TOP_FOREGROUND)
876
+ right_fg = get_as_color(Keys::BORDER_RIGHT_FOREGROUND)
877
+ bottom_fg = get_as_color(Keys::BORDER_BOTTOM_FOREGROUND)
878
+ left_fg = get_as_color(Keys::BORDER_LEFT_FOREGROUND)
879
+
880
+ top_bg = get_as_color(Keys::BORDER_TOP_BACKGROUND)
881
+ right_bg = get_as_color(Keys::BORDER_RIGHT_BACKGROUND)
882
+ bottom_bg = get_as_color(Keys::BORDER_BOTTOM_BACKGROUND)
883
+ left_bg = get_as_color(Keys::BORDER_LEFT_BACKGROUND)
884
+
885
+ if implicit_borders?
886
+ has_top = true
887
+ has_right = true
888
+ has_bottom = true
889
+ has_left = true
890
+ end
891
+
892
+ return str if border == Gloss::Borders::NO_BORDER || (!has_top && !has_right && !has_bottom && !has_left)
893
+
894
+ lines, width = get_lines(str)
895
+
896
+ if has_left
897
+ border.left = " " if border.left.empty?
898
+ width += Gloss::Borders.max_rune_width(border.left)
899
+ end
900
+
901
+ border.right = " " if has_right && border.right.empty?
902
+ border.top_left = " " if has_top && has_left && border.top_left.empty?
903
+ border.top_right = " " if has_top && has_right && border.top_right.empty?
904
+ border.bottom_left = " " if has_bottom && has_left && border.bottom_left.empty?
905
+ border.bottom_right = " " if has_bottom && has_right && border.bottom_right.empty?
906
+
907
+ if has_top
908
+ if !has_left && !has_right
909
+ border.top_left = ""
910
+ border.top_right = ""
911
+ elsif !has_left
912
+ border.top_left = ""
913
+ elsif !has_right
914
+ border.top_right = ""
915
+ end
916
+ end
917
+
918
+ if has_bottom
919
+ if !has_left && !has_right
920
+ border.bottom_left = ""
921
+ border.bottom_right = ""
922
+ elsif !has_left
923
+ border.bottom_left = ""
924
+ elsif !has_right
925
+ border.bottom_right = ""
926
+ end
927
+ end
928
+
929
+ border.top_left = first_rune(border.top_left)
930
+ border.top_right = first_rune(border.top_right)
931
+ border.bottom_right = first_rune(border.bottom_right)
932
+ border.bottom_left = first_rune(border.bottom_left)
933
+
934
+ out = +""
935
+ if has_top
936
+ top = render_horizontal_edge(border.top_left, border.top, border.top_right, width)
937
+ out << style_border(top, top_fg, top_bg) << "\n"
938
+ end
939
+
940
+ left_runes = go_runes(border.left)
941
+ left_index = 0
942
+ right_runes = go_runes(border.right)
943
+ right_index = 0
944
+
945
+ lines.each_with_index do |l, i|
946
+ if has_left
947
+ ch = left_runes[left_index]
948
+ left_index += 1
949
+ left_index = 0 if left_index >= left_runes.length
950
+ out << style_border(ch, left_fg, left_bg)
951
+ end
952
+ out << l
953
+ if has_right
954
+ ch = right_runes[right_index]
955
+ right_index += 1
956
+ right_index = 0 if right_index >= right_runes.length
957
+ out << style_border(ch, right_fg, right_bg)
958
+ end
959
+ out << "\n" if i < lines.length - 1
960
+ end
961
+
962
+ if has_bottom
963
+ bottom = render_horizontal_edge(border.bottom_left, border.bottom, border.bottom_right, width)
964
+ out << "\n" << style_border(bottom, bottom_fg, bottom_bg)
965
+ end
966
+
967
+ out
968
+ end
969
+
970
+ def render_horizontal_edge(left, middle, right, width)
971
+ middle = " " if middle.empty?
972
+ left_width = Gloss::Text.string_width(left)
973
+ right_width = Gloss::Text.string_width(right)
974
+
975
+ runes = go_runes(middle)
976
+ widths = runes.map { |ch| Gloss::Text.string_width(ch) }
977
+ j = 0
978
+ out = +""
979
+ out << left
980
+ i = left_width + right_width
981
+ # Go loops forever when every rune of the edge is zero-width; stop instead.
982
+ if widths.any?(&:positive?)
983
+ while i < width + right_width
984
+ out << runes[j]
985
+ j += 1
986
+ j = 0 if j >= runes.length
987
+ i += widths[j]
988
+ end
989
+ end
990
+ out << right
991
+ out
992
+ end
993
+
994
+ def style_border(border, fg, bg)
995
+ return border if no_color?(fg) && no_color?(bg)
996
+
997
+ r = Gloss::Renderer.default
998
+ style = Gloss::Termenv::Style.new
999
+ style = style.foreground(fg.color(r)) unless no_color?(fg)
1000
+ style = style.background(bg.color(r)) unless no_color?(bg)
1001
+ style.styled(border)
1002
+ end
1003
+
1004
+ def first_rune(str)
1005
+ return str if str.empty?
1006
+
1007
+ go_runes(str).first
1008
+ end
1009
+
1010
+ # --- Go string helpers -----------------------------------------------------------------------
1011
+
1012
+ # strings.Split(s, "\n")
1013
+ def go_split(str)
1014
+ return [+""] if str.empty?
1015
+
1016
+ str.b.split("\n", -1).map { |l| l.force_encoding(UTF_8) }
1017
+ end
1018
+
1019
+ # strings.ReplaceAll on bytes.
1020
+ def replace_all(str, from, to)
1021
+ b = str.b
1022
+ return str unless b.include?(from)
1023
+
1024
+ b.gsub(from.b, to.b).force_encoding(UTF_8)
1025
+ end
1026
+
1027
+ # getLines
1028
+ def get_lines(str)
1029
+ lines = go_split(str)
1030
+ widest = 0
1031
+ lines.each do |l|
1032
+ w = Gloss::Text.string_width(l)
1033
+ widest = w if widest < w
1034
+ end
1035
+ [lines, widest]
1036
+ end
1037
+
1038
+ # Ranges over a Go string: yields [rune, utf-8 string]; invalid bytes yield U+FFFD.
1039
+ def each_rune(str)
1040
+ if str.ascii_only?
1041
+ str.each_char { |ch| yield ch.ord, ch }
1042
+ return
1043
+ end
1044
+
1045
+ b = str.b
1046
+ n = b.bytesize
1047
+ i = 0
1048
+ while i < n
1049
+ r, size = Gloss::Text.decode_rune(b, i, n)
1050
+ yield r, (r < 0x80 ? r.chr : [r].pack("U"))
1051
+ i += size
1052
+ end
1053
+ end
1054
+
1055
+ # []rune(s) as strings.
1056
+ def go_runes(str)
1057
+ out = []
1058
+ each_rune(str) { |_r, ch| out << ch }
1059
+ out
1060
+ end
1061
+ end
1062
+ end