whirled_peas 0.7.1 → 0.11.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 (262) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/CHANGELOG.md +25 -0
  4. data/README.md +530 -121
  5. data/Rakefile +1 -20
  6. data/bin/easing +33 -0
  7. data/bin/reset_cursor +11 -0
  8. data/bin/screen_test +68 -0
  9. data/examples/components.rb +35 -0
  10. data/examples/graph.rb +54 -0
  11. data/examples/intro.rb +3 -3
  12. data/examples/scrolling.rb +13 -23
  13. data/lib/data/themes.yaml +13 -0
  14. data/lib/whirled_peas.rb +33 -13
  15. data/lib/whirled_peas/animator.rb +5 -0
  16. data/lib/whirled_peas/animator/debug_consumer.rb +17 -0
  17. data/lib/whirled_peas/animator/easing.rb +72 -0
  18. data/lib/whirled_peas/animator/frame.rb +5 -0
  19. data/lib/whirled_peas/animator/frameset.rb +33 -0
  20. data/lib/whirled_peas/animator/producer.rb +35 -0
  21. data/lib/whirled_peas/animator/renderer_consumer.rb +31 -0
  22. data/lib/whirled_peas/command.rb +5 -0
  23. data/lib/whirled_peas/command/base.rb +86 -0
  24. data/lib/whirled_peas/command/config_command.rb +44 -0
  25. data/lib/whirled_peas/command/debug.rb +21 -0
  26. data/lib/whirled_peas/command/fonts.rb +22 -0
  27. data/lib/whirled_peas/command/frame_command.rb +34 -0
  28. data/lib/whirled_peas/command/frames.rb +24 -0
  29. data/lib/whirled_peas/command/help.rb +38 -0
  30. data/lib/whirled_peas/command/play.rb +112 -0
  31. data/lib/whirled_peas/command/record.rb +57 -0
  32. data/lib/whirled_peas/command/still.rb +29 -0
  33. data/lib/whirled_peas/command/themes.rb +77 -0
  34. data/lib/whirled_peas/command_line.rb +24 -212
  35. data/lib/whirled_peas/component.rb +35 -0
  36. data/lib/whirled_peas/component/list_with_active.rb +88 -0
  37. data/lib/whirled_peas/config.rb +56 -6
  38. data/lib/whirled_peas/device.rb +5 -0
  39. data/lib/whirled_peas/device/null_device.rb +8 -0
  40. data/lib/whirled_peas/device/output_file.rb +19 -0
  41. data/lib/whirled_peas/device/screen.rb +26 -0
  42. data/lib/whirled_peas/graphics/box_painter.rb +4 -4
  43. data/lib/whirled_peas/graphics/composer.rb +27 -2
  44. data/lib/whirled_peas/graphics/container_coords.rb +24 -2
  45. data/lib/whirled_peas/graphics/container_painter.rb +63 -82
  46. data/lib/whirled_peas/graphics/content_dimensions.rb +19 -0
  47. data/lib/whirled_peas/graphics/content_painter.rb +20 -0
  48. data/lib/whirled_peas/graphics/debugger.rb +1 -7
  49. data/lib/whirled_peas/graphics/graph_dimensions.rb +12 -0
  50. data/lib/whirled_peas/graphics/graph_painter.rb +97 -0
  51. data/lib/whirled_peas/graphics/grid_painter.rb +0 -3
  52. data/lib/whirled_peas/graphics/painter.rb +10 -0
  53. data/lib/whirled_peas/graphics/renderer.rb +8 -2
  54. data/lib/whirled_peas/graphics/scrollbar_helper.rb +124 -0
  55. data/lib/whirled_peas/graphics/text_painter.rb +7 -11
  56. data/lib/whirled_peas/null_logger.rb +0 -1
  57. data/lib/whirled_peas/settings/border.rb +25 -4
  58. data/lib/whirled_peas/settings/container_settings.rb +14 -9
  59. data/lib/whirled_peas/settings/debugger.rb +1 -1
  60. data/lib/whirled_peas/settings/element_settings.rb +12 -3
  61. data/lib/whirled_peas/settings/graph_settings.rb +21 -0
  62. data/lib/whirled_peas/settings/grid_settings.rb +7 -0
  63. data/lib/whirled_peas/settings/position.rb +17 -5
  64. data/lib/whirled_peas/settings/text_settings.rb +1 -0
  65. data/lib/whirled_peas/settings/theme.rb +40 -0
  66. data/lib/whirled_peas/settings/theme_library.rb +63 -0
  67. data/lib/whirled_peas/utils/ansi.rb +13 -0
  68. data/lib/whirled_peas/utils/file_handler.rb +57 -0
  69. data/lib/whirled_peas/version.rb +1 -1
  70. data/screen_test/components/list_with_active/l2r_active_color.frame +1 -0
  71. data/screen_test/components/list_with_active/l2r_active_color.rb +18 -0
  72. data/screen_test/components/list_with_active/l2r_position_end.frame +1 -0
  73. data/screen_test/components/list_with_active/l2r_position_end.rb +16 -0
  74. data/screen_test/components/list_with_active/l2r_position_middle.frame +1 -0
  75. data/screen_test/components/list_with_active/l2r_position_middle.rb +16 -0
  76. data/screen_test/components/list_with_active/l2r_position_start.frame +1 -0
  77. data/screen_test/components/list_with_active/l2r_position_start.rb +16 -0
  78. data/screen_test/components/list_with_active/l2r_separator.frame +1 -0
  79. data/screen_test/components/list_with_active/l2r_separator.rb +17 -0
  80. data/screen_test/components/list_with_active/t2b_active_color.frame +1 -0
  81. data/screen_test/components/list_with_active/t2b_active_color.rb +19 -0
  82. data/screen_test/components/list_with_active/t2b_position_end.frame +1 -0
  83. data/screen_test/components/list_with_active/t2b_position_end.rb +17 -0
  84. data/screen_test/components/list_with_active/t2b_position_middle.frame +1 -0
  85. data/screen_test/components/list_with_active/t2b_position_middle.rb +17 -0
  86. data/screen_test/components/list_with_active/t2b_position_start.frame +1 -0
  87. data/screen_test/components/list_with_active/t2b_position_start.rb +17 -0
  88. data/screen_test/components/list_with_active/t2b_separator.frame +1 -0
  89. data/screen_test/components/list_with_active/t2b_separator.rb +18 -0
  90. data/screen_test/elements/box.frame +1 -1
  91. data/screen_test/elements/box.rb +1 -1
  92. data/screen_test/elements/graph.frame +1 -0
  93. data/screen_test/elements/graph.rb +12 -0
  94. data/screen_test/elements/grid.frame +1 -1
  95. data/screen_test/elements/grid.rb +1 -1
  96. data/screen_test/elements/screen_overflow_x.frame +1 -1
  97. data/screen_test/elements/screen_overflow_x.rb +1 -1
  98. data/screen_test/elements/screen_overflow_y.frame +1 -1
  99. data/screen_test/elements/screen_overflow_y.rb +1 -1
  100. data/screen_test/elements/text.frame +1 -1
  101. data/screen_test/elements/text.rb +1 -1
  102. data/screen_test/elements/text_multiline.frame +1 -1
  103. data/screen_test/elements/text_multiline.rb +1 -1
  104. data/screen_test/elements/theme.frame +1 -0
  105. data/screen_test/elements/theme.rb +26 -0
  106. data/screen_test/settings/align/box_around.frame +1 -1
  107. data/screen_test/settings/align/box_around.rb +1 -1
  108. data/screen_test/settings/align/box_between.frame +1 -1
  109. data/screen_test/settings/align/box_between.rb +1 -1
  110. data/screen_test/settings/align/box_center.frame +1 -1
  111. data/screen_test/settings/align/box_center.rb +1 -1
  112. data/screen_test/settings/align/box_default.frame +1 -1
  113. data/screen_test/settings/align/box_default.rb +1 -1
  114. data/screen_test/settings/align/box_evenly.frame +1 -1
  115. data/screen_test/settings/align/box_evenly.rb +1 -1
  116. data/screen_test/settings/align/box_left.frame +1 -1
  117. data/screen_test/settings/align/box_left.rb +1 -1
  118. data/screen_test/settings/align/box_right.frame +1 -1
  119. data/screen_test/settings/align/box_right.rb +1 -1
  120. data/screen_test/settings/align/children_center.frame +1 -1
  121. data/screen_test/settings/align/children_center.rb +1 -1
  122. data/screen_test/settings/align/children_left.frame +1 -1
  123. data/screen_test/settings/align/children_left.rb +1 -1
  124. data/screen_test/settings/align/children_right.frame +1 -1
  125. data/screen_test/settings/align/children_right.rb +1 -1
  126. data/screen_test/settings/align/grid_center.frame +1 -1
  127. data/screen_test/settings/align/grid_center.rb +1 -1
  128. data/screen_test/settings/align/grid_default.frame +1 -1
  129. data/screen_test/settings/align/grid_default.rb +1 -1
  130. data/screen_test/settings/align/grid_left.frame +1 -1
  131. data/screen_test/settings/align/grid_left.rb +1 -1
  132. data/screen_test/settings/align/grid_right.frame +1 -1
  133. data/screen_test/settings/align/grid_right.rb +1 -1
  134. data/screen_test/settings/ansi/bold.frame +1 -1
  135. data/screen_test/settings/ansi/bold.rb +1 -1
  136. data/screen_test/settings/ansi/color.frame +1 -1
  137. data/screen_test/settings/ansi/color.rb +1 -1
  138. data/screen_test/settings/ansi/underline.frame +1 -1
  139. data/screen_test/settings/ansi/underline.rb +1 -1
  140. data/screen_test/settings/border.frame +1 -1
  141. data/screen_test/settings/border.rb +1 -1
  142. data/screen_test/settings/content_start/box_bottom.frame +1 -0
  143. data/screen_test/settings/content_start/box_bottom.rb +17 -0
  144. data/screen_test/settings/content_start/box_left.frame +1 -0
  145. data/screen_test/settings/{position → content_start}/box_left.rb +2 -2
  146. data/screen_test/settings/content_start/box_left_negative.frame +1 -0
  147. data/screen_test/settings/{position → content_start}/box_left_negative.rb +2 -2
  148. data/screen_test/settings/content_start/box_right.frame +1 -0
  149. data/screen_test/settings/content_start/box_right.rb +17 -0
  150. data/screen_test/settings/content_start/box_top.frame +1 -0
  151. data/screen_test/settings/{position → content_start}/box_top.rb +2 -2
  152. data/screen_test/settings/content_start/box_top_negative.frame +1 -0
  153. data/screen_test/settings/{position → content_start}/box_top_negative.rb +2 -2
  154. data/screen_test/settings/content_start/grid_left.frame +1 -0
  155. data/screen_test/settings/{position → content_start}/grid_left.rb +2 -2
  156. data/screen_test/settings/content_start/grid_left_negative.frame +1 -0
  157. data/screen_test/settings/{position → content_start}/grid_left_negative.rb +2 -2
  158. data/screen_test/settings/content_start/grid_top.frame +1 -0
  159. data/screen_test/settings/{position → content_start}/grid_top.rb +2 -2
  160. data/screen_test/settings/content_start/grid_top_negative.frame +1 -0
  161. data/screen_test/settings/{position → content_start}/grid_top_negative.rb +2 -2
  162. data/screen_test/settings/flow/box_b2t.frame +1 -1
  163. data/screen_test/settings/flow/box_b2t.rb +1 -1
  164. data/screen_test/settings/flow/box_l2r.frame +1 -1
  165. data/screen_test/settings/flow/box_l2r.rb +1 -1
  166. data/screen_test/settings/flow/box_r2l.frame +1 -1
  167. data/screen_test/settings/flow/box_r2l.rb +1 -1
  168. data/screen_test/settings/flow/box_t2b.frame +1 -1
  169. data/screen_test/settings/flow/box_t2b.rb +1 -1
  170. data/screen_test/settings/flow/grid_b2t.frame +1 -1
  171. data/screen_test/settings/flow/grid_b2t.rb +1 -1
  172. data/screen_test/settings/flow/grid_l2r.frame +1 -1
  173. data/screen_test/settings/flow/grid_l2r.rb +1 -1
  174. data/screen_test/settings/flow/grid_r2l.frame +1 -1
  175. data/screen_test/settings/flow/grid_r2l.rb +1 -1
  176. data/screen_test/settings/flow/grid_t2b.frame +1 -1
  177. data/screen_test/settings/flow/grid_t2b.rb +1 -1
  178. data/screen_test/settings/height/box.frame +1 -1
  179. data/screen_test/settings/height/box.rb +1 -1
  180. data/screen_test/settings/height/box_border_sizing.frame +1 -1
  181. data/screen_test/settings/height/box_border_sizing.rb +1 -1
  182. data/screen_test/settings/height/grid.frame +1 -1
  183. data/screen_test/settings/height/grid.rb +1 -1
  184. data/screen_test/settings/height/overflow_box.frame +1 -1
  185. data/screen_test/settings/height/overflow_box.rb +1 -1
  186. data/screen_test/settings/height/overflow_box_l2r.frame +1 -1
  187. data/screen_test/settings/height/overflow_box_l2r.rb +1 -1
  188. data/screen_test/settings/height/overflow_box_t2b.frame +1 -1
  189. data/screen_test/settings/height/overflow_box_t2b.rb +1 -1
  190. data/screen_test/settings/height/overflow_grid.frame +1 -1
  191. data/screen_test/settings/height/overflow_grid.rb +1 -1
  192. data/screen_test/settings/margin.frame +1 -1
  193. data/screen_test/settings/margin.rb +1 -1
  194. data/screen_test/settings/padding.frame +1 -1
  195. data/screen_test/settings/padding.rb +1 -1
  196. data/screen_test/settings/scroll/horiz_box.frame +1 -1
  197. data/screen_test/settings/scroll/horiz_box.rb +3 -5
  198. data/screen_test/settings/scroll/horiz_box_align_center.rb +3 -5
  199. data/screen_test/settings/scroll/horiz_box_align_right.rb +3 -5
  200. data/screen_test/settings/scroll/vert_box.frame +1 -1
  201. data/screen_test/settings/scroll/vert_box.rb +6 -8
  202. data/screen_test/settings/title_font.frame +1 -1
  203. data/screen_test/settings/title_font.rb +1 -1
  204. data/screen_test/settings/valign/box_around.frame +1 -1
  205. data/screen_test/settings/valign/box_around.rb +1 -1
  206. data/screen_test/settings/valign/box_between.frame +1 -1
  207. data/screen_test/settings/valign/box_between.rb +1 -1
  208. data/screen_test/settings/valign/box_bottom.frame +1 -1
  209. data/screen_test/settings/valign/box_bottom.rb +1 -1
  210. data/screen_test/settings/valign/box_default.frame +1 -1
  211. data/screen_test/settings/valign/box_default.rb +1 -1
  212. data/screen_test/settings/valign/box_evenly.frame +1 -1
  213. data/screen_test/settings/valign/box_evenly.rb +1 -1
  214. data/screen_test/settings/valign/box_middle.frame +1 -1
  215. data/screen_test/settings/valign/box_middle.rb +1 -1
  216. data/screen_test/settings/valign/box_top.frame +1 -1
  217. data/screen_test/settings/valign/box_top.rb +1 -1
  218. data/screen_test/settings/valign/grid_bottom.frame +1 -1
  219. data/screen_test/settings/valign/grid_bottom.rb +1 -1
  220. data/screen_test/settings/valign/grid_default.frame +1 -1
  221. data/screen_test/settings/valign/grid_default.rb +1 -1
  222. data/screen_test/settings/valign/grid_middle.frame +1 -1
  223. data/screen_test/settings/valign/grid_middle.rb +1 -1
  224. data/screen_test/settings/valign/grid_top.frame +1 -1
  225. data/screen_test/settings/valign/grid_top.rb +1 -1
  226. data/screen_test/settings/width/box_border_sizing.frame +1 -1
  227. data/screen_test/settings/width/box_border_sizing.rb +1 -1
  228. data/screen_test/settings/width/box_content.frame +1 -1
  229. data/screen_test/settings/width/box_content.rb +1 -1
  230. data/screen_test/settings/width/box_default.frame +1 -1
  231. data/screen_test/settings/width/box_default.rb +1 -1
  232. data/screen_test/settings/width/grid.frame +1 -1
  233. data/screen_test/settings/width/grid.rb +1 -1
  234. data/screen_test/settings/width/overflow_align_center.frame +1 -1
  235. data/screen_test/settings/width/overflow_align_center.rb +1 -1
  236. data/screen_test/settings/width/overflow_align_right.frame +1 -1
  237. data/screen_test/settings/width/overflow_align_right.rb +1 -1
  238. data/screen_test/settings/width/overflow_box.frame +1 -1
  239. data/screen_test/settings/width/overflow_box.rb +1 -1
  240. data/screen_test/settings/width/overflow_box_l2r.frame +1 -1
  241. data/screen_test/settings/width/overflow_box_l2r.rb +1 -1
  242. data/screen_test/settings/width/overflow_box_t2b.frame +1 -1
  243. data/screen_test/settings/width/overflow_box_t2b.rb +1 -1
  244. data/screen_test/settings/width/overflow_grid.frame +1 -1
  245. data/screen_test/settings/width/overflow_grid.rb +1 -1
  246. data/tools/whirled_peas/tools/screen_tester.rb +131 -65
  247. metadata +86 -25
  248. data/lib/whirled_peas/frame.rb +0 -6
  249. data/lib/whirled_peas/frame/consumer.rb +0 -30
  250. data/lib/whirled_peas/frame/debug_consumer.rb +0 -30
  251. data/lib/whirled_peas/frame/event_loop.rb +0 -90
  252. data/lib/whirled_peas/frame/producer.rb +0 -67
  253. data/lib/whirled_peas/graphics/screen.rb +0 -70
  254. data/lib/whirled_peas/graphics/text_dimensions.rb +0 -15
  255. data/screen_test/settings/position/box_left.frame +0 -1
  256. data/screen_test/settings/position/box_left_negative.frame +0 -1
  257. data/screen_test/settings/position/box_top.frame +0 -1
  258. data/screen_test/settings/position/box_top_negative.frame +0 -1
  259. data/screen_test/settings/position/grid_left.frame +0 -1
  260. data/screen_test/settings/position/grid_left_negative.frame +0 -1
  261. data/screen_test/settings/position/grid_top.frame +0 -1
  262. data/screen_test/settings/position/grid_top_negative.frame +0 -1
