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