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.
@@ -1,44 +1,26 @@
1
1
  require 'logger'
2
2
 
3
+ require 'whirled_peas/errors'
4
+
5
+ require 'whirled_peas/config'
3
6
  require 'whirled_peas/frame'
7
+ require 'whirled_peas/template'
4
8
  require 'whirled_peas/ui'
9
+ require 'whirled_peas/utils'
5
10
  require 'whirled_peas/version'
6
11
 
7
12
  module WhirledPeas
8
- class Error < StandardError; end
9
-
10
- DEFAULT_HOST = 'localhost'
11
- DEFAULT_PORT = 8765
12
- DEFAULT_REFRESH_RATE = 30
13
-
14
-
15
- def self.start(driver, template_factory, log_level: Logger::INFO, refresh_rate: DEFAULT_REFRESH_RATE, host: DEFAULT_HOST, port: DEFAULT_PORT)
16
- logger = Logger.new(File.open('whirled_peas.log', 'a'))
17
- logger.level = log_level
18
- logger.formatter = proc do |severity, datetime, progname, msg|
19
- "[#{severity}] #{datetime.strftime('%Y-%m-%dT%H:%M:%S.%L')} (#{progname}) - #{msg}\n"
20
- end
21
-
22
- consumer = Frame::Consumer.new(template_factory, refresh_rate, logger)
23
- consumer_thread = Thread.new { consumer.start(host: host, port: port) }
24
-
25
- Frame::Producer.start(logger: logger, host: host, port: port) do |producer|
26
- begin
27
- driver.start(producer)
28
- producer.stop
29
- rescue => e
30
- logger.warn('MAIN') { "Driver exited with error, terminating producer..." }
31
- logger.error('MAIN') { e }
32
- logger.error('MAIN') { e.backtrace.join("\n") }
33
- producer.terminate
34
- raise
35
- end
36
- end
13
+ def self.config
14
+ @config ||= Config.new
15
+ end
37
16
 
38
- consumer_thread.join
17
+ def self.configure(&block)
18
+ yield config
39
19
  end
40
20
 
41
21
  def self.template(&block)
22
+ require 'whirled_peas/template/element'
23
+
42
24
  template = UI::Template.new
43
25
  yield template, template.settings
44
26
  template