@@ -0,0 +1,88 @@
1
+ module WhirledPeas
2
+ module Component
3
+ class ListWithActive
4
+ attr_accessor :active_index, :active_color, :active_bg_color, :separator
5
+
6
+ attr_reader :items
7
+
8
+ attr_writer :flow, :viewport_size
9
+
10
+ def items=(value)
11
+ @items = value.map(&:to_s)
12
+ end
13
+
14
+ def flow
15
+ @flow || :l2r
16
+ end
17
+
18
+ def total_size
19
+ @total_size ||= if flow == :l2r
20
+ size = separator.nil? ? 0 : (items.count - 1) * separator.length
21
+ items.each { |item| size += item.length }
22
+ size
23
+ else
24
+ items.count + (separator.nil? ? 0 : items.count - 1)
25
+ end
26
+ end
27
+
28
+ def viewport_size
29
+ @viewport_size || total_size
30
+ end
31
+
32
+ def compose(composer, settings)
33
+ %i[items active_index].each do |required_attr|
34
+ if send(required_attr).nil?
35
+ raise ArugmentError, "Require field #{required_attr} missing"
36
+ end
37
+ end
38
+
39
+ composer.add_box('ListWithActive') do |composer, settings|
40
+ settings.flow = flow
41
+ settings.align = :left
42
+ settings.full_border
43
+ if flow == :l2r
44
+ settings.width = viewport_size
45
+ active_start = separator.nil? ? 0 : active_index * separator.length
46
+ items.first(active_index).each do |item|
47
+ active_start += item.length
48
+ end
49
+ active_size = items[active_index].length
50
+ else
51
+ settings.height = viewport_size
52
+ active_start = active_index + (separator.nil? ? 0 : active_index)
53
+ active_size = 1
54
+ end
55
+
56
+ if viewport_size < total_size
57
+ front_padding = (viewport_size - active_size) * 0.667
58
+ offset = (active_start - front_padding).round
59
+ if offset < 0
60
+ offset = 0
61
+ elsif offset > total_size - viewport_size
62
+ offset = total_size - viewport_size
63
+ end
64
+
65
+ if flow == :l2r
66
+ settings.content_start.left = -offset
67
+ settings.scrollbar.horiz = true
68
+ else
69
+ settings.content_start.top = -offset
70
+ settings.scrollbar.vert = true
71
+ end
72
+ end
73
+
74
+ items.each.with_index do |item, index|
75
+ composer.add_text { separator } if !separator.nil? && index > 0
76
+ composer.add_text do |_, settings|
77
+ if index == active_index
78
+ settings.bg_color = active_bg_color
79
+ settings.color = active_color
80
+ end
81
+ item
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
@@ -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
@@ -55,8 +55,8 @@ module WhirledPeas
55
55
  private
