whirled_peas 0.7.1 → 0.8.0

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.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/CHANGELOG.md +7 -0
  4. data/README.md +116 -88
  5. data/Rakefile +1 -20
  6. data/bin/reset_cursor +11 -0
  7. data/bin/screen_test +68 -0
  8. data/examples/intro.rb +3 -3
  9. data/examples/scrolling.rb +5 -4
  10. data/lib/whirled_peas.rb +2 -4
  11. data/lib/whirled_peas/animator.rb +5 -0
  12. data/lib/whirled_peas/animator/debug_consumer.rb +17 -0
  13. data/lib/whirled_peas/animator/easing.rb +72 -0
  14. data/lib/whirled_peas/animator/frame.rb +5 -0
  15. data/lib/whirled_peas/animator/frameset.rb +33 -0
  16. data/lib/whirled_peas/animator/producer.rb +35 -0
  17. data/lib/whirled_peas/animator/renderer_consumer.rb +31 -0
  18. data/lib/whirled_peas/command.rb +5 -0
  19. data/lib/whirled_peas/command/base.rb +86 -0
  20. data/lib/whirled_peas/command/config_command.rb +44 -0
  21. data/lib/whirled_peas/command/debug.rb +21 -0
  22. data/lib/whirled_peas/command/fonts.rb +22 -0
  23. data/lib/whirled_peas/command/frame_command.rb +34 -0
  24. data/lib/whirled_peas/command/frames.rb +24 -0
  25. data/lib/whirled_peas/command/help.rb +38 -0
  26. data/lib/whirled_peas/command/play.rb +108 -0
  27. data/lib/whirled_peas/command/record.rb +57 -0
  28. data/lib/whirled_peas/command/still.rb +29 -0
  29. data/lib/whirled_peas/command_line.rb +22 -212
  30. data/lib/whirled_peas/config.rb +56 -6
  31. data/lib/whirled_peas/device.rb +5 -0
  32. data/lib/whirled_peas/device/null_device.rb +8 -0
  33. data/lib/whirled_peas/device/output_file.rb +19 -0
  34. data/lib/whirled_peas/device/screen.rb +26 -0
  35. data/lib/whirled_peas/graphics/container_painter.rb +91 -0
  36. data/lib/whirled_peas/graphics/painter.rb +10 -0
  37. data/lib/whirled_peas/graphics/renderer.rb +8 -2
  38. data/lib/whirled_peas/utils/ansi.rb +13 -0
  39. data/lib/whirled_peas/utils/file_handler.rb +57 -0
  40. data/lib/whirled_peas/version.rb +1 -1
  41. data/tools/whirled_peas/tools/screen_tester.rb +117 -65
  42. metadata +27 -8
  43. data/lib/whirled_peas/frame.rb +0 -6
  44. data/lib/whirled_peas/frame/consumer.rb +0 -30
  45. data/lib/whirled_peas/frame/debug_consumer.rb +0 -30
  46. data/lib/whirled_peas/frame/event_loop.rb +0 -90
  47. data/lib/whirled_peas/frame/producer.rb +0 -67
  48. data/lib/whirled_peas/graphics/screen.rb +0 -70
@@ -40,13 +40,13 @@ class TemplateFactory
40
40
  end
41
41
  end
42
42
 
43
- class Driver
43
+ class Application
44
44
  def start(producer)
45
- producer.send_frame('intro', duration: 5)
45
+ producer.add_frame('intro', duration: 5)
46
46
  end
47
47
  end
48
48
 
49
49
  WhirledPeas.configure do |config|
50
50
  config.template_factory = TemplateFactory.new
51
- config.driver = Driver.new
51
+ config.application = Application.new
52
52
  end
@@ -39,15 +39,16 @@ class TemplateFactory
39
39
  end
40
40
  end
41
41
 
42
- class Driver
42
+ class Application
43
43
  def start(producer)
44
- 53.times do |i|
45
- producer.send_frame('intro', duration: 0.3, args: { top: -i })
44
+ producer.frameset(5, easing: :bezier) do |fs|
45
+ 53.times { |i| fs.add_frame('intro', { top: -i }) }
46
46
  end
47
+ producer.add_frame('hold', duration: 1, args: { top: -52})
47
48
  end
48
49
  end
49
50
 
