hokusai-zero 0.1.1

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 (166) hide show
  1. checksums.yaml +7 -0
  2. data/Dockerfile +26 -0
  3. data/Gemfile +15 -0
  4. data/Gemfile.lock +91 -0
  5. data/LICENSE +21 -0
  6. data/README.md +28 -0
  7. data/ast/genheader +3 -0
  8. data/ast/include/hashmap.c +1151 -0
  9. data/ast/include/hashmap.c.license +20 -0
  10. data/ast/include/hashmap.h +54 -0
  11. data/ast/src/core/ast.c +448 -0
  12. data/ast/src/core/ast.h +259 -0
  13. data/ast/src/core/common.h +24 -0
  14. data/ast/src/core/component.c +85 -0
  15. data/ast/src/core/component.h +35 -0
  16. data/ast/src/core/hml.c +665 -0
  17. data/ast/src/core/hml.h +11 -0
  18. data/ast/src/core/input.c +458 -0
  19. data/ast/src/core/input.h +118 -0
  20. data/ast/src/core/style.c +101 -0
  21. data/ast/src/core/style.h +41 -0
  22. data/ast/src/core/text.c +784 -0
  23. data/ast/src/core/text.h +93 -0
  24. data/ast/src/core/util.c +140 -0
  25. data/ast/src/core/util.h +48 -0
  26. data/ast/src/hokusai.c +6 -0
  27. data/ast/src/hokusai.h +6 -0
  28. data/ast/test/fixtures/test.ui +13 -0
  29. data/ast/test/greatest.h +1266 -0
  30. data/ast/test/hokusai.c +14 -0
  31. data/ast/test/parser.c +234 -0
  32. data/ast/test/text.c +116 -0
  33. data/ext/extconf.rb +27 -0
  34. data/grammar/Cargo.lock +80 -0
  35. data/grammar/Cargo.toml +26 -0
  36. data/grammar/binding.gyp +20 -0
  37. data/grammar/bindings/node/binding.cc +28 -0
  38. data/grammar/bindings/node/index.js +19 -0
  39. data/grammar/bindings/rust/build.rs +40 -0
  40. data/grammar/bindings/rust/lib.rs +52 -0
  41. data/grammar/corpus/1_document.txt +131 -0
  42. data/grammar/corpus/2_selectors.txt +58 -0
  43. data/grammar/corpus/3_spaces.txt +69 -0
  44. data/grammar/corpus/4_errors.txt +10 -0
  45. data/grammar/corpus/5_macros.txt +175 -0
  46. data/grammar/corpus/6_styles.txt +81 -0
  47. data/grammar/grammar.js +275 -0
  48. data/grammar/package-lock.json +34 -0
  49. data/grammar/package.json +33 -0
  50. data/grammar/src/grammar.json +1269 -0
  51. data/grammar/src/node-types.json +474 -0
  52. data/grammar/src/parser.c +5772 -0
  53. data/grammar/src/scanner.c +258 -0
  54. data/grammar/src/tree_sitter/parser.h +230 -0
  55. data/grammar/src/tree_sitter/scanner.h +12 -0
  56. data/grammar/test.nml +10 -0
  57. data/hokusai.gemspec +19 -0
  58. data/ui/examples/assets/DigitalDisplay.ttf +0 -0
  59. data/ui/examples/assets/OpenSans-Regular.ttf +0 -0
  60. data/ui/examples/assets/addy.png +0 -0
  61. data/ui/examples/assets/baby_sean.png +0 -0
  62. data/ui/examples/assets/football-troll.png +0 -0
  63. data/ui/examples/assets/gear.png +0 -0
  64. data/ui/examples/assets/icecold.ttf +0 -0
  65. data/ui/examples/assets/science-troll.png +0 -0
  66. data/ui/examples/buddy.rb +31 -0
  67. data/ui/examples/clock.rb +58 -0
  68. data/ui/examples/counter.rb +123 -0
  69. data/ui/examples/dynamic.rb +147 -0
  70. data/ui/examples/foobar.rb +236 -0
  71. data/ui/examples/stock.rb +115 -0
  72. data/ui/examples/stock_decider/option.rb +74 -0
  73. data/ui/examples/tic_tac_toe.rb +246 -0
  74. data/ui/lib/lib_hokusai.rb +425 -0
  75. data/ui/spec/hokusai/ast_spec.rb +88 -0
  76. data/ui/spec/hokusai/automation/keys_transcoder_spec.rb +50 -0
  77. data/ui/spec/hokusai/automation/selector_spec.rb +68 -0
  78. data/ui/spec/hokusai/block_spec.rb +126 -0
  79. data/ui/spec/hokusai/directives_spec.rb +327 -0
  80. data/ui/spec/hokusai/e2e/client_spec.rb +58 -0
  81. data/ui/spec/hokusai/e2e/meta_spec.rb +42 -0
  82. data/ui/spec/hokusai/publisher_spec.rb +38 -0
  83. data/ui/spec/hokusai/slots_spec.rb +150 -0
  84. data/ui/spec/hokusai/util/piece_table_spec.rb +90 -0
  85. data/ui/spec/hokusai_spec.rb +0 -0
  86. data/ui/spec/spec_helper.rb +30 -0
  87. data/ui/src/hokusai/ast.rb +446 -0
  88. data/ui/src/hokusai/automation/client.rb +167 -0
  89. data/ui/src/hokusai/automation/constants.rb +98 -0
  90. data/ui/src/hokusai/automation/converters/selector_converter.rb +61 -0
  91. data/ui/src/hokusai/automation/driver.rb +54 -0
  92. data/ui/src/hokusai/automation/driver_command_queue.rb +50 -0
  93. data/ui/src/hokusai/automation/driver_commands/base.rb +79 -0
  94. data/ui/src/hokusai/automation/driver_commands/get_attribute.rb +41 -0
  95. data/ui/src/hokusai/automation/driver_commands/invoke.rb +33 -0
  96. data/ui/src/hokusai/automation/driver_commands/locate.rb +48 -0
  97. data/ui/src/hokusai/automation/driver_commands/trigger_keyboard.rb +94 -0
  98. data/ui/src/hokusai/automation/driver_commands/trigger_mouse.rb +213 -0
  99. data/ui/src/hokusai/automation/keys_transcoder.rb +128 -0
  100. data/ui/src/hokusai/automation/selector.rb +39 -0
  101. data/ui/src/hokusai/automation/server.rb +114 -0
  102. data/ui/src/hokusai/automation.rb +3 -0
  103. data/ui/src/hokusai/backends/raylib/config.rb +47 -0
  104. data/ui/src/hokusai/backends/raylib/font.rb +113 -0
  105. data/ui/src/hokusai/backends/raylib/keys.rb +124 -0
  106. data/ui/src/hokusai/backends/raylib.rb +449 -0
  107. data/ui/src/hokusai/backends/sdl2/Monaco.ttf +0 -0
  108. data/ui/src/hokusai/backends/sdl2/color.rb +12 -0
  109. data/ui/src/hokusai/backends/sdl2/config.rb +31 -0
  110. data/ui/src/hokusai/backends/sdl2/font.rb +127 -0
  111. data/ui/src/hokusai/backends/sdl2/keys.rb +119 -0
  112. data/ui/src/hokusai/backends/sdl2.rb +529 -0
  113. data/ui/src/hokusai/block.rb +237 -0
  114. data/ui/src/hokusai/blocks/button.rb +100 -0
  115. data/ui/src/hokusai/blocks/checkbox.rb +51 -0
  116. data/ui/src/hokusai/blocks/circle.rb +28 -0
  117. data/ui/src/hokusai/blocks/clipped.rb +23 -0
  118. data/ui/src/hokusai/blocks/cursor.rb +49 -0
  119. data/ui/src/hokusai/blocks/dynamic.rb +37 -0
  120. data/ui/src/hokusai/blocks/empty.rb +10 -0
  121. data/ui/src/hokusai/blocks/hblock.rb +35 -0
  122. data/ui/src/hokusai/blocks/image.rb +18 -0
  123. data/ui/src/hokusai/blocks/input.rb +200 -0
  124. data/ui/src/hokusai/blocks/label.rb +39 -0
  125. data/ui/src/hokusai/blocks/panel.rb +126 -0
  126. data/ui/src/hokusai/blocks/rect.rb +24 -0
  127. data/ui/src/hokusai/blocks/scissor_begin.rb +18 -0
  128. data/ui/src/hokusai/blocks/scissor_end.rb +12 -0
  129. data/ui/src/hokusai/blocks/scrollbar.rb +103 -0
  130. data/ui/src/hokusai/blocks/selectable.rb +77 -0
  131. data/ui/src/hokusai/blocks/svg.rb +20 -0
  132. data/ui/src/hokusai/blocks/text.rb +214 -0
  133. data/ui/src/hokusai/blocks/titlebar/osx.rb +145 -0
  134. data/ui/src/hokusai/blocks/toggle.rb +55 -0
  135. data/ui/src/hokusai/blocks/vblock.rb +35 -0
  136. data/ui/src/hokusai/commands/base.rb +22 -0
  137. data/ui/src/hokusai/commands/circle.rb +47 -0
  138. data/ui/src/hokusai/commands/image.rb +45 -0
  139. data/ui/src/hokusai/commands/rect.rb +158 -0
  140. data/ui/src/hokusai/commands/scissor.rb +22 -0
  141. data/ui/src/hokusai/commands/text.rb +92 -0
  142. data/ui/src/hokusai/commands.rb +87 -0
  143. data/ui/src/hokusai/diff.rb +124 -0
  144. data/ui/src/hokusai/error.rb +3 -0
  145. data/ui/src/hokusai/event.rb +54 -0
  146. data/ui/src/hokusai/events/keyboard.rb +84 -0
  147. data/ui/src/hokusai/events/mouse.rb +172 -0
  148. data/ui/src/hokusai/font.rb +280 -0
  149. data/ui/src/hokusai/meta.rb +152 -0
  150. data/ui/src/hokusai/mounting/loop_entry.rb +230 -0
  151. data/ui/src/hokusai/mounting/mount_entry.rb +74 -0
  152. data/ui/src/hokusai/mounting/update_entry.rb +101 -0
  153. data/ui/src/hokusai/node.rb +98 -0
  154. data/ui/src/hokusai/node_mounter.rb +102 -0
  155. data/ui/src/hokusai/painter.rb +214 -0
  156. data/ui/src/hokusai/publisher.rb +32 -0
  157. data/ui/src/hokusai/style.rb +72 -0
  158. data/ui/src/hokusai/types.rb +266 -0
  159. data/ui/src/hokusai/util/clamping_iterator.rb +202 -0
  160. data/ui/src/hokusai/util/piece_table.rb +111 -0
  161. data/ui/src/hokusai/util/selection.rb +145 -0
  162. data/ui/src/hokusai.rb +120 -0
  163. data/ui/vendor/.gitkeep +0 -0
  164. data/vendor/.gitkeep +0 -0
  165. data/xmake.lua +192 -0
  166. metadata +222 -0
