whirled_peas 0.3.0 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d3cb7aa7af0bf74f8818eb5796f310e3b65a73851b7cbab35834e14fe0262ac1
4
- data.tar.gz: 6ae73d252d69d3730c1e1814d441800fca08177a4401abc3e4dcbe6edcd34548
3
+ metadata.gz: 740b5ae88bfdd0f6dfbf4b1ef9e220a4298ddd8f4d8b01d62e904e3bfb4e109b
4
+ data.tar.gz: 1e78f0bf1664187825441258909a750c74455017266d01b4b64d5cdd7c609572
5
5
  SHA512:
6
- metadata.gz: 2b0965214a99209e4830ed68da162cc83d249188e197d8b9fe11b61356f797e2172a648aef2b23f83f9ac45db40d9f4e7daee9471f67069e61998e5f24a8d0ba
7
- data.tar.gz: e0d546f8a60606a918aadf1743848b9839703328bf14a05dcb67f680886ee2eef2c4ae542d89055daf822f74200f64a719e77f1223520266aa7d6703b18ccb03
6
+ metadata.gz: '0960008e3b09aae39cf0655ff57d6dde8859bd3437844b68b37fd72b49bc04199dae2fa5544ba219f795d87f5b5ada80d56f0e96f005eaffddbd74de67d4037c'
7
+ data.tar.gz: ce998b69ac1db491577cdfaaaa4b35fb79a0bd07d24c622f426e38b1ff3eb0851ff10c90d24b8b7f48dc1b97dd01c11f93960a011c2611df201f3d86c45ce65a
@@ -1,5 +1,9 @@
1
1
  # Changelog
2
2
 