@@ -0,0 +1,270 @@
1
+ module WhirledPeas
2
+ class Command
3
+ DEFAULT_REFRESH_RATE = 30
4
+
5
+ DEFAULT_LOG_LEVEL = Logger::INFO
6
+ DEFAULT_FORMATTER = proc do |severity, datetime, progname, msg|
7
+ if msg.is_a?(Exception)
8
+ msg = %Q(#{msg.class}: #{msg.to_s}\n #{msg.backtrace.join("\n ")})
9
+ end
10
+ "[#{severity}] #{datetime.strftime('%Y-%m-%dT%H:%M:%S.%L')} (#{progname}) - #{msg}\n"
11
+ end
12
+
13
+ def self.command_name
14
+ self.name.split('::').last.sub(/Command$/, '').gsub(/([a-z])([A-Z])/, '\1_\2').downcase
15
+ end
16
+
17
+ def self.build_logger(output, level=DEFAULT_LOG_LEVEL, formatter=DEFAULT_FORMATTER)
18
+ logger = Logger.new(output)
19
+ logger.level = level
20
+ logger.formatter = formatter
21
+ logger
22
+ end
23
+
24
+ attr_reader :args
25
+
26
+ def initialize(args)
27
+ @args = args
28
+ end
29
+
30
+ def valid?
31
+ @error_text = nil
32
+ validate!
33
+ @error_text.nil?
34
+ end
35
+
36
+ def print_error
37
+ puts @error_text if @error_text
38
+ print_usage
39
+ end
40
+
41
+ def start
42
+ end
43
+
44
+ private
45
+
46
+ def print_usage
47
+ puts "Usage: #{$0} #{self.class.command_name}"
48
+ end
49
+
50
+ def validate!
51
+ # Set @error_text if the options are not valid
52
+ end
53
+ end
54
+
55
+ class TitleFontsCommand < Command
56
+ def start
57
+ require 'whirled_peas/utils/title_font'
58
+
59
+ Utils::TitleFont.fonts.keys.each do |key|
60
+ puts Utils::TitleFont.to_s(key.to_s, key)
61
+ puts key.inspect
62
+ puts
63
+ end
64
+ end
65
+ end
66
+
67
+ class ConfigCommand < Command
68
+ def start
69
+ require config
70
+ end
71
+
72
+ private
73
+
74
+ attr_reader :config
75
+
76
+ def validate!
77
+ if args.length == 0
78
+ @error_text = "#{self.class.command_name} requires a config file"
79
+ elsif !File.exist?(args[0])
80
+ @error_text = "File not found: #{args[0]}"
81
+ elsif args[0][-3..-1] != '.rb'
82
+ @error_text = 'Config file should be a .rb file'
83
+ else
84
+ @config = args[0][0] == '/' ? args[0] : File.join(Dir.pwd, args[0])
85
+ end
86
+ end
87
+
88
+ def print_usage
89
+ puts "Usage: #{$0} #{self.class.command_name} <config file>"
90
+ end
91
+ end
92
+
93
+ class StartCommand < ConfigCommand
94
+ LOGGER_ID = 'MAIN'
95
+
96
+ def start
97
+ super
98
+ require 'whirled_peas/frame/event_loop'
99
+ require 'whirled_peas/frame/producer'
100
+
101
+ logger = self.class.build_logger(File.open('whirled_peas.log', 'a'))
102
+
103
+ consumer = Frame::EventLoop.new(
104
+ WhirledPeas.config.template_factory,
105
+ WhirledPeas.config.loading_template_factory,
106
+ DEFAULT_REFRESH_RATE,
107
+ logger
108
+ )
109
+ Frame::Producer.produce(consumer, logger) do |producer|
110
+ begin
111
+ WhirledPeas.config.driver.start(producer)
112
+ rescue => e
113
+ logger.warn(LOGGER_ID) { 'Driver exited with error, terminating producer...' }
114
+ logger.error(LOGGER_ID) { e }
115
+ raise
116
+ end
117
+ end
118
+ end
119
+ end
120
+
121
+ class ListFramesCommand < ConfigCommand
122
+ def start
123
+ super
124
+ require 'whirled_peas/frame/print_consumer'
125
+ require 'whirled_peas/frame/producer'
126
+
127
+ logger = self.class.build_logger(STDOUT)
128
+ Frame::Producer.produce(Frame::PrintConsumer.new, logger) do |producer|
129
+ begin
130
+ WhirledPeas.config.driver.start(producer)
131
+ rescue => e
132
+ logger.warn(LOGGER_ID) { 'Driver exited with error, terminating producer...' }
133
+ logger.error(LOGGER_ID) { e }
134
+ raise
135
+ end
136
+ end
137
+ end
138
+ end
139
+
140
+ class PlayFrameCommand < ConfigCommand
141
+ def start
142
+ super
143
+
144
+ if args.last == '--debug'
145
+ puts WhirledPeas.config.template_factory.build(frame, frame_args).inspect
146
+ exit
147
+ end
148
+
149
+ require 'whirled_peas/frame/event_loop'
150
+ require 'whirled_peas/frame/producer'
151
+
152
+ logger = self.class.build_logger(File.open('whirled_peas.log', 'a'))
153
+
154
+ consumer = Frame::EventLoop.new(
155
+ WhirledPeas.config.template_factory,
156
+ WhirledPeas.config.loading_template_factory,
157
+ DEFAULT_REFRESH_RATE,
158
+ logger
159
+ )
160
+ Frame::Producer.produce(consumer, logger) do |producer|
161
+ producer.send_frame(args[1], duration: 5, args: frame_args)
162
+ end
163
+ end
164
+
165
+ private
166
+
167
+ attr_reader :frame, :frame_args
168
+
169
+ def validate!
170
+ super
171
+ if !@error_text.nil?
172
+ return
173
+ elsif args.length < 2
174
+ @error_text = "#{self.class.command_name} requires a frame name"
175
+ else
176
+ require 'json'
177
+ @frame = args[1]
178
+ @frame_args = {}
179
+ JSON.parse(args[2] || '{}').each do |key, value|
180
+ @frame_args[key.to_sym] = value
181
+ end
182
+ end
183
+ end
184
+
185
+ def print_usage
186
+ puts "Usage: #{$0} #{self.class.command_name} <config file> <frame> [args as a JSON string] [--debug]"
187
+ end
188
+ end
189
+
190
+ class LoadingCommand < ConfigCommand
191
+ def start
192
+ super
193
+ unless WhirledPeas.config.loading_template_factory
194
+ puts 'No loading screen configured'
195
+ exit
196
+ end
197
+
198
+ if args.last == '--debug'
199
+ puts WhirledPeas.config.loading_template_factory.build.inspect
200
+ exit
201
+ end
202
+
203
+ require 'whirled_peas/frame/event_loop'
204
+ require 'whirled_peas/frame/producer'
205
+
206
+ logger = self.class.build_logger(File.open('whirled_peas.log', 'a'))
207
+ consumer = Frame::EventLoop.new(
208
+ WhirledPeas.config.template_factory,
209
+ WhirledPeas.config.loading_template_factory,
210
+ DEFAULT_REFRESH_RATE,
211
+ logger
212
+ )
213
+ Frame::Producer.produce(consumer, logger) { sleep(5) }
214
+ end
215
+
216
+ private
217
+
218
+ def print_usage
219
+ puts "Usage: #{$0} #{self.class.command_name} [--debug]"
220
+ end
221
+ end
222
+
223
+ class CommandLine
224
+ COMMANDS = [
225
+ StartCommand,
226
+ ListFramesCommand,
227
+ PlayFrameCommand,
228
+ LoadingCommand,
229
+ TitleFontsCommand
230
+ ].map.with_object({}) { |c, h| h[c.command_name] = c }
231
+
232
+ def initialize(args)
233
+ @args = args
234
+ end
235
+
236
+ def start
237
+ if args.length < 1
238
+ print_usage
239
+ exit(1)
240
+ end
241
+
242
+ command = args.shift
243
+
244
+ unless COMMANDS.key?(command)
245
+ puts "Unrecognized command: #{command}"
246
+ print_usage
247
+ exit(1)
248
+ end
249
+
250
+ cmd = COMMANDS[command].new(args)
251
+
252
+ unless cmd.valid?
253
+ cmd.print_error
254
+ exit(1)
255
+ end
256
+
257
+ cmd.start
258
+ end
259
+
260
+ private
261
+
262
+ attr_reader :args
263
+
264
+ def print_usage
265
+ puts "Usage: #{$0} <command> [command options]"
266
+ puts
267
+ puts "Available commands: #{COMMANDS.keys.join(', ')}"
268
+ end
269
+ end
270
+ end
@@ -0,0 +1,21 @@
1
+ module WhirledPeas
2
+ class Config
3
+ attr_writer :driver, :template_factory
4
+ attr_accessor :loading_template_factory
5
+
6
+ def driver
7
+ unless @driver
8
+ raise ConfigurationError, 'driver must be configured'
9
+ end
10
+ @driver
11
+ end
12
+
13
+ def template_factory
14
+ unless @template_factory
15
+ raise ConfigurationError, 'template_factory must be configured'
16
+ end
17
+ @template_factory
18
+ end
19
+ end
20
+ private_constant :Config
21
+ end
@@ -0,0 +1,5 @@
1
+ module WhirledPeas
2
+ class Error < StandardError; end
3
+
4
+ class ConfigurationError < Error; end
5
+ end
@@ -1,13 +1,6 @@
1
- require_relative 'frame/consumer'
2
- require_relative 'frame/producer'
3
-
4
1
  module WhirledPeas
5
2
  module Frame
6
- TERMINATE = '__term__'
7
3
  EOF = '__EOF__'
8
-
9
- DEFAULT_ADDRESS = 'localhost'
10
- DEFAULT_PORT = 8765
11
4
  end
12
5
 
13
6
  private_constant :Frame
@@ -0,0 +1,91 @@
1
+ require_relative '../null_logger'
2
+ require_relative '../ui/screen'
3
+
4
+ module WhirledPeas
5
+ module Frame
6
+ class EventLoop
7
+ LOGGER_ID = 'EVENT LOOP'
8
+
9
+ def initialize(template_factory, loading_template_factory, refresh_rate, logger=NullLogger.new)
10
+ @template_factory = template_factory
11
+ @loading_template_factory = loading_template_factory
12
+ @queue = Queue.new
13
+ @frame_duration = 1.0 / refresh_rate
14
+ @logger = logger
15
+ end
16
+
17
+ def enqueue(name, duration, args)
18
+ # If duration is nil, set it to the duration of a single frame
19
+ queue.push([name, duration || frame_duration, args])
20
+ end
21
+
22
+ def running?
23
+ @running
24
+ end
25
+
26
+ def start(screen=UI::Screen.new)
27
+ wait_for_content(screen)
28
+ play_content(screen)
29
+ rescue
30
+ logger.warn(LOGGER_ID) { 'Exiting with error' }
31
+ raise
32
+ ensure
33
+ # We may have exited due to an EOF or a raised exception, set state so that
34
+ # instance reflects actual state.
35
+ @running = false
36
+ screen.finalize if screen
37
+ end
38
+
39
+ def stop
40
+ logger.info(LOGGER_ID) { 'Stopping...' }
41
+ enqueue(Frame::EOF, nil, {})
42
+ @running = false
43
+ end
44
+
45
+ private
46
+
47
+ attr_reader :template_factory, :loading_template_factory, :queue, :frame_duration, :logger
48
+
49
+ def wait_for_content(screen)
50
+ if loading_template_factory
51
+ play_loading_screen(screen)
52
+ else
53
+ sleep(frame_duration) while queue.empty?
54
+ end
55
+ end
56
+
57
+ def play_loading_screen(screen)
58
+ while queue.empty?
59
+ screen.paint(loading_template_factory.build)
60
+ sleep(frame_duration)
61
+ end
62
+ end
63
+
64
+ def play_content(screen)
65
+ @running = true
66
+ template = nil
67
+ frame_until = Time.new(0) # Tell the loop to immediately pick up a new frame
68
+ while running?
69
+ frame_start = Time.now
70
+ next_frame_at = frame_start + frame_duration
71
+ if frame_until > frame_start
72
+ # While we're still displaying the previous frame, refresh the screen
73
+ screen.refresh
74
+ elsif !queue.empty?
75
+ name, duration, args = queue.pop
76
+ if name == Frame::EOF
77
+ @running = false
78
+ else
79
+ frame_until = frame_start + duration
80
+ template = template_factory.build(name, args)
81
+ screen.paint(template)
82
+ end
83
+ else
84
+ wait_for_content(screen)
85
+ end
86
+ sleep([0, next_frame_at - Time.now].max)
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end