56
56
 
57
57
  def paint_horizontally(canvas, canvas_coords, &block)
58
- stroke_left = canvas_coords.content_left
59
- stroke_top = canvas_coords.content_top
58
+ stroke_left = canvas_coords.offset_content_left
59
+ stroke_top = canvas_coords.offset_content_top
60
60
  children_width = 0
61
61
  each_child { |c| children_width += c.dimensions.outer_width }
62
62
  left_offset, spacing_offset = horiz_justify_offset(children_width)
@@ -76,8 +76,8 @@ module WhirledPeas
76
76
  end
77
77
 
78
78
  def paint_vertically(canvas, canvas_coords, &block)
79
- stroke_left = canvas_coords.content_left
80
- stroke_top = canvas_coords.content_top
79
+ stroke_left = canvas_coords.offset_content_left
80
+ stroke_top = canvas_coords.offset_content_top
81
81
  children_height = 0
82
82
  each_child { |c| children_height += c.dimensions.outer_height }
83
83
  top_offset, spacing_offset = vert_justify_offset(children_height)
@@ -1,8 +1,11 @@
1
1
  require 'whirled_peas/settings/box_settings'
2
+ require 'whirled_peas/settings/graph_settings'
2
3
  require 'whirled_peas/settings/grid_settings'
3
4
  require 'whirled_peas/settings/text_settings'