50
51
  WhirledPeas.configure do |config|
51
52
  config.template_factory = TemplateFactory.new
52
- config.driver = Driver.new
53
+ config.application = Application.new
53
54
  end
@@ -1,8 +1,6 @@
1
- require 'logger'
2
-
3
- require 'whirled_peas/errors'
1
+ require 'whirled_peas/animator'
4
2
  require 'whirled_peas/config'
5
- require 'whirled_peas/frame'
3
+ require 'whirled_peas/errors'
6
4
  require 'whirled_peas/graphics'
7
5
  require 'whirled_peas/settings'
8
6
  require 'whirled_peas/utils'
@@ -0,0 +1,5 @@
1
+ module WhirledPeas
2
+ module Animator
3
+ end
4
+ private_constant :Animator
5
+ end
@@ -0,0 +1,17 @@
1
+ module WhirledPeas
2
+ module Animator
3
+ class DebugConsumer
4
+ def add_frameset(frameset)
5
+ require 'json'
6
+
7
+ frameset.each_frame do |frame, args|
8
+ puts [frame, *(JSON.generate(args) unless args.empty?)].join(' ')
9
+ end
10
+ end
11
+
12
+ def process
13
+ # no op
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,72 @@
1
+ module WhirledPeas
2
+ module Animator
3
+ class Easing
4
+ # Implementations of available ease-in functions. Ease-out and ease-in-out can all be
5
+ # derived from ease-in.
6
+ EASING = {
7
+ bezier: proc do |value|
8
+ value /= 2
9
+ 2 * value * value * (3 - 2 * value)
10
+ end,
11
+ linear: proc { |value| value },
12
+ parametric: proc do |value|
13
+ value /= 2
14
+ squared = value * value
15
+ 2 * squared / (2 * (squared - value) + 1)
16
+ end,
17
+ quadratic: proc do |value|
18
+ value * value
19
+ end
20
+ }
21
+
22
+ EFFECTS = %i[in out in_out]
23
+
24
+ def initialize(easing=:linear, effect=:in_out)
25
+ unless EASING.key?(easing)
26
+ raise ArgumentError,
27
+ "Invalid easing function: #{easing}, expecting one of #{EASING.keys.join(', ')}"
28
+ end
29
+ unless EFFECTS.include?(effect)
30
+ raise ArgumentError,
31
+ "Invalid effect: #{effect}, expecting one of #{EFFECTS.join(', ')}"
32
+ end
33
+ @easing = easing
34
+ @effect = effect
35
+ end
36
+
37
+ def ease(value)
38
+ case effect
39
+ when :in
40
+ ease_in(value)
41
+ when :out
42
+ ease_out(value)
43
+ else
44
+ ease_in_out(value)
45
+ end
46
+ end
47
+
48
+ private
49
+
50
+ attr_reader :easing, :effect
51
+
52
+ # The procs in EASING define the ease-in functions, so we simply need
53
+ # to invoke the function with the given normalized value
54
+ def ease_in(value)
55
+ EASING[easing].call(value)
56
+ end
57
+
58
+ # The ease-out function will be the ease-in function rotated 180 degrees.
59
+ def ease_out(value)
60
+ 1 - EASING[easing].call(1 - value)
61
+ end
62
+
63
+ def ease_in_out(value)
64
+ if value < 0.5
65
+ ease_in(value * 2) / 2
66
+ else
67
+ 0.5 + ease_out(2 * value - 1) / 2
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,5 @@
1
+ module WhirledPeas
2
+ module Animator
3
+ Frame = Struct.new(:name, :args)
4
+ end
5
+ end
@@ -0,0 +1,33 @@
1
+ require_relative 'easing'
2
+ require_relative 'frame'
3
+
4
+ module WhirledPeas
5
+ module Animator
6
+ class Frameset
7
+ def initialize(frame_slots, easing, effect)
8
+ @frame_slots = frame_slots
9
+ @easing = Easing.new(easing, effect)
10
+ @frames = []
11
+ end
12
+
13
+ def add_frame(name, args={})
14
+ frames << [name, args]
15
+ end
16
+
17
+ # Yield each frame in an "eased" order
18
+ def each_frame(&block)
19
+ frame_slots.times do |i|
20
+ input = i.to_f / (frame_slots - 1)
21
+ eased_value = @easing.ease(input)
22
+ index = (eased_value * (frames.length - 1)).floor
23
+ yield *frames[index]
24
+ end
25
+ end
26
+
27
+ private
28
+
29
+ attr_reader :frame_slots, :easing, :frames
30
+ end
31
+ private_constant :Frameset
32
+ end
33
+ end
@@ -0,0 +1,35 @@
1
+ require_relative 'easing'
2
+ require_relative 'frameset'
3
+
4
+ module WhirledPeas
5
+ module Animator
6
+ class Producer
7
+ def self.produce(consumer, refresh_rate)
8
+ producer = new(consumer, refresh_rate)
9
+ yield producer
10
+ consumer.process
11
+ end
12
+
13
+ def initialize(consumer, refresh_rate)
14
+ @consumer = consumer
15
+ @refresh_rate = refresh_rate
16
+ end
17
+
18
+ def add_frame(name, duration: nil, args: {})
19
+ frameset(duration || 1 / refresh_rate) do |fs|
20
+ fs.add_frame(name, args)
21
+ end
22
+ end
23
+
24
+ def frameset(duration, easing: :linear, effect: :in_out, &block)
25
+ fs = Frameset.new((duration * refresh_rate).round, easing, effect)
26
+ yield fs
27
+ consumer.add_frameset(fs)
28
+ end
29
+
30
+ private
31
+
32
+ attr_reader :consumer, :refresh_rate
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,31 @@
1
+ require 'whirled_peas/graphics/renderer'
2
+ require 'whirled_peas/utils/ansi'
3
+
4
+ module WhirledPeas
5
+ module Animator
6
+ class RendererConsumer
7
+ def initialize(template_factory, device, width, height)
8
+ @template_factory = template_factory
9
+ @device = device
10
+ @width = width
11
+ @height = height
12
+ @renders = []
13
+ end
14
+
15
+ def add_frameset(frameset)
16
+ frameset.each_frame do |frame, args|
17
+ template = template_factory.build(frame, args)
18
+ renders << Graphics::Renderer.new(template, width, height).paint
19
+ end
20
+ end
21
+
22
+ def process
23
+ device.handle_renders(renders)
24
+ end
25
+
26
+ private
27
+
28
+ attr_reader :template_factory, :device, :width, :height, :renders
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,5 @@
1
+ module WhirledPeas
2
+ module Command
3
+ end
4
+ private_constant :Command
5
+ end
@@ -0,0 +1,86 @@
1
+ module WhirledPeas
2
+ module Command
3
+ # Abstract base class for all commands
4
+ class Base
5
+ # Returns the name of the command as expected by the command line script. By convention,
6
+ # this name is snake case version of the class name, e.g. the command `do_something`
7
+ # would be implemented by `WhirledPeas::Command::DoSomething`, which needs to inherit
8
+ # from this class.
9
+ def self.command_name
10
+ name.split('::').last.gsub(/([a-z])([A-Z])/, '\1_\2').downcase
11
+ end
12
+
13
+ def self.description
14
+ end
15
+
16
+ def self.print_usage
17
+ puts description
18
+ end
19
+
20
+ attr_reader :args
21
+
22
+ def initialize(args, config)
23
+ @args = args
24
+ @config = config
25
+ end
26
+
27
+ # Returns the name of the command as expected by the command line script. By convention,
28
+ # this name is snake case version of the class name, e.g. the command `do_something`
29
+ # would be implemented by `WhirledPeas::Command::DoSomething`, which needs to inherit
30
+ # from this class.
31
+ def command_name
32
+ self.class.command_name
33
+ end
34
+
35
+ def build_logger
36
+ require 'logger'
37
+
38
+ if config.log_file.is_a?(IO)
39
+ output = config.log_file
40
+ else
41
+ File.open(config.log_file, 'a')
42
+ end
43
+
44
+ logger = Logger.new(output)
45
+ logger.level = config.log_level
46
+ logger.formatter = config.log_formatter
47
+ logger
48
+ end
49
+
50
+ # @return [true|false] true if all of the required options were provided
51
+ def valid?
52
+ @error_text = nil
53
+ validate!
54
+ @error_text.nil?
55
+ end
56
+
57
+ # Display the validation error and print a usage statement
58
+ def print_error
59
+ puts @error_text if @error_text
60
+ print_usage
61
+ end
62
+
63
+ # Commands that inherit from this class must override this method
64
+ def start
65
+ end
66
+
67
+ private
68
+
69
+ attr_reader :config
70
+
71
+ def print_usage
72
+ puts ["Usage: #{$0} #{command_name}", *options_usage].join(' ')
73
+ end
74
+
75
+ # Commands that inherit from this class can override this method to validate
76
+ # command line options
77
+ def validate!
78
+ # Set @error_text if the options are not valid
79
+ end
80
+
81
+ def options_usage
82
+ end
83
+ end
84
+ private_constant :Base
85
+ end
86
+ end
@@ -0,0 +1,44 @@
1
+ require_relative 'base'
2
+
3
+ module WhirledPeas
4
+ module Command
5
+ # Abstract command that expects a config file as an argument and then requires the
6
+ # specified file. All implementing classes must call `super` if they override `start`
7
+ # or `validate!`
8
+ class ConfigCommand < Base
9
+ def start
10
+ require config_file
11
+ rescue => e
12
+ puts "Error loading #{config_file}"
13
+ puts e.message
14
+ exit(1)
15
+ end
16
+
17
+ private
18
+
19
+ attr_reader :config_file
20
+
21
+ def validate!
22
+ super
23
+ # Note that the main script consumes the <command> argument from ARGV, so we
24
+ # expect the config file to be at index 0.
25
+ config_file = args.shift
26
+ if config_file.nil?
27
+ @error_text = "#{command_name} requires a config file"
28
+ elsif !File.exist?(config_file)
29
+ @error_text = "File not found: #{config_file}"
30
+ elsif config_file[-3..-1] != '.rb'
31
+ @error_text = 'Config file should be a .rb file'
32
+ else
33
+ # We think we have a valid ruby config file, set the absolute path to @config
34
+ @config_file = config_file[0] == '/' ? config_file : File.join(Dir.pwd, config_file)
35
+ end
36
+ end
37
+
38
+ def options_usage
39
+ '<config file>'
40
+ end
41
+ end
42
+ private_constant :ConfigCommand
43
+ end
44
+ end
@@ -0,0 +1,21 @@
1
+ require_relative 'frame_command'
2
+
3
+ module WhirledPeas
4
+ module Command
5
+ # Display the template tree for a single frame with the specified arguments.
6
+ class Debug < FrameCommand
7
+ def self.description
8
+ 'Print template tree for specified frame'
9
+ end
10
+
11
+ def start
12
+ super
13
+
14
+ require 'whirled_peas/graphics/debugger'
15
+
16
+ template = config.template_factory.build(frame, frame_args)
17
+ puts Graphics::Debugger.new(template).debug
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,22 @@
1
+ require_relative 'base'
2
+
3
+ module WhirledPeas
4
+ module Command
5
+ # List title fonts installed on the user's system and print sample text in each.
6
+ class Fonts < Base
7
+ def self.description
8
+ 'List installed title fonts with sample text'
9
+ end
10
+
11
+ def start
12
+ require 'whirled_peas/utils/title_font'
13
+
14
+ Utils::TitleFont.fonts.keys.each do |key|
15
+ puts Utils::TitleFont.to_s(key.to_s, key)
16
+ puts key.inspect
17
+ puts
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,34 @@
1
+ require_relative 'config_command'
2
+
3
+ module WhirledPeas
4
+ module Command
5
+ class FrameCommand < ConfigCommand
6
+ private
7
+
8
+ attr_reader :frame, :frame_args
9
+
10
+ def validate!
11
+ super
12
+ frame = args.shift
13
+ raw_args = args.shift
14
+ if frame.nil?
15
+ @error_text = "#{command_name} requires a frame name"
16
+ else
17
+ @frame = frame
18
+ @frame_args = {}
19
+ return if raw_args.nil?
20
+
21
+ require 'json'
22
+
23
+ JSON.parse(raw_args || '{}').each do |key, value|
24
+ @frame_args[key.to_sym] = value
25
+ end
26
+ end
27
+ end
28
+
29
+ def options_usage
30
+ "#{super} <frame> [args as a JSON string]"
31
+ end
32
+ end
33
+ end
34
+ end