whirled_peas 0.1.0 → 0.4.1

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