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
data/lib/whirled_peas.rb
CHANGED
@@ -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
|
-
|
9
|
-
|
10
|
-
|
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
|
-
|
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
|
data/lib/whirled_peas/frame.rb
CHANGED
@@ -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
|