5
+ require 'whirled_peas/settings/theme_library'
4
6
 
5
7
  require_relative 'box_painter'
8
+ require_relative 'graph_painter'
6
9
  require_relative 'grid_painter'
7
10
  require_relative 'text_painter'
8
11
 
@@ -23,8 +26,10 @@ module WhirledPeas
23
26
  "Element-#{@counter}"
24
27
  end
25
28
 
26
- def self.build
27
- settings = Settings::BoxSettings.new
29
+ def self.build(theme_name=nil, &block)
30
+ theme_name ||= Settings::ThemeLibrary.default_name
31
+ theme = Settings::ThemeLibrary.get(theme_name)
32
+ settings = Settings::BoxSettings.new(theme)
28
33
  template = BoxPainter.new('TEMPLATE', settings)
29
34
  composer = Composer.new(template)
30
35
  value = yield composer, settings
@@ -45,6 +50,7 @@ module WhirledPeas
45
50
  child = TextPainter.new(name, child_settings)
46
51
  # TextPainters are not composable, so yield nil
47
52
  content = yield nil, child_settings
53
+ child_settings.validate!
48
54
  unless self.class.stringable?(content)
49
55
  raise ArgumentError, "Unsupported type for text: #{content.class}"
50
56
  end
@@ -52,11 +58,29 @@ module WhirledPeas
52
58
  painter.add_child(child)