@@ -0,0 +1,123 @@
1
+ require_relative "../src/hokusai"
2
+ require_relative "../src/hokusai/backends/raylib"
3
+
4
+ class Counter < Hokusai::Block
5
+ style <<~EOF
6
+ [style]
7
+ additionStyles {
8
+ background: rgb(214, 49, 24);
9
+ rounding: 0.0;
10
+ outline: outline(1,4,4,1);
11
+ outline_color: rgb(216, 26, 137);
12
+ }
13
+
14
+ additionLabel {
15
+ size: 40;
16
+ color: rgb(255,255,255);
17
+ }
18
+
19
+ subtractStyles {
20
+ background: rgb(0, 85, 170);
21
+ rounding: 0.0;
22
+ }
23
+
24
+ subtractLabel {
25
+ size: 50;
26
+ color: rgb(255,255,255);
27
+ }
28
+
29
+ scrollbar {
30
+ width: 25;
31
+ control_height: 100;
32
+ control_padding: 5;
33
+ control_rounding: 5;
34
+ }
35
+ EOF
36
+
37
+ template <<-EOF
38
+ [template]
39
+ hblock { @keypress="update_keys"}
40
+ vblock
41
+ hblock
42
+ label#count {
43
+ :content="count"
44
+ size="130"
45
+ :color="count_color"
46
+ }
47
+ hblock
48
+ vblock#add { ...additionStyles }
49
+ label {
50
+ content="Add"
51
+ @click="increment"
52
+ ...additionLabel
53
+ }
54
+ vblock#subtract { ...subtractStyles }
55
+ label {
56
+ content="Subtract"
57
+ @click="decrement"
58
+ ...subtractLabel
59
+ }
60
+ [if="count_positive"]
61
+ scrollbar { ...scrollbar }
62
+ EOF
63
+
64
+ uses(
65
+ vblock: Hokusai::Blocks::Vblock,
66
+ hblock: Hokusai::Blocks::Hblock,
67
+ label: Hokusai::Blocks::Label,
68
+ scrollbar: Hokusai::Blocks::Scrollbar,
69
+ image: Hokusai::Blocks::Image
70
+ )
71
+
72
+ attr_accessor :count, :keys
73
+
74
+ def count_positive
75
+ count > 0
76
+ end
77
+
78
+ def modal
79
+ !keys.empty?
80
+ end
81
+
82
+ def update_keys(event)
83
+ @keys << event.char
84
+ end
85
+
86
+ def increment(event)
87
+ self.count += 1
88
+ end
89
+
90
+ def decrement(event)
91
+ self.count -= 1
92
+ end
93
+
94
+ def count_color
95
+ self.count > 0 ? [0,0,255] : [255,0,0]
96
+ end
97
+
98
+ def initialize(**args)
99
+ @count = 0
100
+ @keys = ""
101
+ super(**args)
102
+ end
103
+ end
104
+
105
+ Hokusai::Backends::RaylibBackend.run(Counter) do |config|
106
+ # config.after_load do
107
+ # # Note: how to load a font...
108
+ # # Thank you to Vladimir Nikolic - https://www.1001fonts.com/users/vladimirnikolic/
109
+ # font = Hokusai::Backends::RaylibBackend::Font.from("#{__dir__}/icecold.ttf")
110
+ # Hokusai.fonts.register "icecold", font
111
+ # Hokusai.fonts.activate "icecold"
112
+ # end
113
+
114
+ config.after_load do
115
+ font = Hokusai::Backends::RaylibBackend::Font.from_ext("#{__dir__}/assets/OpenSans-Regular.ttf", 160)
116
+ Hokusai.fonts.register "opensans", font
117
+ Hokusai.fonts.activate "opensans"
118
+ end
119
+
120
+ config.width = 500
121
+ config.height = 500
122
+ config.title = "Counter application"
123
+ end
@@ -0,0 +1,147 @@
1
+ # dependencies
2
+ require_relative "../src/hokusai"
3
+ require_relative "../src/hokusai/backends/sdl2"
4
+ require_relative "../src/hokusai/backends/raylib"
5
+ require_relative "./stock"
6
+ require_relative "./tic_tac_toe"
7
+
8
+ module Dynamic
9
+ class Block < Hokusai::Block
10
+ template <<~EOF
11
+ [template]
12
+ empty
13
+ EOF
14
+
15
+ uses(empty: Hokusai::Blocks::Empty)
16
+
17
+ computed! :script
18
+
19
+ def on_mounted
20
+ klass = eval(script)
21
+
22
+ raise Hokusai::Error.new("Class #{klass} is not a Hokusai::Block") unless klass.ancestors.include?(Hokusai::Block)
23
+
24
+ node.meta.set_child(0, klass.mount)
25
+ end
26
+ end
27
+
28
+ class App < Hokusai::Block
29
+ style <<~EOF
30
+ [style]
31
+ mainStyle {
32
+ background: rgb(8, 8, 8);
33
+ }
34
+
35
+ appText {
36
+ content: "Address Lookup";
37
+ size: 30;
38
+ color: rgb(222,222,222);
39
+ }
40
+ EOF
41
+
42
+ template <<~EOF
43
+ [template]
44
+ vblock {
45
+ ...mainStyle
46
+ }
47
+ text { ...appText }
48
+ variable { :script="script" }
49
+ EOF
50
+
51
+ uses(
52
+ vblock: Hokusai::Blocks::Vblock,
53
+ text: Hokusai::Blocks::Text,
54
+ variable: Dynamic::Block,
55
+ )
56
+
57
+ def script
58
+ <<~RUBY
59
+ require "rest-client"
60
+ require "uri"
61
+
62
+ class TestDynamic < Hokusai::Block
63
+ style <<~EOF
64
+ [style]
65
+ inputStyle {
66
+ background: rgb(18, 18, 18);
67
+ text_color: rgb(244,244,244);
68
+ size: 25;
69
+ height: 80;
70
+ outline: outline(1,0,1,0);
71
+ outline_color: rgb(222,222,222);
72
+ }
73
+
74
+ textStyle {
75
+ color: rgb(225, 228, 224);
76
+ size: 23;
77
+ cursor: "manual";
78
+ padding: padding(5, 20, 0, 20);
79
+ markdown: true;
80
+ }
81
+
82
+ buttonStyle {
83
+ size: 20;
84
+ content: "Open in Maps";
85
+ }
86
+ EOF
87
+
88
+ template <<~EOF
89
+ [template]
90
+ input {
91
+ ...inputStyle
92
+ @change="geocode"
93
+ }
94
+ hblock
95
+ [if="response"]
96
+ panel
97
+ text {
98
+ ...textStyle
99
+ :content="content"
100
+ }
101
+ EOF
102
+
103
+ uses(
104
+ hblock: Hokusai::Blocks::Hblock,
105
+ text: Hokusai::Blocks::Text,
106
+ input: Hokusai::Blocks::Input,
107
+ panel: Hokusai::Blocks::Panel,
108
+ button: Hokusai::Blocks::Button
109
+ )
110
+
111
+ attr_reader :response
112
+
113
+ def geocode(address)
114
+ uri = "https://geoffrey.skinnyjames.net/api/v1/us/geocode/\#{URI.encode_uri_component(address)}"
115
+ @response = JSON.parse(RestClient.get(uri).body, symbolize_names: true)
116
+ end
117
+
118
+ def content
119
+ @response&.reduce("") do |memo, payload|
120
+ link = "https://maps.google.com/maps?q=\#{payload[:latitude]}+\#{payload[:longitude]}"
121
+
122
+ memo << "Address: [\#{payload[:address_number]} \#{payload[:street_name]} (\#{payload[:postal_community]}, \#{payload[:state]})](\#{link})\n"
123
+ memo << " **Latitude:** _\#{payload[:latitude]}_, **Longitude:** _\#{payload[:longitude]}_\n"
124
+ memo << " [Some link](https://google.com)\n\n"
125
+ end || "Placeholder"
126
+ end
127
+ end
128
+
129
+ TestDynamic
130
+ RUBY
131
+ end
132
+ end
133
+ end
134
+
135
+ Hokusai::Backends::SDLBackend.run(Dynamic::App) do |config|
136
+ # backend specific configuration
137
+ config.width = 600
138
+ config.height = 500
139
+ config.title = "Dynamic Application"
140
+
141
+ config.after_load do
142
+ # most heavy logic, including text wrapping calculations are implemented in C
143
+ font = Hokusai::Backends::SDLBackend::Font.from("#{__dir__}/assets/OpenSans-Regular.ttf")
144
+ Hokusai.fonts.register "opensans", font
145
+ Hokusai.fonts.activate "opensans"
146
+ end
147
+ end
@@ -0,0 +1,236 @@
1
+ # frozen_string_literal: true
2
+
3
+ # dependencies
4
+ require_relative "../src/hokusai"
5
+ require_relative "../src/hokusai/backends/sdl2"
6
+ require_relative "../src/hokusai/backends/raylib"
7
+ require_relative "./stock"
8
+ require_relative "./tic_tac_toe"
9
+
10
+ # can use ruby stdlib
11
+ # or any library in the ruby ecosystem
12
+ require "json"
13
+ require "net/http"
14
+
15
+ module Demo
16
+ # Block that represents a singular post
17
+ #
18
+ # Displays post content, and throws in a game of Tic Tac Toe for good measure.
19
+ #
20
+ # Takes up full width on small viewport, centers on large viewport
21
+ # Duplication in the template can easily be extracted into a separate block
22
+ class Post < Hokusai::Block
23
+ style <<~EOF
24
+ [style]
25
+ textStyle {
26
+ color: rgb(243, 243, 243);
27
+ }
28
+ EOF
29
+
30
+ template <<~EOF
31
+ [template]
32
+ hblock
33
+ vblock.about { width="80" :background="about_background" }
34
+ image {
35
+ width="80"
36
+ height="80"
37
+ :source="about_image_source"
38
+ }
39
+ label { ...textStyle :content="post_author_name" size="9" }
40
+ vblock.content
41
+ text { ...textStyle markdown="true" :content="post_title" size="20" }
42
+ text {
43
+ :content="post_body"
44
+ :padding="text_padding"
45
+ size="17
46
+ ...textStyle
47
+ @height_updated="text_height_updated"
48
+ }
49
+ tic_tac_toe { height="400" width="400" }
50
+ EOF
51
+
52
+ # Mandatory props
53
+ computed! :entry
54
+ computed! :index
55
+
56
+ # injects the screen type from the provisioned value
57
+ # and aliases it to :media_type
58
+ inject :screen_type, :media_type
59
+
60
+ uses(
61
+ hblock: Hokusai::Blocks::Hblock,
62
+ vblock: Hokusai::Blocks::Vblock,
63
+ image: Hokusai::Blocks::Image,
64
+ text: Hokusai::Blocks::Text,
65
+ label: Hokusai::Blocks::Label,
66
+ empty: Hokusai::Blocks::Empty,
67
+ tic_tac_toe: TicTacToe::App
68
+ )
69
+ #text { @height_updated="save_height" :padding="text_padding" :content="content" :color="label_color" }
70
+
71
+ # This block is rendered inside a scrollable panel
72
+ # in between a `clip begin` and `clip end` command
73
+ #
74
+ # `Hokusai.can_render(canvas) will tell us if this block will even be visible`
75
+ # If not visible, I won't waste memory / cpu to render
76
+ def render(canvas)
77
+ if Hokusai.can_render(canvas)
78
+ yield(canvas)
79
+ end
80
+ end
81
+
82
+ def small_viewport
83
+ media_type == :small
84
+ end
85
+
86
+ def post_index
87
+ index.to_s
88
+ end
89
+
90
+ def post_body
91
+ entry.content
92
+ end
93
+
94
+ def post_title
95
+ @post_title ||= "_#{entry.title.upcase}_ - #{DateTime.now.strftime("%m/%d/%Y %H:%M %p")}"
96
+ end
97
+
98
+ def post_author_name
99
+ @post_author_name ||= entry.id.even? ? "Adeline" : "Skinnyjames"
100
+ end
101
+
102
+ def about_image_source
103
+ @about_image_source ||= entry.id.even? ? "#{__dir__}/assets/addy.png" : "#{__dir__}/assets/baby_sean.png"
104
+ end
105
+
106
+ # Cache the color so that it isn't recomputing at `O(n) complexity`
107
+ #
108
+ # TODO: extract styling / prop declaration into separate template
109
+ def about_background
110
+ @author_background ||= Hokusai::Color.new(155,155,155,20)
111
+ end
112
+
113
+ # Cache the text padding
114
+ def text_padding
115
+ @text_padding ||= Hokusai::Padding.new(20, 5, 20, 5)
116
+ end
117
+
118
+ # `height_updated` handler for the text node
119
+ #
120
+ # Since panels are just blocks, and have no insight to the height of their children
121
+ # I will update the height of this post dynamically based on the height of the rendered text
122
+ def text_height_updated(height)
123
+ # tic tac toe height + about node height || content height + padding
124
+ @height = 400 + [80, height].max + 80
125
+ end
126
+
127
+ def on_mounted
128
+ text_height_updated(0)
129
+ end
130
+
131
+ # Lifecycle hook
132
+ #
133
+ # `height` and `width` are the only special props
134
+ # they are used by the layout rendered to compute the canvas
135
+ #
136
+ # We will set it after the block is updated on each iteration of the event loop
137
+ def after_updated
138
+ node.meta.set_prop(:height, @height) if @height != node.meta.get_prop(:height)
139
+ end
140
+ end
141
+
142
+ # Plain old ruby class
143
+ PostEntry = Struct.new(:id, :title, :content)
144
+
145
+ # Entrypoint Block
146
+ # Nothing is special about this block - any block can be used as an entrypoint
147
+ #
148
+ # NOTE: Scrollbars, panels, and other basic functions are also plain blocks
149
+ class App < Hokusai::Block
150
+ style <<~EOF
151
+ [style]
152
+ main {
153
+ background: rgb(34, 38, 57);
154
+ }
155
+
156
+ panelStyle {
157
+ scroll_color: rgb(30, 30, 163);
158
+ scroll_background: rgb(15, 11, 48);
159
+ }
160
+ EOF
161
+
162
+ template <<~EOF
163
+ [template]
164
+ vblock { ...main }
165
+ stock { height="150" }
166
+ panel { ...panelStyle }
167
+ [for="post in posts"]
168
+ post {
169
+ :key="key(post)"
170
+ :index="index"
171
+ :entry="post"
172
+ }
173
+ EOF
174
+
175
+ # keys map to template node names
176
+ # values map to blocks
177
+ uses(
178
+ hblock: Hokusai::Blocks::Hblock,
179
+ vblock: Hokusai::Blocks::Vblock,
180
+ label: Hokusai::Blocks::Label,
181
+ panel: Hokusai::Blocks::Panel,
182
+ text: Hokusai::Blocks::Text,
183
+ stock: StockDecider::App, # some app to chart stock prices for a friend
184
+ post: Post,
185
+ )
186
+
187
+ # methods can be accessed in computed props
188
+ attr_accessor :posts
189
+
190
+ # initializer override
191
+ def initialize(**args)
192
+ @posts = []
193
+
194
+ super
195
+ end
196
+
197
+ # loop state can be passed to methods
198
+ def key(entry)
199
+ entry.id
200
+ end
201
+
202
+ # lifecycle hook
203
+ # `on_mounted`
204
+ # `before_updated`
205
+ # `after_updated`
206
+ # `on_destroy`
207
+ def on_mounted
208
+ uri = URI("https://jsonplaceholder.typicode.com/posts")
209
+ res = JSON.parse(Net::HTTP.get(uri), symbolize_names: true)
210
+
211
+ self.posts = res.map { |json| PostEntry.new(json[:id], json[:title], json[:body]) }.freeze
212
+
213
+ # can access details about this block
214
+ #
215
+ # get the node count
216
+ puts node.meta.node_count
217
+ # show the ast
218
+ puts dump
219
+ end
220
+ end
221
+ end
222
+
223
+ # Backends include Raylib and SDL2
224
+ Hokusai::Backends::SDLBackend.run(Demo::App) do |config|
225
+ # backend specific configuration
226
+ config.width = 600
227
+ config.height = 500
228
+ config.title = "Demo Application"
229
+
230
+ config.after_load do
231
+ # most heavy logic, including text wrapping calculations are implemented in C
232
+ font = Hokusai::Backends::SDLBackend::Font.from("#{__dir__}/assets/OpenSans-Regular.ttf")
233
+ Hokusai.fonts.register "opensans", font
234
+ Hokusai.fonts.activate "opensans"
235
+ end
236
+ end
@@ -0,0 +1,115 @@
1
+ require_relative "../src/hokusai"
2
+ require_relative "../src/hokusai/backends/raylib"
3
+ require_relative "../src/hokusai/backends/sdl2"
4
+ require_relative "./stock_decider/option"
5
+
6
+ # aim for over 24 percent sort by percentage first
7
+ # 3 week = 1 option premium * 100 shares / 12
8
+ module StockDecider
9
+ # Expecting a data prop in the form of
10
+ #
11
+ class StackedBar < Hokusai::Block
12
+ COLORS = {
13
+ :week_3 => [157, 221, 117],
14
+ :week_4 => [117, 157, 221],
15
+ :week_5 => [221, 180, 117]
16
+ }
17
+
18
+ template <<~EOF
19
+ [template]
20
+ virtual
21
+ EOF
22
+
23
+ uses(
24
+ vblock: Hokusai::Blocks::Vblock,
25
+ label: Hokusai::Blocks::Label
26
+ )
27
+
28
+ computed! :data
29
+
30
+ def symbol
31
+ data[0]
32
+ end
33
+
34
+ def render(canvas)
35
+ key, weeks = data
36
+ theight = canvas.height - 50
37
+ draw do
38
+ weeks.reject(&:nil?).sort { |a, b| b <=> a }.each_with_index do |percent, idx|
39
+ color = COLORS["week_#{idx + 3}".to_sym]
40
+ height = (theight * percent) / 100
41
+ y = canvas.y + theight - height
42
+
43
+ rect(canvas.x, y , 150, height) do |command|
44
+ command.color = Hokusai::Color.convert(color)
45
+ command.outline = Hokusai::Outline.convert("4,4, 0, 0")
46
+ command.outline_color = Hokusai::Color.convert("0,0,0,110")
47
+ end
48
+
49
+ text("Week #{idx + 3} (#{percent.round(2)})", canvas.x + 2, y + 10) do |command|
50
+ command.size = 12
51
+ end
52
+ end
53
+
54
+ text(symbol, canvas.x + 12, theight + 5) do |command|
55
+ command.size = 25
56
+ command.color = Hokusai::Color.new(233,233,233,255)
57
+ end
58
+ end
59
+
60
+ canvas.x -= 100
61
+ yield canvas
62
+ end
63
+ end
64
+
65
+ class App < Hokusai::Block
66
+ template <<~EOF
67
+ [template]
68
+ vblock { background="84,23,77" padding="0,20,0,20"}
69
+ hblock { padding="0,20,0,20"}
70
+ [for="data in attractive_options"]
71
+ stackedbar { :key="key(index)" :data="data" }
72
+ EOF
73
+
74
+ uses(
75
+ vblock: Hokusai::Blocks::Vblock,
76
+ hblock: Hokusai::Blocks::Hblock,
77
+ stackedbar: StackedBar
78
+ )
79
+
80
+ def attractive_options
81
+ @options.to_a
82
+ end
83
+
84
+ def key(index)
85
+ "Key_#{index}"
86
+ end
87
+
88
+ def on_mounted
89
+ puts ["mounted"]
90
+ add_ticker("TSLA")
91
+ add_ticker("OXY")
92
+ add_ticker("DG")
93
+ end
94
+
95
+ def add_ticker(sym)
96
+ ticker_opts = Option.from_ticker(sym)
97
+ w3 = ticker_opts.select { |a| a.attractive?(3) }.sort_by { |a| a.percent(3) }.last
98
+ w4 = ticker_opts.select { |a| a.attractive?(4) }.sort_by { |a| a.percent(4) }.last
99
+ w5 = ticker_opts.select { |a| a.attractive?(5) }.sort_by { |a| a.percent(5) }.last
100
+ @options[sym] = [w3&.percent(3), w4&.percent(4), w5&.percent(5)]
101
+ puts @options[sym]
102
+ end
103
+
104
+ def initialize(**args)
105
+ @options = {}
106
+ super
107
+ end
108
+ end
109
+ end
110
+ # #
111
+ # Hokusai::Backends::SDLBackend.run(StockDecider::App) do |config|
112
+ # config.width = 500
113
+ # config.height = 500
114
+ # config.title = "Loop application"
115
+ # end
@@ -0,0 +1,74 @@
1
+ require 'json'
2
+ require 'date'
3
+ require "rest-client"
4
+
5
+ module StockDecider
6
+ class Option
7
+ URL = "https://cdn.cboe.com/api/global/delayed_quotes/options"
8
+ def self.from_ticker(sym)
9
+ res = RestClient.get("#{URL}/#{sym.upcase}.json").body
10
+ data = JSON.parse(res, symbolize_names: true)[:data]
11
+ data[:options].map do |option|
12
+ new(option, data[:current_price])
13
+ end
14
+ end
15
+
16
+ def initialize(payload, current)
17
+ @current_price = current
18
+ @payload = payload
19
+ end
20
+
21
+ def option
22
+ @payload[:option]
23
+ end
24
+
25
+ def cycles(weeks)
26
+ 52 / weeks
27
+ end
28
+
29
+ def name
30
+ option
31
+ end
32
+
33
+ def attractive?(weeks)
34
+ datespan > weeks - 1 && datespan < weeks + 1
35
+ end
36
+
37
+ def datespan
38
+ (expiration - DateTime.now).to_i / 7
39
+ end
40
+
41
+ def percent(weeks)
42
+ (@current_price * cycles(weeks)) / strike
43
+ end
44
+
45
+ def type
46
+ call? ? "Call" : "Put"
47
+ end
48
+
49
+ def call?
50
+ option.scan(/([CP])\d+$/)[0][0] == "C"
51
+ end
52
+
53
+ def expiration
54
+ value = option.scan(/^[A-Za-z]+(\d+)/)[0][0]
55
+ year = "20#{value[0..1]}".to_i
56
+ month = value[2..3].to_i
57
+ day = value[4..5].to_i
58
+
59
+ DateTime.new(year, month, day)
60
+ end
61
+
62
+ def strike
63
+ option.scan(/\d+$/)[0].to_i / 1000
64
+ end
65
+
66
+ def code
67
+ option.scan(/^[A-Za-z]+/)[0]
68
+ end
69
+
70
+ def put?
71
+ !call?
72
+ end
73
+ end
74
+ end