whirled_peas 0.7.1 → 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -1,13 +1,47 @@
1
+ require 'logger'
2
+
1
3
  module WhirledPeas
2
4
  class Config
3
- attr_writer :driver, :template_factory
4
- attr_accessor :loading_template_factory
5
+ # Refreshed rate measured in frames per second
6
+ DEFAULT_REFRESH_RATE = 30
7
+
8
+ DEFAULT_LOG_LEVEL = Logger::INFO
9
+ DEFAULT_LOG_FILE = 'whirled_peas.log'
5
10
 
6
- def driver
7
- unless @driver
8
- raise ConfigurationError, 'driver must be configured'
11
+ # This formatter expects a loggers to send `progname` in each log call. This value
12
+ # should be an all uppercase version of the module or class that is invoking the
13
+ # logger. Ruby's logger supports setting this value on a per-log statement basis
14
+ # when the log message is passed in through a block:
15
+ #
16
+ # logger.<level>(progname, &block)
17
+ #
18
+ # E.g.
19
+ #
20
+ # class Foo
21
+ # def bar
22
+ # logger.warn('FOO') { 'Something fishy happened in #bar' }
23
+ # end
24
+ # end
25
+ #
26
+ # The block format also has the advantage that the evaluation of the block only
27
+ # occurs if the message gets logged. So expensive to calculate debug statements
28
+ # will not impact the performance of the application if the log level is INFO or
29
+ # higher.
30
+ DEFAULT_FORMATTER = proc do |severity, datetime, progname, msg|
31
+ # Convert an instance of an exception into a nicely formatted message string
32
+ if msg.is_a?(Exception)
33
+ msg = %Q(#{msg.class}: #{msg.to_s}\n #{msg.backtrace.join("\n ")})
9
34
  end
10
- @driver
35
+ "[#{severity}] #{datetime.strftime('%Y-%m-%dT%H:%M:%S.%L')} (#{progname}) - #{msg}\n"
36
+ end
37
+
38
+ attr_writer :application, :template_factory, :refresh_rate, :log_level, :log_formatter, :log_file
39
+
40
+ def application
41
+ unless @application
42
+ raise ConfigurationError, 'application must be configured'
43
+ end
44
+ @application
11
45
  end
12
46
 
13
47
  def template_factory
@@ -16,6 +50,22 @@ module WhirledPeas
16
50
  end
17
51
  @template_factory
18
52
  end
53
+
54
+ def refresh_rate
55
+ @refresh_rate || DEFAULT_REFRESH_RATE
56
+ end
57
+
58
+ def log_level
59
+ @log_level || DEFAULT_LOG_LEVEL
60
+ end
61
+
62
+ def log_formatter
63
+ @log_formatter || DEFAULT_FORMATTER
64
+ end
65
+
66
+ def log_file
67
+ @log_file || DEFAULT_LOG_FILE
68
+ end
19
69
  end
20
70
  private_constant :Config
21
71
  end
@@ -0,0 +1,5 @@
1
+ module WhirledPeas
2
+ module Device
3
+ end
4
+ private_constant :Device
5
+ end
@@ -0,0 +1,8 @@
1
+ module WhirledPeas
2
+ module Device
3
+ class NullDevice
4
+ def handle_renders(*)
5
+ end
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,19 @@
1
+ require 'whirled_peas/utils/file_handler'
2
+
3
+ module WhirledPeas
4
+ module Device
5
+ class OutputFile
6
+ def initialize(file)
7
+ @file = file
8
+ end
9
+
10
+ def handle_renders(renders)
11
+ Utils::FileHandler.write(file, renders)
12
+ end
13
+
14
+ private
15
+
16
+ attr_reader :file
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,26 @@
1
+ require 'highline'
2
+
3
+ module WhirledPeas
4
+ module Device
5
+ class Screen
6
+ def initialize(refresh_rate, output: STDOUT)
7
+ @refresh_rate = refresh_rate
8
+ @output = output
9
+ end
10
+
11
+ def handle_renders(renders)
12
+ renders.each do |strokes|
13
+ frame_at = Time.now
14
+ output.print(strokes)
15
+ output.flush
16
+ next_frame_at = frame_at + 1.0 / refresh_rate
17
+ sleep([0, next_frame_at - Time.now].max)
18
+ end
19
+ end
20
+
21
+ private
22
+
23
+ attr_reader :refresh_rate, :output
24
+ end
25
+ end
26
+ end
@@ -5,6 +5,8 @@ require_relative 'painter'
5
5
 
6
6
  module WhirledPeas
7
7
  module Graphics
8
+ # Abstract Painter for containers. Containers (as the name implies) contain other child
9
+ # elements and must delegate painting of the children to the children themselves.
8
10
  class ContainerPainter < Painter
9
11
  PADDING = ' '
10
12
 
@@ -13,38 +15,65 @@ module WhirledPeas
13
15
  @children = []
14
16
  end
15
17
 
18
+ # Paint the common attributes of containers (e.g. border and background color). Any
19
+ # class that inherits from this one should call `super` at the start of its #paint
20
+ # method, before painting its children.
16
21
  def paint(canvas, left, top, &block)
17
22
  return unless canvas.writable?
18
23
  return unless needs_printing?
19
24
  canvas_coords = coords(left, top)
25
+
26
+ # Paint the border, background color, and scrollbar starting from the top left
27
+ # border position, moving down row by row until we reach the bottom border
28
+ # position
20
29
  stroke_left = canvas_coords.border_left
21
30
  stroke_top = canvas_coords.border_top
31
+
32
+ # All strokes will have the same formatting options
22
33
  formatting = [*settings.border.color, *settings.bg_color]
34
+
35
+ # Paint the top border if the settings call for it
23
36
  if settings.border.top?
24
37
  canvas.stroke(stroke_left, stroke_top, top_border_stroke(canvas_coords), formatting, &block)
25
38
  stroke_top += 1
26
39
  end
40
+ # Precalculate the middle border container grids with more than 1 row
27
41
  middle_border = dimensions.num_rows > 1 ? middle_border_stroke(canvas_coords) : ''
42
+
43
+ # Paint each grid row by row
28
44
  dimensions.num_rows.times do |row_num|
45
+ # In a grid with N rows, we will need to paint N - 1 inner horizontal borders.
46
+ # This code treats the inner horizontal border as the top of each row except for
47
+ # the first one.
29
48
  if row_num > 0 && settings.border.inner_horiz?
30
49
  canvas.stroke(stroke_left, stroke_top, middle_border, formatting, &block)
31
50
  stroke_top += 1
32
51
  end
52
+
53
+ # Paint the interior of each row (horizontal borders, veritical scroll bar and
54
+ # background color for the padding and content area)
33
55
  canvas_coords.inner_grid_height.times do |row_within_cell|
34
56
  canvas.stroke(stroke_left, stroke_top, content_line_stroke(canvas_coords, row_within_cell), formatting, &block)
35
57
  stroke_top += 1
36
58
  end
59
+
60
+ # Paint the horizontal scroll bar is the settings call for it
37
61
  if settings.scrollbar.horiz?
38
62
  canvas.stroke(stroke_left, stroke_top, bottom_scroll_stroke(canvas_coords), formatting, &block)
39
63
  stroke_top += 1
40
64
  end
41
65
  end
66
+
67
+ # Paint the bottom border if the settings call for it
42
68
  if settings.border.bottom?
43
69
  canvas.stroke(stroke_left, stroke_top, bottom_border_stroke(canvas_coords), formatting, &block)
44
70
  stroke_top += 1
45
71
  end
46
72
  end
47
73
 
74
+ # Tightly manage access to the children (rather than simply exposing the underlying
75
+ # array). This allows subclasses to easily modify behavior based on that element's
76
+ # specific settings.
48
77
  def add_child(child)
49
78
  children << child
50
79
  end
@@ -65,6 +94,8 @@ module WhirledPeas
65
94
 
66
95
  attr_reader :children
67
96
 
97
+ # Determine if there is anything to print for the container (this does not accont for
98
+ # children, just the border, scrollbar, and background color)
68
99
  def needs_printing?
69
100
  return true if settings.bg_color
70
101
  return true if settings.border.outer?
@@ -73,10 +104,15 @@ module WhirledPeas
73
104
  settings.scrollbar.horiz? || settings.scrollbar.vert?
74
105
  end
75
106
 
107
+ # Return an object that allows easy access to important coordinates within the container,
108
+ # e.g. the left position where the left border is printed
76
109
  def coords(left, top)
77
110
  ContainerCoords.new(dimensions, settings, left, top)
78
111
  end
79
112
 
113
+ # @return [Array<Integer>] a two-item array, the first being the amount of horizontal
114
+ # spacing to paint *before the first* child and the second being the amount of spacing
115
+ # to paint *between each* child
80
116
  def horiz_justify_offset(containing_width)
81
117
  if settings.align_center?
82
118
  [(dimensions.content_width - containing_width) / 2, 0]
@@ -96,6 +132,9 @@ module WhirledPeas
96
132
  end
97
133
  end
98
134
 
135
+ # @return [Array<Integer>] a two-item array, the first being the amount of vertical
136
+ # spacing to paint *above the first* child and the second being the amount of spacing
137
+ # to paint *between each* child
99
138
  def vert_justify_offset(containing_height)
100
139
  if settings.valign_middle?
101
140
  [(dimensions.content_height - containing_height) / 2, 0]
@@ -115,6 +154,16 @@ module WhirledPeas
115
154
  end
116
155
  end
117
156
 
157
+ # Return a stroke for one line of the container
158
+ #
159
+ # @param left_border [String] the character to print as the first character if there
160
+ # is a left border
161
+ # @param junc_border [String] the character to print as the junction between two grid
162
+ # columns if there is an inner vertical border
163
+ # @param right_border [String] the character to print as the last character if there
164
+ # is a right border
165
+ # @block [String] the block should yield a string that represents the interior
166
+ # (including padding) of a grid cell
118
167
  def line_stroke(left_border, junc_border, right_border, &block)
119
168
  stroke = ''
120
169
  stroke += left_border if settings.border.left?
@@ -126,6 +175,7 @@ module WhirledPeas
126
175
  stroke
127
176
  end
128
177
 
178
+ # Return the stroke for the top border
129
179
  def top_border_stroke(canvas_coords)
130
180
  line_stroke(
131
181
  settings.border.style.top_left,
@@ -136,6 +186,7 @@ module WhirledPeas
136
186
  end
137
187
  end
138
188
 
189
+ # Return the stroke for an inner horizontal border
139
190
  def middle_border_stroke(canvas_coords)
140
191
  line_stroke(
141
192
  settings.border.style.left_junc,
@@ -146,6 +197,7 @@ module WhirledPeas
146
197
  end
147
198
  end
148
199
 
200
+ # Return the stroke for the bottom border
149
201
  def bottom_border_stroke(canvas_coords)
150
202
  line_stroke(
151
203
  settings.border.style.bottom_left,
@@ -156,6 +208,7 @@ module WhirledPeas
156
208
  end
157
209
  end
158
210
 
211
+ # Return the stroke for a grid row between any borders
159
212
  def content_line_stroke(canvas_coords, row_within_cell)
160
213
  line_stroke(
161
214
  settings.border.style.left_vert,
@@ -180,6 +233,7 @@ module WhirledPeas
180
233
  end
181
234
  end
182
235
 
236
+ # Return the stroke for the horizontal scroll bar
183
237
  def bottom_scroll_stroke(canvas_coords)
184
238
  line_stroke(
185
239
  settings.border.style.left_vert,
@@ -197,6 +251,7 @@ module WhirledPeas
197
251
  end
198
252
  end
199
253
 
254
+ # Contants to paint scrollbars
200
255
  GUTTER = ' '
201
256
  HORIZONTAL = %w[▗ ▄ ▖]
202
257
  VERTICAL = %w[
@@ -205,16 +260,33 @@ module WhirledPeas
205
260
 
206
261
  ]
207
262
 
263
+ # Determine the character to paint the horizontal scroll bar with for the given column
264
+ #
265
+ # @see #scroll_char for more details
208
266
  def horiz_scroll_char(col_count, viewable_col_count, first_visible_col, curr_col)
209
267
  scroll_char(col_count, viewable_col_count, first_visible_col, curr_col, HORIZONTAL)
210
268
  end
211
269
 
270
+ # Determine the character to paint the vertical scroll bar with for the given row
271
+ #
272
+ # @see #scroll_char for more details
212
273
  def vert_scroll_char(row_count, viewable_row_count, first_visible_row, curr_row)
213
274
  scroll_char(row_count, viewable_row_count, first_visible_row, curr_row, VERTICAL)
214
275
  end
215
276
 
216
277
  private
217
278
 
279
+ # Determine which character to paint a for a scrollbar
280
+ #
281
+ # @param total_count [Integer] total number of rows/columns in the content
282
+ # @param viewable_count [Integer] number of rows/columns visible in the viewport
283
+ # @param first_visible [Integer] zero-based index of the first row/column that is visible
284
+ # in the viewport
285
+ # @param curr [Integer] zero-based index of the row/column (relative to the first visible
286
+ # row/column) that the painted character is being requested for
287
+ # @param chars [Array<String>] an array with three 1-character strings, the frist is the
288
+ # "second half" scrollbar character, the second is the "full" scrollbar character, and
289
+ # the third is the "first half" scrollbar character.
218
290
  def scroll_char(total_count, viewable_count, first_visible, curr, chars)
219
291
  return GUTTER unless total_count > 0 && viewable_count > 0
220
292
  # The scroll handle has the exact same relative size and position in the scroll gutter
@@ -239,6 +311,25 @@ module WhirledPeas
239
311
  # | * *
240
312
  # +---------1---------2---------3*********4*********+
241
313
  # |...........********|
314
+ #
315
+ # Returning to the first example, we can match up the arguments to this method to the
316
+ # diagram
317
+ #
318
+ # total_count = 50
319
+ # |<----------------------------------------------->|
320
+ # | |
321
+ # | veiwable_count = 20 |
322
+ # | |<----------------->| |
323
+ # ↓ ↓ ↓ ↓
324
+ # +---------1-----****2*********3******---4---------+
325
+ # | * * |
326
+ # | hidden * viewable * hidden |
327
+ # | * * |
328
+ # +---------1-----****2*********3******---4---------+
329
+ # |......****?***.....|
330
+ # ↑ ↑
331
+ # first_visible = 16 |
332
+ # curr = 11
242
333
 
243
334
  # The first task of determining how much of the handle is visible in a row/column is to
244
335
  # calculate the range (as a precentage of the total) of viewable items
@@ -1,5 +1,7 @@
1
1
  module WhirledPeas
2
2
  module Graphics
3
+ # Abstract base Painter class. Given a canvas and start coordinates (left, top), a painter
4
+ # is responsible for generating the "strokes" that display the element.
3
5
  class Painter
4
6
  attr_reader :name, :settings
5
7
 
@@ -8,9 +10,17 @@ module WhirledPeas
8
10
  @name = name
9
11
  end
10
12
 
13
+ # Paint the element onto the canvas by yielding strokes to the block. A stroke is composed
14
+ # of a left, top, and chars. E.g.
15
+ #
16
+ # yield 10, 3, 'Hello World!'
17
+ #
18
+ # paints the string "Hello World!" in the 10th column from the left, 3rd row down.
11
19
  def paint(canvas, left, top, &block)
12
20
  end
13
21
 
22
+ # Return a dimension object that provider the `outer_width` and `outer_height` of the
23
+ # element being painted.
14
24
  def dimensions
15
25
  end
16
26
 
@@ -9,13 +9,19 @@ module WhirledPeas
9
9
  @height = height
10
10
  end
11
11
 
12
- def paint(&block)
12
+ def paint
13
13
  # Modify the template's settings so that it fills the entire screen
14
14
  template.settings.width = width
15
15
  template.settings.height = height
16
16
  template.settings.sizing = :border
17
17
  template.settings.set_margin(left: 0, top: 0, right: 0, bottom: 0)
18
- template.paint(Canvas.new(0, 0, width, height), 0, 0, &block)
18
+ strokes = [Utils::Ansi.cursor_visible(false), Utils::Ansi.cursor_pos, Utils::Ansi.clear_down]
19
+ template.paint(Canvas.new(0, 0, width, height), 0, 0) do |left, top, fstring|
20
+ next unless fstring.length > 0
21
+ strokes << Utils::Ansi.cursor_pos(left: left, top: top)
22
+ strokes << fstring
23
+ end
24
+ strokes.join
19
25
  end
20
26
 
21
27
  private
@@ -28,6 +28,19 @@ module WhirledPeas
28
28
  BRIGHT_OFFSET = 60
29
29
 
30
30
  class << self
31
+ def with_screen(output, width: nil, height: nil, &block)
32
+ require 'highline'
33
+ unless width && height
34
+ width, height = HighLine.new.terminal.terminal_size
35
+ end
36
+ yield width, height
37
+ ensure
38
+ output.print clear
39
+ output.print cursor_pos(top: height - 1)
40
+ output.print cursor_visible(true)
41
+ output.flush
42
+ end
43
+
31
44
  def cursor_pos(top: 0, left: 0)
32
45
  "#{ESC}[#{top + 1};#{left + 1}H"
33
46
  end
@@ -0,0 +1,57 @@
1
+ require 'base64'
2
+ require 'zlib'
3
+
4
+ module WhirledPeas
5
+ module Utils
6
+ module FileHandler
7
+ module FileWriter
8
+ VERSION = '1'
9
+
10
+ def self.write(fp, renders)
11
+ fp.puts renders.count
12
+ renders.each do |strokes|
13
+ encoded = Base64.encode64(strokes)
14
+ fp.puts encoded.count("\n")
15
+ fp.puts encoded
16
+ end
17
+ end
18
+ end
19
+ private_constant :FileWriter
20
+
21
+ class FileReaderV1
22
+ def self.read(fp)
23
+ num_renders = Integer(fp.readline.chomp, 10)
24
+ num_renders.times.map do
25
+ num_strokes = Integer(fp.readline.chomp, 10)
26
+ Base64.decode64(num_strokes.times.map { fp.readline }.join)
27
+ end
28
+ end
29
+
30
+ private
31
+
32
+ attr_reader :file
33
+ end
34
+ private_constant :FileReaderV1
35
+
36
+ READERS = {
37
+ '1' => FileReaderV1
38
+ }
39
+ private_constant :READERS
40
+
41
+ def self.write(file, renders)
42
+ Zlib::GzipWriter.open(file, Zlib::BEST_COMPRESSION) do |gz|
43
+ gz.puts FileWriter::VERSION
44
+ FileWriter.write(gz, renders)
45
+ end
46
+ end
47
+
48
+ def self.read(file)
49
+ Zlib::GzipReader.open(file) do |gz|
50
+ version = gz.gets.chomp
51
+ raise ArgumentError, "Invalid file: #{file}" unless READERS.key?(version)
52
+ READERS[version].read(gz)
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end