53
59
  end
54
60
 
61
+ def add_graph(name=self.class.next_name, &block)
62
+ child_settings = Settings::GraphSettings.inherit(painter.settings)
63
+ child = GraphPainter.new(name, child_settings)
64
+ # GraphPainters are not composable, so yield nil
65
+ content = yield nil, child_settings
66
+ child_settings.validate!
67
+ unless content.is_a?(Array) && content.length > 0
68
+ raise ArgumentError, 'Graphs require a non-empty array as the content'
69
+ end
70
+ child.content = content
71
+ painter.add_child(child)
72
+ end
73
+
74
+ def add_component(component)
75
+ component.compose(self, settings)
76
+ end
77
+
55
78
  def add_box(name=self.class.next_name, &block)
56
79
  child_settings = Settings::BoxSettings.inherit(painter.settings)
57
80
  child = BoxPainter.new(name, child_settings)
58
81
  composer = self.class.new(child)
59
82
  value = yield composer, child.settings
83
+ child_settings.validate!
60
84
  painter.add_child(child)
61
85
  if !child.children? && self.class.stringable?(value)
62
86
  composer.add_text("#{name}-Text") { value.to_s }
@@ -68,6 +92,7 @@ module WhirledPeas
68
92
  child = GridPainter.new(name, child_settings)
