whirled_peas 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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