whirled_peas 0.1.0 → 0.4.1
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 +4 -4
- data/.travis.yml +2 -0
- data/CHANGELOG.md +29 -0
- data/Gemfile +1 -0
- data/README.md +213 -48
- data/bin/title_fonts +6 -0
- data/exe/whirled_peas +7 -0
- data/lib/whirled_peas.rb +12 -30
- data/lib/whirled_peas/command_line.rb +270 -0
- data/lib/whirled_peas/config.rb +21 -0
- data/lib/whirled_peas/errors.rb +5 -0
- data/lib/whirled_peas/frame.rb +0 -7
- data/lib/whirled_peas/frame/event_loop.rb +91 -0
- data/lib/whirled_peas/frame/print_consumer.rb +33 -0
- data/lib/whirled_peas/frame/producer.rb +35 -31
- data/lib/whirled_peas/template.rb +5 -0
- data/lib/whirled_peas/template/element.rb +230 -0
- data/lib/whirled_peas/{ui → template}/settings.rb +28 -10
- data/lib/whirled_peas/ui.rb +1 -3
- data/lib/whirled_peas/ui/canvas.rb +37 -4
- data/lib/whirled_peas/ui/painter.rb +22 -18
- data/lib/whirled_peas/ui/screen.rb +24 -23
- data/lib/whirled_peas/utils.rb +5 -0
- data/lib/whirled_peas/utils/ansi.rb +103 -0
- data/lib/whirled_peas/{ui/ansi.rb → utils/color.rb} +23 -76
- data/lib/whirled_peas/utils/title_font.rb +75 -0
- data/lib/whirled_peas/version.rb +1 -1
- data/whirled_peas.gemspec +4 -2
- metadata +22 -18
- data/lib/whirled_peas/frame/consumer.rb +0 -61
- data/lib/whirled_peas/frame/loop.rb +0 -56
- data/lib/whirled_peas/ui/element.rb +0 -199
- data/lib/whirled_peas/ui/stroke.rb +0 -29
- data/sandbox/auto.rb +0 -13
- data/sandbox/box.rb +0 -19
- data/sandbox/grid.rb +0 -13
- data/sandbox/sandbox.rb +0 -17
- data/sandbox/text.rb +0 -33
@@ -0,0 +1,33 @@
|
|
1
|
+
require_relative '../null_logger'
|
2
|
+
|
3
|
+
module WhirledPeas
|
4
|
+
module Frame
|
5
|
+
class PrintConsumer
|
6
|
+
LOGGER_ID = 'EVENT LOOP'
|
7
|
+
|
8
|
+
def initialize(logger=NullLogger.new)
|
9
|
+
@logger = logger
|
10
|
+
end
|
11
|
+
|
12
|
+
def enqueue(name, duration, args)
|
13
|
+
if name == Frame::EOF
|
14
|
+
puts "EOF frame detected"
|
15
|
+
else
|
16
|
+
displayed_for = duration ? "#{duration} second(s)" : '1 frame'
|
17
|
+
args_str = args.empty? ? '' : " (#{args.inspect})"
|
18
|
+
puts "Frame '#{name}' displayed for #{displayed_for}#{args_str}"
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def start
|
23
|
+
end
|
24
|
+
|
25
|
+
def stop
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
attr_reader :logger
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -1,57 +1,61 @@
|
|
1
1
|
require 'socket'
|
2
2
|
require 'json'
|
3
3
|
|
4
|
+
require_relative '../null_logger'
|
5
|
+
|
4
6
|
module WhirledPeas
|
5
7
|
module Frame
|
6
8
|
class Producer
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
9
|
+
LOGGER_ID = 'PRODUCER'
|
10
|
+
|
11
|
+
# Manages the EventLoop lifecycle and yields a Producer to send frames to the
|
12
|
+
# EventLoop
|
13
|
+
def self.produce(consumer, logger=NullLogger.new)
|
14
|
+
producer = new(consumer, logger)
|
15
|
+
consumer_thread = Thread.new do
|
16
|
+
Thread.current.report_on_exception = false
|
17
|
+
consumer.start
|
18
|
+
end
|
12
19
|
yield producer
|
13
|
-
|
20
|
+
producer.send_frame(Frame::EOF)
|
21
|
+
producer.flush
|
14
22
|
rescue => e
|
15
|
-
|
16
|
-
logger.
|
17
|
-
logger.error(
|
23
|
+
consumer.stop if consumer
|
24
|
+
logger.warn(LOGGER_ID) { 'Exited with error' }
|
25
|
+
logger.error(LOGGER_ID) { e }
|
26
|
+
raise
|
18
27
|
ensure
|
19
|
-
|
28
|
+
consumer_thread.join if consumer_thread
|
20
29
|
end
|
21
30
|
|
22
|
-
def initialize(
|
23
|
-
@
|
31
|
+
def initialize(consumer, logger=NullLogger.new)
|
32
|
+
@consumer = consumer
|
24
33
|
@logger = logger
|
25
34
|
@queue = Queue.new
|
26
35
|
end
|
27
36
|
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
37
|
+
# Buffer a frame to be played for the given duration. `#flush` must be called
|
38
|
+
# for frames to get pushed to the EventLoop.
|
39
|
+
#
|
40
|
+
# @param name [String] name of frame, which is passed to #build of the
|
41
|
+
# TemplateFactory
|
42
|
+
# @param duration [Float|Integer] duration in seconds the frame should be,
|
43
|
+
# displayed (default is nil, which results in a duration of a single refresh
|
44
|
+
# cycle)
|
45
|
+
# @param args [Hash] key/value pair of arguments, which is passed to #build of
|
46
|
+
# the TemplateFactory
|
47
|
+
def send_frame(name, duration: nil, args: {})
|
34
48
|
queue.push([name, duration, args])
|
35
49
|
end
|
36
50
|
|
51
|
+
# Send any buffered frames to the EventLoop
|
37
52
|
def flush
|
38
|
-
while !queue.empty?
|
39
|
-
name, duration, args = queue.pop
|
40
|
-
send(name, duration: duration, args: args)
|
41
|
-
end
|
42
|
-
end
|
43
|
-
|
44
|
-
def stop
|
45
|
-
send(Frame::EOF)
|
46
|
-
end
|
47
|
-
|
48
|
-
def terminate
|
49
|
-
send(Frame::TERMINATE)
|
53
|
+
consumer.enqueue(*queue.pop) while !queue.empty?
|
50
54
|
end
|
51
55
|
|
52
56
|
private
|
53
57
|
|
54
|
-
attr_reader :
|
58
|
+
attr_reader :consumer, :logger, :queue
|
55
59
|
end
|
56
60
|
end
|
57
61
|
end
|
@@ -0,0 +1,230 @@
|
|
1
|
+
require_relative '../utils/title_font'
|
2
|
+
|
3
|
+
require_relative 'settings'
|
4
|
+
|
5
|
+
module WhirledPeas
|
6
|
+
module UI
|
7
|
+
STRINGALBE_CLASSES = [FalseClass, Float, Integer, NilClass, String, Symbol, TrueClass]
|
8
|
+
|
9
|
+
class Element
|
10
|
+
attr_accessor :preferred_width, :preferred_height
|
11
|
+
attr_reader :name, :settings
|
12
|
+
|
13
|
+
def initialize(name, settings)
|
14
|
+
@name = name
|
15
|
+
@settings = settings
|
16
|
+
end
|
17
|
+
end
|
18
|
+
private_constant :Element
|
19
|
+
|
20
|
+
class TextElement < Element
|
21
|
+
attr_reader :lines
|
22
|
+
|
23
|
+
def initialize(name, settings)
|
24
|
+
super(name, TextSettings.merge(settings))
|
25
|
+
end
|
26
|
+
|
27
|
+
def lines=(val)
|
28
|
+
unless STRINGALBE_CLASSES.include?(val.class)
|
29
|
+
raise ArgmentError, "Unsupported type for TextElement: #{val.class}"
|
30
|
+
end
|
31
|
+
if settings.title_font
|
32
|
+
@lines = Utils::TitleFont.to_s(val.to_s, settings.title_font).split("\n")
|
33
|
+
else
|
34
|
+
@lines = [val.to_s]
|
35
|
+
end
|
36
|
+
@preferred_width = settings.width || @lines.first.length
|
37
|
+
@preferred_height = @lines.length
|
38
|
+
end
|
39
|
+
|
40
|
+
def inspect(indent='')
|
41
|
+
dims = unless preferred_width.nil?
|
42
|
+
"#{indent + ' '}- Dimensions: #{preferred_width}x#{preferred_height}"
|
43
|
+
end
|
44
|
+
[
|
45
|
+
"#{indent}+ #{name} [#{self.class.name}]",
|
46
|
+
dims,
|
47
|
+
"#{indent + ' '}- Settings",
|
48
|
+
settings.inspect(indent + ' ')
|
49
|
+
].compact.join("\n")
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
class ComposableElement < Element
|
54
|
+
class << self
|
55
|
+
def next_name
|
56
|
+
@counter ||= 0
|
57
|
+
@counter += 1
|
58
|
+
"Element-#{@counter}"
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def initialize(name, settings)
|
63
|
+
super
|
64
|
+
end
|
65
|
+
|
66
|
+
def children
|
67
|
+
@children ||= []
|
68
|
+
end
|
69
|
+
|
70
|
+
def add_text(name=self.class.next_name, &block)
|
71
|
+
element = TextElement.new(name, settings)
|
72
|
+
element.lines = yield nil, element.settings
|
73
|
+
children << element
|
74
|
+
end
|
75
|
+
|
76
|
+
def add_box(name=self.class.next_name, &block)
|
77
|
+
element = BoxElement.new(name, settings)
|
78
|
+
value = yield element, element.settings
|
79
|
+
children << element
|
80
|
+
if element.children.empty? && STRINGALBE_CLASSES.include?(value.class)
|
81
|
+
element.add_text { value.to_s }
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def add_grid(name=self.class.next_name, &block)
|
86
|
+
element = GridElement.new(name, settings)
|
87
|
+
values = yield element, element.settings
|
88
|
+
children << element
|
89
|
+
if element.children.empty? && values.is_a?(Array)
|
90
|
+
values.each { |v| element.add_text { v.to_s } }
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def inspect(indent='')
|
95
|
+
kids = children.map { |c| c.inspect(indent + ' ') }.join("\n")
|
96
|
+
dims = unless preferred_width.nil?
|
97
|
+
"#{indent + ' '}- Dimensions: #{preferred_width}x#{preferred_height}"
|
98
|
+
end
|
99
|
+
[
|
100
|
+
"#{indent}+ #{name} [#{self.class.name}]",
|
101
|
+
dims,
|
102
|
+
"#{indent + ' '}- Settings",
|
103
|
+
settings.inspect(indent + ' '),
|
104
|
+
"#{indent + ' '}- Children",
|
105
|
+
kids
|
106
|
+
].compact.join("\n")
|
107
|
+
end
|
108
|
+
|
109
|
+
private
|
110
|
+
|
111
|
+
def margin_width
|
112
|
+
settings.margin.left + settings.margin.right
|
113
|
+
end
|
114
|
+
|
115
|
+
def margin_height
|
116
|
+
settings.margin.top + settings.margin.bottom
|
117
|
+
end
|
118
|
+
|
119
|
+
def outer_border_width
|
120
|
+
(settings.border.left? ? 1 : 0) + (settings.border.right? ? 1 : 0)
|
121
|
+
end
|
122
|
+
|
123
|
+
def outer_border_height
|
124
|
+
(settings.border.top? ? 1 : 0) + (settings.border.bottom? ? 1 : 0)
|
125
|
+
end
|
126
|
+
|
127
|
+
def inner_border_width
|
128
|
+
settings.border.inner_vert? ? 1 : 0
|
129
|
+
end
|
130
|
+
|
131
|
+
def inner_border_height
|
132
|
+
settings.border.inner_horiz? ? 1 : 0
|
133
|
+
end
|
134
|
+
|
135
|
+
def padding_width
|
136
|
+
settings.padding.left + settings.padding.right
|
137
|
+
end
|
138
|
+
|
139
|
+
def padding_height
|
140
|
+
settings.padding.top + settings.padding.bottom
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
class Template < ComposableElement
|
145
|
+
def initialize(settings=TemplateSettings.new)
|
146
|
+
super('TEMPLATE', settings)
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
class BoxElement < ComposableElement
|
151
|
+
attr_writer :content_width, :content_height
|
152
|
+
|
153
|
+
def initialize(name, settings)
|
154
|
+
super(name, BoxSettings.merge(settings))
|
155
|
+
end
|
156
|
+
|
157
|
+
def self.from_template(template, width, height)
|
158
|
+
box = new(template.name, template.settings)
|
159
|
+
template.children.each { |c| box.children << c }
|
160
|
+
box.content_width = box.preferred_width = width
|
161
|
+
box.content_height = box.preferred_height = height
|
162
|
+
box
|
163
|
+
end
|
164
|
+
|
165
|
+
def content_width
|
166
|
+
@content_width ||= begin
|
167
|
+
child_widths = children.map(&:preferred_width)
|
168
|
+
width = settings.horizontal_flow? ? child_widths.sum : (child_widths.max || 0)
|
169
|
+
[width, *settings.width].max
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
def preferred_width
|
174
|
+
@preferred_width ||=
|
175
|
+
margin_width + outer_border_width + padding_width + content_width
|
176
|
+
end
|
177
|
+
|
178
|
+
def content_height
|
179
|
+
@content_height ||= begin
|
180
|
+
child_heights = children.map(&:preferred_height)
|
181
|
+
settings.vertical_flow? ? child_heights.sum : (child_heights.max || 0)
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
def preferred_height
|
186
|
+
@preferred_height ||=
|
187
|
+
margin_height + outer_border_height + padding_height + content_height
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
class GridElement < ComposableElement
|
192
|
+
def initialize(name, settings)
|
193
|
+
super(name, GridSettings.merge(settings))
|
194
|
+
end
|
195
|
+
|
196
|
+
def col_width
|
197
|
+
return @col_width if @col_width
|
198
|
+
@col_width = 0
|
199
|
+
children.each do |child|
|
200
|
+
@col_width = child.preferred_width if child.preferred_width > @col_width
|
201
|
+
end
|
202
|
+
@col_width
|
203
|
+
end
|
204
|
+
|
205
|
+
def row_height
|
206
|
+
return @row_height if @row_height
|
207
|
+
@row_height = 0
|
208
|
+
children.each do |child|
|
209
|
+
@row_height = child.preferred_height if child.preferred_height > @row_height
|
210
|
+
end
|
211
|
+
@row_height
|
212
|
+
end
|
213
|
+
|
214
|
+
def preferred_width
|
215
|
+
margin_width +
|
216
|
+
outer_border_width +
|
217
|
+
settings.num_cols * (padding_width + col_width) +
|
218
|
+
(settings.num_cols - 1) * inner_border_width
|
219
|
+
end
|
220
|
+
|
221
|
+
def preferred_height
|
222
|
+
num_rows = (children.length / settings.num_cols).ceil
|
223
|
+
margin_height +
|
224
|
+
outer_border_height +
|
225
|
+
num_rows * (padding_height + row_height) +
|
226
|
+
(num_rows - 1) * inner_border_height
|
227
|
+
end
|
228
|
+
end
|
229
|
+
end
|
230
|
+
end
|
@@ -1,5 +1,8 @@
|
|
1
1
|
require 'json'
|
2
2
|
|
3
|
+
require_relative '../utils/color'
|
4
|
+
require_relative '../utils/title_font'
|
5
|
+
|
3
6
|
module WhirledPeas
|
4
7
|
module UI
|
5
8
|
module TextAlign
|
@@ -305,11 +308,11 @@ module WhirledPeas
|
|
305
308
|
end
|
306
309
|
private_constant :ElementSettings
|
307
310
|
|
308
|
-
module
|
311
|
+
module WidthSettings
|
309
312
|
attr_accessor :width
|
310
313
|
end
|
311
314
|
|
312
|
-
module
|
315
|
+
module AlignSettings
|
313
316
|
def align
|
314
317
|
@_align || TextAlign::LEFT
|
315
318
|
end
|
@@ -322,7 +325,7 @@ module WhirledPeas
|
|
322
325
|
merged = super
|
323
326
|
merged._align = if @_align
|
324
327
|
@_align
|
325
|
-
elsif parent.is_a?(
|
328
|
+
elsif parent.is_a?(AlignSettings)
|
326
329
|
parent._align
|
327
330
|
end
|
328
331
|
merged
|
@@ -342,6 +345,11 @@ module WhirledPeas
|
|
342
345
|
@_margin.bottom = bottom if bottom
|
343
346
|
end
|
344
347
|
|
348
|
+
def clear_margin
|
349
|
+
set_margin(left: 0, top: 0, right: 0, bottom: 0)
|
350
|
+
@_auto_margin = nil
|
351
|
+
end
|
352
|
+
|
345
353
|
def margin
|
346
354
|
@_margin || Margin.new
|
347
355
|
end
|
@@ -387,7 +395,7 @@ module WhirledPeas
|
|
387
395
|
@_border.color = color unless color.nil?
|
388
396
|
end
|
389
397
|
|
390
|
-
def
|
398
|
+
def clear_border
|
391
399
|
set_border(
|
392
400
|
left: false, top: false, right: false, bottom: false, inner_horiz: false, inner_vert: false
|
393
401
|
)
|
@@ -424,6 +432,10 @@ module WhirledPeas
|
|
424
432
|
@_padding.bottom = bottom if bottom
|
425
433
|
end
|
426
434
|
|
435
|
+
def clear_padding
|
436
|
+
set_padding(left: 0, top: 0, right: 0, bottom: 0)
|
437
|
+
end
|
438
|
+
|
427
439
|
def padding
|
428
440
|
@_padding || Padding.new
|
429
441
|
end
|
@@ -444,8 +456,14 @@ module WhirledPeas
|
|
444
456
|
end
|
445
457
|
|
446
458
|
class TextSettings < ElementSettings
|
447
|
-
include
|
448
|
-
include
|
459
|
+
include WidthSettings
|
460
|
+
include AlignSettings
|
461
|
+
|
462
|
+
attr_reader :title_font
|
463
|
+
|
464
|
+
def title_font=(font)
|
465
|
+
@title_font = Utils::TitleFont.validate!(font)
|
466
|
+
end
|
449
467
|
end
|
450
468
|
|
451
469
|
class ContainerSettings < ElementSettings
|
@@ -455,8 +473,8 @@ module WhirledPeas
|
|
455
473
|
end
|
456
474
|
|
457
475
|
class BoxSettings < ContainerSettings
|
458
|
-
include
|
459
|
-
include
|
476
|
+
include WidthSettings
|
477
|
+
include AlignSettings
|
460
478
|
|
461
479
|
def flow=(flow)
|
462
480
|
@_flow = DisplayFlow.validate!(flow)
|
@@ -498,8 +516,8 @@ module WhirledPeas
|
|
498
516
|
end
|
499
517
|
|
500
518
|
class GridSettings < ContainerSettings
|
501
|
-
include
|
502
|
-
include
|
519
|
+
include WidthSettings
|
520
|
+
include AlignSettings
|
503
521
|
|
504
522
|
attr_accessor :num_cols
|
505
523
|
attr_writer :transpose
|