whirled_peas 0.7.1 → 0.11.0

Sign up to get free protection for your applications and to get access to all the features.
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