69
93
  composer = self.class.new(child)
70
94
  values = yield composer, child.settings
95
+ child_settings.validate!
71
96
  painter.add_child(child)
72
97
  if !child.children? && values.is_a?(Array)
73
98
  values.each.with_index do |value, index|
@@ -9,11 +9,11 @@ module WhirledPeas
9
9
  end
10
10
 
11
11
  def left
12
- start_left + settings.position.left
12
+ start_left
13
13
  end
14
14
 
15
15
  def top
16
- start_top + settings.position.top
16
+ start_top
17
17
  end
18
18
 
19
19
  def border_left
@@ -36,10 +36,32 @@ module WhirledPeas
36
36
  padding_left + settings.padding.left + col_index * grid_width
37
37
  end
38
38
 
39
+ def offset_content_left(col_index=0)
40
+ if settings.content_start.left
41
+ content_left(col_index) + settings.content_start.left
42
+ elsif settings.content_start.right
43
+ left_offset = dimensions.content_width - dimensions.children_width
44
+ content_left(col_index) + left_offset - settings.content_start.right
45
+ else
46
+ content_left(col_index)
47
+ end
48
+ end
49
+
39
50
  def content_top(row_index=0)
40
51
  padding_top + settings.padding.top + row_index * grid_height
41
52
  end
42
53
 
54
+ def offset_content_top(row_index=0)
55
+ if settings.content_start.top
56
+ content_top(row_index) + settings.content_start.top
57
+ elsif settings.content_start.bottom
58
+ top_offset = dimensions.content_height - dimensions.children_height
59
+ content_top(row_index) + top_offset - settings.content_start.bottom
60
+ else
61
+ content_top(row_index)
62
+ end
63
+ end
64
+
43
65
  def inner_grid_width
44
66
  settings.padding.left +
45
67
  dimensions.content_width +
@@ -2,9 +2,12 @@ require 'whirled_peas/utils/formatted_string'
2
2
 
3
3
  require_relative 'container_coords'
4
4
  require_relative 'painter'
5
+ require_relative 'scrollbar_helper'
5
6
 
6
7
  module WhirledPeas
7
8
  module Graphics
9
+ # Abstract Painter for containers. Containers (as the name implies) contain other child
10
+ # elements and must delegate painting of the children to the children themselves.
8
11
  class ContainerPainter < Painter
9
12
  PADDING = ' '
10
13
 
@@ -13,38 +16,65 @@ module WhirledPeas
13
16
  @children = []
14
17
  end
15
18
 
19
+ # Paint the common attributes of containers (e.g. border and background color). Any
20
+ # class that inherits from this one should call `super` at the start of its #paint
21
+ # method, before painting its children.
16
22
  def paint(canvas, left, top, &block)
17
23
  return unless canvas.writable?
18
24
  return unless needs_printing?
19
25
  canvas_coords = coords(left, top)
26
+
27
+ # Paint the border, background color, and scrollbar starting from the top left
28
+ # border position, moving down row by row until we reach the bottom border
29
+ # position
20
30
  stroke_left = canvas_coords.border_left
21
31
  stroke_top = canvas_coords.border_top
32
+
33
+ # All strokes will have the same formatting options
22
34
  formatting = [*settings.border.color, *settings.bg_color]
35
+
36
+ # Paint the top border if the settings call for it
23
37
  if settings.border.top?
24
38
  canvas.stroke(stroke_left, stroke_top, top_border_stroke(canvas_coords), formatting, &block)
25
39
  stroke_top += 1
26
40
  end
41
+ # Precalculate the middle border container grids with more than 1 row
27
42
  middle_border = dimensions.num_rows > 1 ? middle_border_stroke(canvas_coords) : ''
43
+
44
+ # Paint each grid row by row
28
45
  dimensions.num_rows.times do |row_num|
