whirled_peas 0.1.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.
- checksums.yaml +7 -0
- data/.gitignore +14 -0
- data/.rspec +3 -0
- data/.travis.yml +6 -0
- data/CHANGELOG.md +0 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +8 -0
- data/LICENSE.txt +21 -0
- data/README.md +331 -0
- data/Rakefile +6 -0
- data/bin/console +7 -0
- data/bin/setup +8 -0
- data/lib/whirled_peas.rb +46 -0
- data/lib/whirled_peas/frame.rb +14 -0
- data/lib/whirled_peas/frame/consumer.rb +61 -0
- data/lib/whirled_peas/frame/loop.rb +56 -0
- data/lib/whirled_peas/frame/producer.rb +57 -0
- data/lib/whirled_peas/null_logger.rb +20 -0
- data/lib/whirled_peas/ui.rb +7 -0
- data/lib/whirled_peas/ui/ansi.rb +154 -0
- data/lib/whirled_peas/ui/canvas.rb +35 -0
- data/lib/whirled_peas/ui/element.rb +199 -0
- data/lib/whirled_peas/ui/painter.rb +283 -0
- data/lib/whirled_peas/ui/screen.rb +62 -0
- data/lib/whirled_peas/ui/settings.rb +512 -0
- data/lib/whirled_peas/ui/stroke.rb +29 -0
- data/lib/whirled_peas/version.rb +3 -0
- data/sandbox/auto.rb +13 -0
- data/sandbox/box.rb +19 -0
- data/sandbox/grid.rb +13 -0
- data/sandbox/sandbox.rb +17 -0
- data/sandbox/text.rb +33 -0
- data/whirled_peas.gemspec +28 -0
- metadata +120 -0
@@ -0,0 +1,62 @@
|
|
1
|
+
require 'highline'
|
2
|
+
require 'tty-cursor'
|
3
|
+
|
4
|
+
require_relative 'ansi'
|
5
|
+
require_relative 'painter'
|
6
|
+
|
7
|
+
module WhirledPeas
|
8
|
+
module UI
|
9
|
+
class Screen
|
10
|
+
def initialize(print_output=true)
|
11
|
+
@print_output = print_output
|
12
|
+
@terminal = HighLine.new.terminal
|
13
|
+
@cursor = TTY::Cursor
|
14
|
+
@strokes = []
|
15
|
+
refresh_size!
|
16
|
+
Signal.trap('SIGWINCH', proc { self.refresh_size! })
|
17
|
+
end
|
18
|
+
|
19
|
+
def paint(template)
|
20
|
+
@template = template
|
21
|
+
refresh
|
22
|
+
end
|
23
|
+
|
24
|
+
def needs_refresh?
|
25
|
+
@refreshed_width != width || @refreshed_height != height
|
26
|
+
end
|
27
|
+
|
28
|
+
def refresh
|
29
|
+
strokes = [cursor.hide, cursor.move_to(0, 0), cursor.clear_screen_down]
|
30
|
+
Painter.paint(@template, Canvas.new(0, 0, width, height)) do |stroke|
|
31
|
+
unless stroke.chars.nil?
|
32
|
+
strokes << cursor.move_to(stroke.left, stroke.top)
|
33
|
+
strokes << stroke.chars
|
34
|
+
end
|
35
|
+
end
|
36
|
+
return unless @print_output
|
37
|
+
strokes.each(&method(:print))
|
38
|
+
STDOUT.flush
|
39
|
+
@refreshed_width = width
|
40
|
+
@refreshed_height = height
|
41
|
+
end
|
42
|
+
|
43
|
+
def finalize
|
44
|
+
return unless @print_output
|
45
|
+
print UI::Ansi.clear
|
46
|
+
print cursor.move_to(0, height - 1)
|
47
|
+
print cursor.show
|
48
|
+
STDOUT.flush
|
49
|
+
end
|
50
|
+
|
51
|
+
protected
|
52
|
+
|
53
|
+
def refresh_size!
|
54
|
+
@width, @height = terminal.terminal_size
|
55
|
+
end
|
56
|
+
|
57
|
+
private
|
58
|
+
|
59
|
+
attr_reader :cursor, :terminal, :width, :height
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,512 @@
|
|
1
|
+
require 'json'
|
2
|
+
|
3
|
+
module WhirledPeas
|
4
|
+
module UI
|
5
|
+
module TextAlign
|
6
|
+
LEFT = :left
|
7
|
+
CENTER = :center
|
8
|
+
RIGHT = :right
|
9
|
+
|
10
|
+
def self.validate!(align)
|
11
|
+
return unless align
|
12
|
+
if [TextAlign::LEFT, TextAlign::CENTER, TextAlign::RIGHT].include?(align)
|
13
|
+
align
|
14
|
+
else
|
15
|
+
raise ArgumentError, "Invalid alignment: #{align}"
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
module DisplayFlow
|
21
|
+
LEFT_TO_RIGHT = :l2r
|
22
|
+
RIGHT_TO_LEFT = :r2l
|
23
|
+
TOP_TO_BOTTOM = :t2b
|
24
|
+
BOTTOM_TO_TOP = :b2t
|
25
|
+
|
26
|
+
def self.validate!(flow)
|
27
|
+
return unless flow
|
28
|
+
if [
|
29
|
+
DisplayFlow::LEFT_TO_RIGHT,
|
30
|
+
DisplayFlow::RIGHT_TO_LEFT,
|
31
|
+
DisplayFlow::TOP_TO_BOTTOM,
|
32
|
+
DisplayFlow::BOTTOM_TO_TOP
|
33
|
+
].include?(flow)
|
34
|
+
flow
|
35
|
+
else
|
36
|
+
raise ArgumentError, "Invalid flow: #{flow}"
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
class Spacing
|
42
|
+
def left
|
43
|
+
@_left || 0
|
44
|
+
end
|
45
|
+
|
46
|
+
def left=(val)
|
47
|
+
@_left = val
|
48
|
+
end
|
49
|
+
|
50
|
+
def top
|
51
|
+
@_top || 0
|
52
|
+
end
|
53
|
+
|
54
|
+
def top=(val)
|
55
|
+
@_top = val
|
56
|
+
end
|
57
|
+
|
58
|
+
def right
|
59
|
+
@_right || 0
|
60
|
+
end
|
61
|
+
|
62
|
+
def right=(val)
|
63
|
+
@_right = val
|
64
|
+
end
|
65
|
+
|
66
|
+
def bottom
|
67
|
+
@_bottom || 0
|
68
|
+
end
|
69
|
+
|
70
|
+
def bottom=(val)
|
71
|
+
@_bottom = val
|
72
|
+
end
|
73
|
+
|
74
|
+
def merge(parent)
|
75
|
+
merged = Spacing.new
|
76
|
+
merged._left = @_left || parent._left
|
77
|
+
merged._top = @_top || parent._top
|
78
|
+
merged._right = @_right || parent._right
|
79
|
+
merged._bottom = @_bottom || parent._bottom
|
80
|
+
merged
|
81
|
+
end
|
82
|
+
|
83
|
+
def inspect
|
84
|
+
vals = {}
|
85
|
+
vals[:left] = @_left if @_left
|
86
|
+
vals[:top] = @_top if @_top
|
87
|
+
vals[:right] = @_right if @_right
|
88
|
+
vals[:bottom] = @_bottom if @_bottom
|
89
|
+
JSON.generate(vals) if vals.any?
|
90
|
+
end
|
91
|
+
|
92
|
+
protected
|
93
|
+
|
94
|
+
attr_accessor :_left, :_top, :_right, :_bottom
|
95
|
+
end
|
96
|
+
private_constant :Spacing
|
97
|
+
|
98
|
+
class Padding < Spacing
|
99
|
+
end
|
100
|
+
|
101
|
+
class Margin < Spacing
|
102
|
+
end
|
103
|
+
|
104
|
+
BorderStyle = Struct.new(
|
105
|
+
:top_left, :top_horiz, :top_junc, :top_right,
|
106
|
+
:left_vert, :left_junc,
|
107
|
+
:middle_vert, :cross_junc, :middle_horiz,
|
108
|
+
:right_vert, :right_junc,
|
109
|
+
:bottom_left, :bottom_horiz, :bottom_junc, :bottom_right
|
110
|
+
)
|
111
|
+
|
112
|
+
module BorderStyles
|
113
|
+
BOLD = BorderStyle.new(
|
114
|
+
'┏', '━', '┳', '┓',
|
115
|
+
'┃', '┣',
|
116
|
+
'┃', '╋', '━',
|
117
|
+
'┃', '┫',
|
118
|
+
'┗', '━', '┻', '┛'
|
119
|
+
)
|
120
|
+
SOFT = BorderStyle.new(
|
121
|
+
'╭', '─', '┬', '╮',
|
122
|
+
'│', '├',
|
123
|
+
'│', '┼', '─',
|
124
|
+
'│', '┤',
|
125
|
+
'╰', '─', '┴', '╯'
|
126
|
+
)
|
127
|
+
DOUBLE = BorderStyle.new(
|
128
|
+
'╔', '═', '╦', '╗',
|
129
|
+
'║', '╠',
|
130
|
+
'║', '╬', '═',
|
131
|
+
'║', '╣',
|
132
|
+
'╚', '═', '╩', '╝'
|
133
|
+
)
|
134
|
+
|
135
|
+
def self.validate!(style)
|
136
|
+
return unless style
|
137
|
+
if style.is_a?(Symbol)
|
138
|
+
error_message = "Unsupported border style: #{style}"
|
139
|
+
begin
|
140
|
+
style = self.const_get(style.upcase)
|
141
|
+
raise ArgumentError, error_message unless style.is_a?(BorderStyle)
|
142
|
+
style
|
143
|
+
rescue NameError
|
144
|
+
raise ArgumentError, error_message
|
145
|
+
end
|
146
|
+
else
|
147
|
+
style
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
class Border
|
153
|
+
def left?
|
154
|
+
@_left == true
|
155
|
+
end
|
156
|
+
|
157
|
+
def left=(val)
|
158
|
+
@_left = val
|
159
|
+
end
|
160
|
+
|
161
|
+
def top?
|
162
|
+
@_top == true
|
163
|
+
end
|
164
|
+
|
165
|
+
def top=(val)
|
166
|
+
@_top = val
|
167
|
+
end
|
168
|
+
|
169
|
+
def right?
|
170
|
+
@_right == true
|
171
|
+
end
|
172
|
+
|
173
|
+
def right=(val)
|
174
|
+
@_right = val
|
175
|
+
end
|
176
|
+
|
177
|
+
def bottom?
|
178
|
+
@_bottom == true
|
179
|
+
end
|
180
|
+
|
181
|
+
def bottom=(val)
|
182
|
+
@_bottom = val
|
183
|
+
end
|
184
|
+
|
185
|
+
def inner_horiz?
|
186
|
+
@_inner_horiz == true
|
187
|
+
end
|
188
|
+
|
189
|
+
def inner_horiz=(val)
|
190
|
+
@_inner_horiz = val
|
191
|
+
end
|
192
|
+
|
193
|
+
def inner_vert?
|
194
|
+
@_inner_vert == true
|
195
|
+
end
|
196
|
+
|
197
|
+
def inner_vert=(val)
|
198
|
+
@_inner_vert = val
|
199
|
+
end
|
200
|
+
|
201
|
+
def style
|
202
|
+
@_style || BorderStyles::BOLD
|
203
|
+
end
|
204
|
+
|
205
|
+
def style=(val)
|
206
|
+
@_style = BorderStyles.validate!(val)
|
207
|
+
end
|
208
|
+
|
209
|
+
def color
|
210
|
+
@_color || TextColor::WHITE
|
211
|
+
end
|
212
|
+
|
213
|
+
def color=(val)
|
214
|
+
@_color = TextColor.validate!(val)
|
215
|
+
end
|
216
|
+
|
217
|
+
def merge(parent)
|
218
|
+
merged = Border.new
|
219
|
+
merged._left = @_left.nil? ? parent._left : @_left
|
220
|
+
merged._top = @_top.nil? ? parent._top : @_top
|
221
|
+
merged._right = @_right.nil? ? parent._right : @_right
|
222
|
+
merged._bottom = @_bottom.nil? ? parent._bottom : @_bottom
|
223
|
+
merged._inner_horiz = @_inner_horiz.nil? ? parent._inner_horiz : @_inner_horiz
|
224
|
+
merged._inner_vert = @_inner_vert.nil? ? parent._inner_vert : @_inner_vert
|
225
|
+
merged._style = @_style || parent._style
|
226
|
+
merged._color = @_color || parent._color
|
227
|
+
merged
|
228
|
+
end
|
229
|
+
|
230
|
+
def inspect
|
231
|
+
vals = {}
|
232
|
+
vals[:left] = @_left unless @_left.nil?
|
233
|
+
vals[:top] = @_top unless @_top.nil?
|
234
|
+
vals[:right] = @_right unless @_right.nil?
|
235
|
+
vals[:bottom] = @_bottom unless @_bottom.nil?
|
236
|
+
vals[:inner_horiz] = @_inner_horiz unless @_inner_horiz.nil?
|
237
|
+
vals[:inner_vert] = @_inner_vert unless @_inner_vert.nil?
|
238
|
+
JSON.generate(vals) if vals.any?
|
239
|
+
end
|
240
|
+
|
241
|
+
protected
|
242
|
+
|
243
|
+
attr_accessor :_left, :_top, :_right, :_bottom, :_inner_horiz, :_inner_vert, :_style, :_color
|
244
|
+
end
|
245
|
+
|
246
|
+
class ElementSettings
|
247
|
+
def self.merge(parent)
|
248
|
+
self.new.merge(parent)
|
249
|
+
end
|
250
|
+
|
251
|
+
def color
|
252
|
+
@_color
|
253
|
+
end
|
254
|
+
|
255
|
+
def color=(color)
|
256
|
+
@_color = TextColor.validate!(color)
|
257
|
+
end
|
258
|
+
|
259
|
+
def bg_color
|
260
|
+
@_bg_color
|
261
|
+
end
|
262
|
+
|
263
|
+
def bg_color=(color)
|
264
|
+
@_bg_color = BgColor.validate!(color)
|
265
|
+
end
|
266
|
+
|
267
|
+
def bold?
|
268
|
+
@_bold || false
|
269
|
+
end
|
270
|
+
|
271
|
+
def bold=(val)
|
272
|
+
@_bold = val
|
273
|
+
end
|
274
|
+
|
275
|
+
def underline?
|
276
|
+
@_underline || false
|
277
|
+
end
|
278
|
+
|
279
|
+
def underline=(val)
|
280
|
+
@_underline = val
|
281
|
+
end
|
282
|
+
|
283
|
+
def merge(parent)
|
284
|
+
merged = self.class.new
|
285
|
+
merged.color = @color || parent.color
|
286
|
+
merged.bg_color = @bg_color || parent.bg_color
|
287
|
+
merged._bold = @_bold.nil? ? parent._bold : @_bold
|
288
|
+
merged._underline = @_underline.nil? ? parent._underline : @_underline
|
289
|
+
merged
|
290
|
+
end
|
291
|
+
|
292
|
+
def inspect(indent='')
|
293
|
+
values = self.instance_variables.map do |v|
|
294
|
+
unless instance_variable_get(v).nil? || instance_variable_get(v).inspect.nil?
|
295
|
+
"#{indent} #{v} = #{instance_variable_get(v).inspect}"
|
296
|
+
end
|
297
|
+
end.compact
|
298
|
+
details = values.length > 0 ? values.join("\n") : "#{indent} <default>"
|
299
|
+
"#{indent}#{self.class.name}\n#{details}"
|
300
|
+
end
|
301
|
+
|
302
|
+
protected
|
303
|
+
|
304
|
+
attr_accessor :_bold, :_underline
|
305
|
+
end
|
306
|
+
private_constant :ElementSettings
|
307
|
+
|
308
|
+
module WidthSetting
|
309
|
+
attr_accessor :width
|
310
|
+
end
|
311
|
+
|
312
|
+
module AlignSetting
|
313
|
+
def align
|
314
|
+
@_align || TextAlign::LEFT
|
315
|
+
end
|
316
|
+
|
317
|
+
def align=(align)
|
318
|
+
@_align = TextAlign.validate!(align)
|
319
|
+
end
|
320
|
+
|
321
|
+
def merge(parent)
|
322
|
+
merged = super
|
323
|
+
merged._align = if @_align
|
324
|
+
@_align
|
325
|
+
elsif parent.is_a?(AlignSetting)
|
326
|
+
parent._align
|
327
|
+
end
|
328
|
+
merged
|
329
|
+
end
|
330
|
+
|
331
|
+
protected
|
332
|
+
|
333
|
+
attr_accessor :_align
|
334
|
+
end
|
335
|
+
|
336
|
+
module MarginSettings
|
337
|
+
def set_margin(left: nil, top: nil, right: nil, bottom: nil)
|
338
|
+
@_margin = Margin.new unless @_margin
|
339
|
+
@_margin.left = left if left
|
340
|
+
@_margin.top = top if top
|
341
|
+
@_margin.right = right if right
|
342
|
+
@_margin.bottom = bottom if bottom
|
343
|
+
end
|
344
|
+
|
345
|
+
def margin
|
346
|
+
@_margin || Margin.new
|
347
|
+
end
|
348
|
+
|
349
|
+
def auto_margin=(val)
|
350
|
+
@_auto_margin = val
|
351
|
+
end
|
352
|
+
|
353
|
+
def auto_margin?
|
354
|
+
@_auto_margin || false
|
355
|
+
end
|
356
|
+
|
357
|
+
def merge(parent)
|
358
|
+
merged = super
|
359
|
+
if parent.is_a?(MarginSettings)
|
360
|
+
merged._margin = margin.merge(parent.margin)
|
361
|
+
merged._auto_margin = @_auto_margin.nil? ? parent._auto_margin : @_auto_margin
|
362
|
+
else
|
363
|
+
merged._margin = _margin
|
364
|
+
merged._auto_margin = @_auto_margin
|
365
|
+
end
|
366
|
+
merged
|
367
|
+
end
|
368
|
+
|
369
|
+
protected
|
370
|
+
|
371
|
+
attr_accessor :_margin, :_auto_margin
|
372
|
+
end
|
373
|
+
private_constant :MarginSettings
|
374
|
+
|
375
|
+
module BorderSettings
|
376
|
+
def set_border(
|
377
|
+
left: nil, top: nil, right: nil, bottom: nil, inner_horiz: nil, inner_vert: nil, style: nil, color: nil
|
378
|
+
)
|
379
|
+
@_border = Border.new unless @_border
|
380
|
+
@_border.left = left unless left.nil?
|
381
|
+
@_border.top = top unless top.nil?
|
382
|
+
@_border.right = right unless right.nil?
|
383
|
+
@_border.bottom = bottom unless bottom.nil?
|
384
|
+
@_border.inner_horiz = inner_horiz unless inner_horiz.nil?
|
385
|
+
@_border.inner_vert = inner_vert unless inner_vert.nil?
|
386
|
+
@_border.style = style unless style.nil?
|
387
|
+
@_border.color = color unless color.nil?
|
388
|
+
end
|
389
|
+
|
390
|
+
def no_border
|
391
|
+
set_border(
|
392
|
+
left: false, top: false, right: false, bottom: false, inner_horiz: false, inner_vert: false
|
393
|
+
)
|
394
|
+
end
|
395
|
+
|
396
|
+
def full_border(style: nil, color: nil)
|
397
|
+
set_border(
|
398
|
+
left: true, top: true, right: true, bottom: true, inner_horiz: true, inner_vert: true, style: style, color: color
|
399
|
+
)
|
400
|
+
end
|
401
|
+
|
402
|
+
def border
|
403
|
+
@_border || Border.new
|
404
|
+
end
|
405
|
+
|
406
|
+
def merge(parent)
|
407
|
+
merged = super
|
408
|
+
merged._border = parent.is_a?(BorderSettings) ? border.merge(parent.border) : _border
|
409
|
+
merged
|
410
|
+
end
|
411
|
+
|
412
|
+
protected
|
413
|
+
|
414
|
+
attr_accessor :_border
|
415
|
+
end
|
416
|
+
private_constant :BorderSettings
|
417
|
+
|
418
|
+
module PaddingSettings
|
419
|
+
def set_padding(left: nil, top: nil, right: nil, bottom: nil)
|
420
|
+
@_padding = Padding.new unless @_padding
|
421
|
+
@_padding.left = left if left
|
422
|
+
@_padding.top = top if top
|
423
|
+
@_padding.right = right if right
|
424
|
+
@_padding.bottom = bottom if bottom
|
425
|
+
end
|
426
|
+
|
427
|
+
def padding
|
428
|
+
@_padding || Padding.new
|
429
|
+
end
|
430
|
+
|
431
|
+
def merge(parent)
|
432
|
+
merged = super
|
433
|
+
merged._padding = parent.is_a?(PaddingSettings) ? padding.merge(parent.padding) : _padding
|
434
|
+
merged
|
435
|
+
end
|
436
|
+
|
437
|
+
protected
|
438
|
+
|
439
|
+
attr_accessor :_padding
|
440
|
+
end
|
441
|
+
private_constant :PaddingSettings
|
442
|
+
|
443
|
+
class TemplateSettings < ElementSettings
|
444
|
+
end
|
445
|
+
|
446
|
+
class TextSettings < ElementSettings
|
447
|
+
include WidthSetting
|
448
|
+
include AlignSetting
|
449
|
+
end
|
450
|
+
|
451
|
+
class ContainerSettings < ElementSettings
|
452
|
+
include MarginSettings
|
453
|
+
include BorderSettings
|
454
|
+
include PaddingSettings
|
455
|
+
end
|
456
|
+
|
457
|
+
class BoxSettings < ContainerSettings
|
458
|
+
include WidthSetting
|
459
|
+
include AlignSetting
|
460
|
+
|
461
|
+
def flow=(flow)
|
462
|
+
@_flow = DisplayFlow.validate!(flow)
|
463
|
+
end
|
464
|
+
|
465
|
+
def flow
|
466
|
+
@_flow || DisplayFlow::LEFT_TO_RIGHT
|
467
|
+
end
|
468
|
+
|
469
|
+
def horizontal_flow?
|
470
|
+
%i[l2r r2l].include?(flow)
|
471
|
+
end
|
472
|
+
|
473
|
+
def vertical_flow?
|
474
|
+
!horizontal_flow?
|
475
|
+
end
|
476
|
+
|
477
|
+
def forward_flow?
|
478
|
+
%i[l2r t2b].include?(flow)
|
479
|
+
end
|
480
|
+
|
481
|
+
def reverse_flow?
|
482
|
+
!forward_flow?
|
483
|
+
end
|
484
|
+
|
485
|
+
def merge(parent)
|
486
|
+
merged = super
|
487
|
+
merged._flow = if @_flow
|
488
|
+
@_flow
|
489
|
+
elsif parent.is_a?(BoxSettings)
|
490
|
+
parent._flow
|
491
|
+
end
|
492
|
+
merged
|
493
|
+
end
|
494
|
+
|
495
|
+
protected
|
496
|
+
|
497
|
+
attr_accessor :_flow
|
498
|
+
end
|
499
|
+
|
500
|
+
class GridSettings < ContainerSettings
|
501
|
+
include WidthSetting
|
502
|
+
include AlignSetting
|
503
|
+
|
504
|
+
attr_accessor :num_cols
|
505
|
+
attr_writer :transpose
|
506
|
+
|
507
|
+
def transpose?
|
508
|
+
@transpose || false
|
509
|
+
end
|
510
|
+
end
|
511
|
+
end
|
512
|
+
end
|