3
+ ## v0.3.0 - 2021-01-21
4
+
5
+ - [617f802](https://github.com/tcollier/whirled_peas/tree/617f8027d6688a2ec81a3e594e529c94485cee85): BREAKING: send frames directly to EventLoop (`Producer#send` renamed to `Producer#send_frame`)
6
+
3
7
  ## v0.2.0 - 2021-01-20
4
8
 
5
9
  - [73eb326](https://github.com/tcollier/whirled_peas/tree/73eb326426f9814e91e3bc7a60dfd87be3d69f7e): Convert "primitive" data types to strings
data/README.md CHANGED
@@ -22,13 +22,22 @@ Or install it yourself as:
22
22
 
23
23
  ## Usage
24
24
 
25
+ A Whirled Peas application consists of the following pieces
26
+
27
+ 1. [REQUIRED] The driver, which emits lightweight frame events
28
+ 1. [REQUIRED] The main template factory, which builds templates to convert frame events from the driver into terminal graphics
29
+ 1. [OPTIONAL] A loading screen template factory, which is used while content is loading
30
+
31
+ These pieces are configured as following
32
+
25
33
  ```ruby
34
+ # visualize.rb
26
35
  require 'whirled_peas'
27
36
 
28
37
  class TemplateFactory
29
38
  def build(frame, args)
30
39
  WhirledPeas.template do |body|
31
- body.add_box do |_, settings|
40
+ body.add_box('Title') do |_, settings|
32
41
  settings.underline = true
33
42
  "Hello #{args['name']}"
34
43
  end
@@ -44,13 +53,39 @@ class Driver
44
53
  end
45
54
  end
46
55
 
47
- WhirledPeas.start(Driver.new, TemplateFactory.new)
56
+ WhirledPeas.configure do |config|
57
+ config.driver = Driver.new
58
+ config.template_factory = TemplateFactory.new
59
+ end
60
+ ```
61
+
62
+ Then the visualizer is started on the command line with
63
+
64
+ ```
65
+ $ whirled_peas start visualize.rb
48
66
  ```
49
67
 
50
- A Whirled Peas application consists of two pieces
68
+ The optional loading screen can be configured like
51
69
 
52
- 1. The driver, which emits lightweight frame events
53
- 1. The template factory, which builds templates to convert frame events from the driver into terminal graphics
70
+ ````ruby
71
+ class LoadingTemplateFactory
72
+ def build
73
+ WhirledPeas.template do |t|
74
+ t.add_box('Loading') do |box, settings|
75
+ settings.set_margin(top: 15)
76
+ settings.auto_margin = true
77
+ settings.full_border(color: :blue, style: :double)
78
+ "Loading..."
79
+ end
80
+ end
81
+ end
82
+ end
83
+
84
+ WhirledPeas.configure do |config|
85
+ # ...
86
+ config.loading_template_factory = LoadingTemplateFactory.new
87
+ end
88
+ ```
54
89
 
55
90
  ### Driver
56
91
 
@@ -63,7 +98,7 @@ The driver is the application code to be visualized. This is typically a lightwe
63
98
  def start(producer)
64
99
  # application code here
65
100
  end
66
- ```
101
+ ````
67
102
 
68
103
  The producer provides a single method
69
104
 
@@ -113,6 +148,10 @@ end
113
148
 
114
149
  To render the frame events sent by the driver, the application requires a template factory. This factory will be called for each frame event, with the frame name and the arguments supplied by the driver. A template factory can be a simple ruby class and thus can maintain state. Whirled Peas provides a few basic building blocks to make simple, yet elegant terminal-based UIs.
115
150
 
151
+ #### Loading Template Factory
152
+
153
+ `WhirledPeas.start` takes an optional template facotry to build a loading screen. This instance must implement `#build` (taking no arguments). The template returned by that method will be painted while the event loop is waiting for frames. The factory method will be called once per refresh cycle, so it's possible to implement animation.
154
+
116
155
  #### Building Blocks
117
156
 
118
157
  A template is created with `WhirledPeas.template`, which yields a `Template` object and `TemplateSettings`. This template object is a `ComposableElement`, which allows for attaching child elements and setting layout options. `GridElement` and `BoxElement` are two other composable elements and `TextElement` is a simple element that can hold a text/number value and has layout options, but cannot have any child elements.
@@ -195,6 +234,7 @@ The available settigs are
195
234
  | `flow` | Flow to display child elements (see [Display Flow](#display-flow)) | `:l2r` | `Box` | Yes |
196
235
  | `margin` | Set the (left, top, right, bottom) margin of the element | `0` | `Box`, `Grid` | Yes |
197
236
  | `padding` | Set the (left, top, right, bottom) padding of the element | `0` | `Box`, `Grid` | Yes |
237
+ | `title_font` | Font used to create "large" text (see [Large Text](#large-text)) | | `Text` |
198
238
  | `transpose` | Display grid elements top-to-bottom, then left-to-right | `false` | `Grid` | No |
199
239
  | `underline` | `true` underlines the font | `false` | `Box`, `Grid`, `Template`, `Text` | Yes |
200
240
  | `width` | Override the calculated with of an element | | `Box`, `Grid`, `Text` | No |
@@ -304,6 +344,27 @@ Many of these also have a "bright" option:
304
344
  - `:bright_red`
305
345
  - `:bright_yellow`
306
346
 
347
+ ##### Large Text
348
+
349
+ The `title_font` setting for `TextElement`s converts the standard terminal font into a large block font. The available fonts vary from system to system. Every system will have a `:default` font available, this font could look like
350
+
351
+ ```
352
+ ██████╗ ███████╗███████╗ █████╗ ██╗ ██╗██╗ ████████╗
353
+ ██╔══██╗██╔════╝██╔════╝██╔══██╗██║ ██║██║ ╚══██╔══╝
354
+ ██║ ██║█████╗ █████╗ ███████║██║ ██║██║ ██║
355
+ ██║ ██║██╔══╝ ██╔══╝ ██╔══██║██║ ██║██║ ██║
356
+ ██████╔╝███████╗██║ ██║ ██║╚██████╔╝███████╗ ██║
357
+ ╚═════╝ ╚══════╝╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚══════╝ ╚═╝
358
+ ```
359
+
360
+ To print out a list of all available fonts as well as sample text in that font, run
361
+
362
+ ```
363
+ $ whirled_peas title_fonts
364
+ ```
365
+
366
+ Note: when using a title font with WhirledPeas for the first time on a system, the gem loads all fonts to check which ones are available. This can be a slow process and may cause a noticeable delay when running a visualization. Running the command above will cache the results and thus when a WhirledPeas visualization is run, there will be no lag from loading fonts.
367
+
307
368
  ### Example
308
369
 
309
370
  ```ruby
@@ -356,6 +417,64 @@ class TemplateFactory
356
417
  end
357
418
  ```
358
419
 
420
+ ### Debugging
421
+
422
+ The `whirled_peas` executable provides some commands that are helpful for debugging.
423
+
424
+ #### list_frames
425
+
426
+ List the frames sent by the driver
427
+
428
+ ```
429
+ $ whirled_peas <config file> list_frames
430
+ Frame 'start' displayed for 5 second(s)
431
+ Frame 'move' displayed for 1 frame ({:direction=>'N'})
432
+ ...
433
+ EOF frame detected
434
+ ```
435
+
436
+ #### play_frame
437
+
438
+ Displays a single frame for several seconds
439
+
440
+ ```
441
+ $ whirled_peas <config file> play_frame move '{"direction":"N"}'
442
+ ```
443
+
444
+ Adding the `--debug` flag will result in just printing out the template's debug information, e.g.
445
+
446
+ ```
447
+ $ whirled_peas <config file> play_frame move '{"direction":"N"}' --debug
448
+ + TEMPLATE [WhirledPeas::UI::Template]
449
+ - Settings
450
+ WhirledPeas::UI::TemplateSettings
451
+ <default>
452
+ - Children
453
+ + TitleContainer [WhirledPeas::UI::BoxElement]
454
+ ...
455
+ ```
456
+
457
+ #### loading
458
+
459
+ Displays the configured loading screen for several seconds
460
+
461
+ ```
462
+ $ whirled_peas <config file> loading
463
+ ```
464
+
465
+ Adding the `--debug` flag will result in just printing out the loading template's debug information, e.g.
466
+
467
+ ```
468
+ $ whirled_peas <config file> loading --debug
469
+ + TEMPLATE [WhirledPeas::UI::Template]
470
+ - Settings
471
+ WhirledPeas::UI::TemplateSettings
472
+ <default>
473
+ - Children
474
+ + TitleContainer [WhirledPeas::UI::BoxElement]
475
+ ...
476
+ ```
477
+
359
478
  ## Development
360
479
 
361
480
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'bundler/setup'
4
+ require 'whirled_peas'
5
+
6
+ WhirledPeas.print_title_fonts
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'bundler/setup'
4
+ require 'whirled_peas'
5
+ require 'whirled_peas/command_line'
6
+
7
+ WhirledPeas::CommandLine.new(ARGV).start
@@ -1,46 +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_REFRESH_RATE = 30
11
-
12
- LOGGER_ID = 'MAIN'
13
-
14
- def self.start(driver, template_factory, log_level: Logger::INFO, refresh_rate: DEFAULT_REFRESH_RATE)
15
- logger = Logger.new(File.open('whirled_peas.log', 'a'))
16
- logger.level = log_level
17
- logger.formatter = proc do |severity, datetime, progname, msg|
18
- if msg.is_a?(Exception)
19
- msg = %Q(#{msg.class}: #{msg.to_s}\n #{msg.backtrace.join("\n ")})
20
- end
21
- "[#{severity}] #{datetime.strftime('%Y-%m-%dT%H:%M:%S.%L')} (#{progname}) - #{msg}\n"
22
- end
23
-
24
- event_loop = Frame::EventLoop.new(template_factory, refresh_rate, logger)
25
- event_loop_thread = Thread.new do
26
- Thread.current.report_on_exception = false
27
- event_loop.start
28
- end
29
-
30
- Frame::Producer.produce(event_loop: event_loop, logger: logger) do |producer|
31
- begin
32
- driver.start(producer)
33
- rescue => e
34
- logger.warn(LOGGER_ID) { 'Driver exited with error, terminating producer...' }
35
- logger.error(LOGGER_ID) { e }
36
- raise
37
- end
38
- end
13
+ def self.config
14
+ @config ||= Config.new
15
+ end
39
16
 
40
- event_loop_thread.join
17
+ def self.configure(&block)
18
+ yield config
41
19
  end
42
20
 
43
21
  def self.template(&block)
22
+ require 'whirled_peas/template/element'
23
+
44
24
  template = UI::Template.new
45
25
  yield template, template.settings
46
26
  template
@@ -0,0 +1,266 @@
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 args[0]
70
+ end
71
+
72
+ private
73
+
74
+ def validate!
75
+ if args.length == 0
76
+ @error_text = "#{self.class.command_name} requires a config file"
77
+ elsif !File.exist?(args[0])
78
+ @error_text = "File not found: #{args[0]}"
79
+ elsif args[0][-3..-1] != '.rb'
80
+ @error_text = 'Config file should be a .rb file'
81
+ end
82
+ end
83
+
84
+ def print_usage
85
+ puts "Usage: #{$0} #{self.class.command_name} <config file>"
86
+ end
87
+ end
88
+
89
+ class StartCommand < ConfigCommand
90
+ LOGGER_ID = 'MAIN'
91
+
92
+ def start
93
+ super
94
+ require 'whirled_peas/frame/event_loop'
95
+ require 'whirled_peas/frame/producer'
96
+
97
+ logger = self.class.build_logger(File.open('whirled_peas.log', 'a'))
98
+
99
+ consumer = Frame::EventLoop.new(
100
+ WhirledPeas.config.template_factory,
101
+ WhirledPeas.config.loading_template_factory,
102
+ DEFAULT_REFRESH_RATE,
103
+ logger
104
+ )
105
+ Frame::Producer.produce(consumer, logger) do |producer|
106
+ begin
107
+ WhirledPeas.config.driver.start(producer)
108
+ rescue => e
109
+ logger.warn(LOGGER_ID) { 'Driver exited with error, terminating producer...' }
110
+ logger.error(LOGGER_ID) { e }
111
+ raise
112
+ end
113
+ end
114
+ end
115
+ end
116
+
117
+ class ListFramesCommand < ConfigCommand
118
+ def start
119
+ super
120
+ require 'whirled_peas/frame/print_consumer'
121
+ require 'whirled_peas/frame/producer'
122
+
123
+ logger = self.class.build_logger(STDOUT)
124
+ Frame::Producer.produce(Frame::PrintConsumer.new, logger) do |producer|
125
+ begin
126
+ WhirledPeas.config.driver.start(producer)
127
+ rescue => e
128
+ logger.warn(LOGGER_ID) { 'Driver exited with error, terminating producer...' }
129
+ logger.error(LOGGER_ID) { e }
130
+ raise
131
+ end
132
+ end
133
+ end
134
+ end
135
+
136
+ class PlayFrameCommand < ConfigCommand
137
+ def start
138
+ super
139
+
140
+ if args.last == '--debug'
141
+ puts WhirledPeas.config.template_factory.build(frame, frame_args).inspect
142
+ exit
143
+ end
144
+
145
+ require 'whirled_peas/frame/event_loop'
146
+ require 'whirled_peas/frame/producer'
147
+
148
+ logger = self.class.build_logger(File.open('whirled_peas.log', 'a'))
149
+
150
+ consumer = Frame::EventLoop.new(
151
+ WhirledPeas.config.template_factory,
152
+ WhirledPeas.config.loading_template_factory,
153
+ DEFAULT_REFRESH_RATE,
154
+ logger
155
+ )
156
+ Frame::Producer.produce(consumer, logger) do |producer|
157
+ producer.send_frame(args[1], duration: 5, args: frame_args)
158
+ end
159
+ end
160
+
161
+ private
162
+
163
+ attr_reader :frame, :frame_args
164
+
165
+ def validate!
166
+ super
167
+ if !@error_text.nil?
168
+ return
169
+ elsif args.length < 2
170
+ @error_text = "#{self.class.command_name} requires a frame name"
171
+ else
172
+ require 'json'
173
+ @frame = args[1]
174
+ @frame_args = {}
175
+ JSON.parse(args[2] || '{}').each do |key, value|
176
+ @frame_args[key.to_sym] = value
177
+ end
178
+ end
179
+ end
180
+
181
+ def print_usage
182
+ puts "Usage: #{$0} #{self.class.command_name} <config file> <frame> [args as a JSON string] [--debug]"
183
+ end
184
+ end
185
+
186
+ class LoadingCommand < ConfigCommand
187
+ def start
188
+ super
189
+ unless WhirledPeas.config.loading_template_factory
190
+ puts 'No loading screen configured'
191
+ exit
192
+ end
193
+
194
+ if args.last == '--debug'
195
+ puts WhirledPeas.config.loading_template_factory.build.inspect
196
+ exit
197
+ end
198
+
199
+ require 'whirled_peas/frame/event_loop'
200
+ require 'whirled_peas/frame/producer'
201
+
202
+ logger = self.class.build_logger(File.open('whirled_peas.log', 'a'))
203
+ consumer = Frame::EventLoop.new(
204
+ WhirledPeas.config.template_factory,
205
+ WhirledPeas.config.loading_template_factory,
206
+ DEFAULT_REFRESH_RATE,
207
+ logger
208
+ )
209
+ Frame::Producer.produce(consumer, logger) { sleep(5) }
210
+ end
211
+
212
+ private
213
+
214
+ def print_usage
215
+ puts "Usage: #{$0} #{self.class.command_name} [--debug]"
216
+ end
217
+ end
218
+
219
+ class CommandLine
220
+ COMMANDS = [
221
+ StartCommand,
222
+ ListFramesCommand,
223
+ PlayFrameCommand,
224
+ LoadingCommand,
225
+ TitleFontsCommand
226
+ ].map.with_object({}) { |c, h| h[c.command_name] = c }
227
+
228
+ def initialize(args)
229
+ @args = args
230
+ end
231
+
232
+ def start
233
+ if args.length < 1
234
+ print_usage
235
+ exit(1)
236
+ end
237
+
238
+ command = args.shift
239
+
240
+ unless COMMANDS.key?(command)
241
+ puts "Unrecognized command: #{command}"
242
+ print_usage
243
+ exit(1)
244
+ end
245
+
246
+ cmd = COMMANDS[command].new(args)
247
+
248
+ unless cmd.valid?
249
+ cmd.print_error
250
+ exit(1)
251
+ end
252
+
253
+ cmd.start
254
+ end
255
+
256
+ private
257
+
258
+ attr_reader :args
259
+
260
+ def print_usage
261
+ puts "Usage: #{$0} <command> [command options]"
262
+ puts
263
+ puts "Available commands: #{COMMANDS.keys.join(', ')}"
264
+ end
265
+ end
266
+ end