46
+ # In a grid with N rows, we will need to paint N - 1 inner horizontal borders.
47
+ # This code treats the inner horizontal border as the top of each row except for
48
+ # the first one.
29
49
  if row_num > 0 && settings.border.inner_horiz?
30
50
  canvas.stroke(stroke_left, stroke_top, middle_border, formatting, &block)
31
51
  stroke_top += 1
32
52
  end
53
+
54
+ # Paint the interior of each row (horizontal borders, veritical scroll bar and
55
+ # background color for the padding and content area)
33
56
  canvas_coords.inner_grid_height.times do |row_within_cell|
34
57
  canvas.stroke(stroke_left, stroke_top, content_line_stroke(canvas_coords, row_within_cell), formatting, &block)
35
58
  stroke_top += 1
36
59
  end
60
+
61
+ # Paint the horizontal scroll bar is the settings call for it
37
62
  if settings.scrollbar.horiz?
38
63
  canvas.stroke(stroke_left, stroke_top, bottom_scroll_stroke(canvas_coords), formatting, &block)
39
64
  stroke_top += 1
40
65
  end
41
66
  end
67
+
68
+ # Paint the bottom border if the settings call for it
42
69
  if settings.border.bottom?
43
70
  canvas.stroke(stroke_left, stroke_top, bottom_border_stroke(canvas_coords), formatting, &block)
44
71
  stroke_top += 1
45
72
  end
46
73
  end
47
74
 
75
+ # Tightly manage access to the children (rather than simply exposing the underlying
76
+ # array). This allows subclasses to easily modify behavior based on that element's
77
+ # specific settings.
48
78
  def add_child(child)
49
79
  children << child
50
80
  end
@@ -65,6 +95,8 @@ module WhirledPeas
65
95
 
66
96
  attr_reader :children
67
97
 
98
+ # Determine if there is anything to print for the container (this does not accont for
99
+ # children, just the border, scrollbar, and background color)
68
100
  def needs_printing?
69
101
  return true if settings.bg_color
70
102
  return true if settings.border.outer?
@@ -73,10 +105,15 @@ module WhirledPeas
73
105
  settings.scrollbar.horiz? || settings.scrollbar.vert?
74
106
  end
75
107
 
108
+ # Return an object that allows easy access to important coordinates within the container,
109
+ # e.g. the left position where the left border is printed
76
110
  def coords(left, top)
77
111
  ContainerCoords.new(dimensions, settings, left, top)
78
112
  end
79
113
 
114
+ # @return [Array<Integer>] a two-item array, the first being the amount of horizontal
115
+ # spacing to paint *before the first* child and the second being the amount of spacing
116
+ # to paint *between each* child
80
117
  def horiz_justify_offset(containing_width)
81
118
  if settings.align_center?
82
119
  [(dimensions.content_width - containing_width) / 2, 0]
@@ -96,6 +133,9 @@ module WhirledPeas
96
133
  end
97
134
  end
98
135
 
136
+ # @return [Array<Integer>] a two-item array, the first being the amount of vertical
137
+ # spacing to paint *above the first* child and the second being the amount of spacing
138
+ # to paint *between each* child
99
139
  def vert_justify_offset(containing_height)
100
140
  if settings.valign_middle?
101
141
  [(dimensions.content_height - containing_height) / 2, 0]
@@ -115,6 +155,16 @@ module WhirledPeas
115
155
  end
116
156
  end
117
157
 
158
+ # Return a stroke for one line of the container
159
+ #
160
+ # @param left_border [String] the character to print as the first character if there
161
+ # is a left border
162
+ # @param junc_border [String] the character to print as the junction between two grid
163
+ # columns if there is an inner vertical border
164
+ # @param right_border [String] the character to print as the last character if there
165
+ # is a right border
166
+ # @block [String] the block should yield a string that represents the interior
167
+ # (including padding) of a grid cell
118
168
  def line_stroke(left_border, junc_border, right_border, &block)
119
169
  stroke = ''
120
170
  stroke += left_border if settings.border.left?
@@ -126,6 +176,7 @@ module WhirledPeas
126
176
  stroke
127
177
  end
128
178
 
179
+ # Return the stroke for the top border
129
180
  def top_border_stroke(canvas_coords)
