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.
@@ -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
- def self.start(logger: NullLogger.new, host:, port:, &block)
8
- server = TCPServer.new(host, port)
9
- client = server.accept
10
- logger.info('PRODUCER') { "Connected to #{host}:#{port}" }
11
- producer = new(client, logger)
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
- logger.info('PRODUCER') { "Exited normally" }
20
+ producer.send_frame(Frame::EOF)
21
+ producer.flush
14
22
  rescue => e
15
- logger.warn('PRODUCER') { "Exited with error" }
16
- logger.error('PRODUCER') { e.message }
17
- logger.error('PRODUCER') { e.backtrace.join("\n") }
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
- client.close if client
28
+ consumer_thread.join if consumer_thread
20
29
  end
21
30
 
22
- def initialize(client, logger=NullLogger.new)
23
- @client = client
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
- def send(name, duration: nil, args: {})
29
- client.puts(JSON.generate('name' => name, 'duration' => duration, **args))
30
- logger.debug('PRODUCER') { "Sending frame: #{name}" }
31
- end
32
-
33
- def enqueue(name, duration: nil, args: {})
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 :client, :logger, :queue
58
+ attr_reader :consumer, :logger, :queue
55
59
  end
56
60
  end
57
61
  end
@@ -0,0 +1,5 @@
1
+ module WhirledPeas
2
+ module Template
3
+ end
4
+ private_constant :Template
5
+ 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 WidthSetting
311
+ module WidthSettings
309
312
  attr_accessor :width
310
313
  end
311
314
 
312
- module AlignSetting
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?(AlignSetting)
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 no_border
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 WidthSetting
448
- include AlignSetting
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 WidthSetting
459
- include AlignSetting
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 WidthSetting
502
- include AlignSetting
519
+ include WidthSettings
520
+ include AlignSettings
503
521
 
504
522
  attr_accessor :num_cols
505
523
  attr_writer :transpose