130
181
  line_stroke(
131
182
  settings.border.style.top_left,
@@ -136,6 +187,7 @@ module WhirledPeas
136
187
  end
137
188
  end
138
189
 
190
+ # Return the stroke for an inner horizontal border
139
191
  def middle_border_stroke(canvas_coords)
140
192
  line_stroke(
141
193
  settings.border.style.left_junc,
@@ -146,6 +198,7 @@ module WhirledPeas
146
198
  end
147
199
  end
148
200
 
201
+ # Return the stroke for the bottom border
149
202
  def bottom_border_stroke(canvas_coords)
150
203
  line_stroke(
151
204
  settings.border.style.bottom_left,
@@ -156,6 +209,7 @@ module WhirledPeas
156
209
  end
157
210
  end
158
211
 
212
+ # Return the stroke for a grid row between any borders
159
213
  def content_line_stroke(canvas_coords, row_within_cell)
160
214
  line_stroke(
161
215
  settings.border.style.left_vert,
@@ -163,16 +217,12 @@ module WhirledPeas
163
217
  settings.border.style.right_vert,
164
218
  ) do
165
219
  if settings.scrollbar.vert?
166
- if dimensions.children_height <= canvas_coords.grid_height || children.first.settings.position.top > 0
167
- scrollbar_char = GUTTER
168
- else
169
- scrollbar_char = vert_scroll_char(
170
- dimensions.children_height + dimensions.padding_height,
171
- canvas_coords.inner_grid_height,
172
- -children.first.settings.position.top,
173
- row_within_cell
174
- )
175
- end
220
+ scrollbar_char = ScrollbarHelper.vert_char(
221
+ dimensions.children_height + dimensions.padding_height,
222
+ canvas_coords.inner_grid_height,
223
+ canvas_coords.top - canvas_coords.offset_content_top,
224
+ row_within_cell
225
+ )
176
226
  PADDING * canvas_coords.inner_grid_width + scrollbar_char
177
227
  else
178
228
  PADDING * canvas_coords.inner_grid_width
@@ -180,6 +230,7 @@ module WhirledPeas
180
230
  end
181
231
  end
182
232
 
233
+ # Return the stroke for the horizontal scroll bar
183
234
  def bottom_scroll_stroke(canvas_coords)
184
235
  line_stroke(
185
236
  settings.border.style.left_vert,
@@ -187,85 +238,15 @@ module WhirledPeas
187
238
  settings.border.style.right_vert,
188
239
  ) do
189
240
  canvas_coords.inner_grid_width.times.map do |col_within_cell|
190
- horiz_scroll_char(
241
+ ScrollbarHelper.horiz_char(
191
242
  dimensions.children_width + dimensions.padding_width,
192
243
  canvas_coords.inner_grid_width,
193
- -children.first.settings.position.left,
244
+ canvas_coords.left - canvas_coords.offset_content_left,
194
245
  col_within_cell
195
246
  )
196
247
  end.join
197
248
  end
198
249
  end
199
-
200
- GUTTER = ' '
201
- HORIZONTAL = %w[▗ ▄ ▖]
202
- VERTICAL = %w[
203
-
204
-
205
-
206
- ]
207
-
208
- def horiz_scroll_char(col_count, viewable_col_count, first_visible_col, curr_col)
209
- scroll_char(col_count, viewable_col_count, first_visible_col, curr_col, HORIZONTAL)
210
- end
211
-
212
- def vert_scroll_char(row_count, viewable_row_count, first_visible_row, curr_row)
213
- scroll_char(row_count, viewable_row_count, first_visible_row, curr_row, VERTICAL)
214
- end
215
-
216
- private
217
-
218
- def scroll_char(total_count, viewable_count, first_visible, curr, chars)
219
- return GUTTER unless total_count > 0 && viewable_count > 0
220
- # The scroll handle has the exact same relative size and position in the scroll gutter
221
- # that the viewable content has in the total content area. For example, a content area
222
- # that is 50 columns wide with a view port that is 20 columns wide might look like
223
- #
224
- # +---------1-----****2*********3******---4---------+
225
- # | * * |
226
- # | hidden * viewable * hidden |
227
- # | * * |
228
- # +---------1-----****2*********3******---4---------+
229
- #
230
- # The scoll gutter, would look like
231
- #
232
- # |......********.....|
233
- #
234
- # Scrolling all the way to the right results in
235
- #
236
- # +---------1---------2---------3*********4*********+
237
- # | * *
238
- # | hidden * viewable *
239
- # | * *
240
- # +---------1---------2---------3*********4*********+
241
- # |...........********|
242
-
243
- # The first task of determining how much of the handle is visible in a row/column is to
244
- # calculate the range (as a precentage of the total) of viewable items
245
- viewable_start = first_visible.to_f / total_count
246
- viewable_end = (first_visible + viewable_count).to_f / total_count
247
-
248
- # Always use the same length for the scroll bar so it does not give an inchworm effect
249
- # as it scrolls along.
250
- #
251
- # Also, double the value now to get granularity for half width
252
- # scrollbar characters.
253
- scrollbar_length = ((2 * viewable_count ** 2).to_f / total_count).ceil
254
- scrollbar_start = ((2 * first_visible * viewable_count).to_f / total_count).floor
255
-
256
- first_half = (scrollbar_start...scrollbar_start + scrollbar_length).include?(2 * curr)
257
- second_half = (scrollbar_start...scrollbar_start + scrollbar_length).include?(2 * curr + 1)
258
-
259
- if first_half && second_half
260
- chars[1]
261
- elsif first_half
262
- chars[2]
263
- elsif second_half
264
- chars[0]
265
- else
266
- GUTTER
267
- end
268
- end
269
250
  end
270
251
  private_constant :ContainerPainter
271
252
  end