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.
- checksums.yaml +7 -0
- data/Dockerfile +26 -0
- data/Gemfile +15 -0
- data/Gemfile.lock +91 -0
- data/LICENSE +21 -0
- data/README.md +28 -0
- data/ast/genheader +3 -0
- data/ast/include/hashmap.c +1151 -0
- data/ast/include/hashmap.c.license +20 -0
- data/ast/include/hashmap.h +54 -0
- data/ast/src/core/ast.c +448 -0
- data/ast/src/core/ast.h +259 -0
- data/ast/src/core/common.h +24 -0
- data/ast/src/core/component.c +85 -0
- data/ast/src/core/component.h +35 -0
- data/ast/src/core/hml.c +665 -0
- data/ast/src/core/hml.h +11 -0
- data/ast/src/core/input.c +458 -0
- data/ast/src/core/input.h +118 -0
- data/ast/src/core/style.c +101 -0
- data/ast/src/core/style.h +41 -0
- data/ast/src/core/text.c +784 -0
- data/ast/src/core/text.h +93 -0
- data/ast/src/core/util.c +140 -0
- data/ast/src/core/util.h +48 -0
- data/ast/src/hokusai.c +6 -0
- data/ast/src/hokusai.h +6 -0
- data/ast/test/fixtures/test.ui +13 -0
- data/ast/test/greatest.h +1266 -0
- data/ast/test/hokusai.c +14 -0
- data/ast/test/parser.c +234 -0
- data/ast/test/text.c +116 -0
- data/ext/extconf.rb +27 -0
- data/grammar/Cargo.lock +80 -0
- data/grammar/Cargo.toml +26 -0
- data/grammar/binding.gyp +20 -0
- data/grammar/bindings/node/binding.cc +28 -0
- data/grammar/bindings/node/index.js +19 -0
- data/grammar/bindings/rust/build.rs +40 -0
- data/grammar/bindings/rust/lib.rs +52 -0
- data/grammar/corpus/1_document.txt +131 -0
- data/grammar/corpus/2_selectors.txt +58 -0
- data/grammar/corpus/3_spaces.txt +69 -0
- data/grammar/corpus/4_errors.txt +10 -0
- data/grammar/corpus/5_macros.txt +175 -0
- data/grammar/corpus/6_styles.txt +81 -0
- data/grammar/grammar.js +275 -0
- data/grammar/package-lock.json +34 -0
- data/grammar/package.json +33 -0
- data/grammar/src/grammar.json +1269 -0
- data/grammar/src/node-types.json +474 -0
- data/grammar/src/parser.c +5772 -0
- data/grammar/src/scanner.c +258 -0
- data/grammar/src/tree_sitter/parser.h +230 -0
- data/grammar/src/tree_sitter/scanner.h +12 -0
- data/grammar/test.nml +10 -0
- data/hokusai.gemspec +19 -0
- data/ui/examples/assets/DigitalDisplay.ttf +0 -0
- data/ui/examples/assets/OpenSans-Regular.ttf +0 -0
- data/ui/examples/assets/addy.png +0 -0
- data/ui/examples/assets/baby_sean.png +0 -0
- data/ui/examples/assets/football-troll.png +0 -0
- data/ui/examples/assets/gear.png +0 -0
- data/ui/examples/assets/icecold.ttf +0 -0
- data/ui/examples/assets/science-troll.png +0 -0
- data/ui/examples/buddy.rb +31 -0
- data/ui/examples/clock.rb +58 -0
- data/ui/examples/counter.rb +123 -0
- data/ui/examples/dynamic.rb +147 -0
- data/ui/examples/foobar.rb +236 -0
- data/ui/examples/stock.rb +115 -0
- data/ui/examples/stock_decider/option.rb +74 -0
- data/ui/examples/tic_tac_toe.rb +246 -0
- data/ui/lib/lib_hokusai.rb +425 -0
- data/ui/spec/hokusai/ast_spec.rb +88 -0
- data/ui/spec/hokusai/automation/keys_transcoder_spec.rb +50 -0
- data/ui/spec/hokusai/automation/selector_spec.rb +68 -0
- data/ui/spec/hokusai/block_spec.rb +126 -0
- data/ui/spec/hokusai/directives_spec.rb +327 -0
- data/ui/spec/hokusai/e2e/client_spec.rb +58 -0
- data/ui/spec/hokusai/e2e/meta_spec.rb +42 -0
- data/ui/spec/hokusai/publisher_spec.rb +38 -0
- data/ui/spec/hokusai/slots_spec.rb +150 -0
- data/ui/spec/hokusai/util/piece_table_spec.rb +90 -0
- data/ui/spec/hokusai_spec.rb +0 -0
- data/ui/spec/spec_helper.rb +30 -0
- data/ui/src/hokusai/ast.rb +446 -0
- data/ui/src/hokusai/automation/client.rb +167 -0
- data/ui/src/hokusai/automation/constants.rb +98 -0
- data/ui/src/hokusai/automation/converters/selector_converter.rb +61 -0
- data/ui/src/hokusai/automation/driver.rb +54 -0
- data/ui/src/hokusai/automation/driver_command_queue.rb +50 -0
- data/ui/src/hokusai/automation/driver_commands/base.rb +79 -0
- data/ui/src/hokusai/automation/driver_commands/get_attribute.rb +41 -0
- data/ui/src/hokusai/automation/driver_commands/invoke.rb +33 -0
- data/ui/src/hokusai/automation/driver_commands/locate.rb +48 -0
- data/ui/src/hokusai/automation/driver_commands/trigger_keyboard.rb +94 -0
- data/ui/src/hokusai/automation/driver_commands/trigger_mouse.rb +213 -0
- data/ui/src/hokusai/automation/keys_transcoder.rb +128 -0
- data/ui/src/hokusai/automation/selector.rb +39 -0
- data/ui/src/hokusai/automation/server.rb +114 -0
- data/ui/src/hokusai/automation.rb +3 -0
- data/ui/src/hokusai/backends/raylib/config.rb +47 -0
- data/ui/src/hokusai/backends/raylib/font.rb +113 -0
- data/ui/src/hokusai/backends/raylib/keys.rb +124 -0
- data/ui/src/hokusai/backends/raylib.rb +449 -0
- data/ui/src/hokusai/backends/sdl2/Monaco.ttf +0 -0
- data/ui/src/hokusai/backends/sdl2/color.rb +12 -0
- data/ui/src/hokusai/backends/sdl2/config.rb +31 -0
- data/ui/src/hokusai/backends/sdl2/font.rb +127 -0
- data/ui/src/hokusai/backends/sdl2/keys.rb +119 -0
- data/ui/src/hokusai/backends/sdl2.rb +529 -0
- data/ui/src/hokusai/block.rb +237 -0
- data/ui/src/hokusai/blocks/button.rb +100 -0
- data/ui/src/hokusai/blocks/checkbox.rb +51 -0
- data/ui/src/hokusai/blocks/circle.rb +28 -0
- data/ui/src/hokusai/blocks/clipped.rb +23 -0
- data/ui/src/hokusai/blocks/cursor.rb +49 -0
- data/ui/src/hokusai/blocks/dynamic.rb +37 -0
- data/ui/src/hokusai/blocks/empty.rb +10 -0
- data/ui/src/hokusai/blocks/hblock.rb +35 -0
- data/ui/src/hokusai/blocks/image.rb +18 -0
- data/ui/src/hokusai/blocks/input.rb +200 -0
- data/ui/src/hokusai/blocks/label.rb +39 -0
- data/ui/src/hokusai/blocks/panel.rb +126 -0
- data/ui/src/hokusai/blocks/rect.rb +24 -0
- data/ui/src/hokusai/blocks/scissor_begin.rb +18 -0
- data/ui/src/hokusai/blocks/scissor_end.rb +12 -0
- data/ui/src/hokusai/blocks/scrollbar.rb +103 -0
- data/ui/src/hokusai/blocks/selectable.rb +77 -0
- data/ui/src/hokusai/blocks/svg.rb +20 -0
- data/ui/src/hokusai/blocks/text.rb +214 -0
- data/ui/src/hokusai/blocks/titlebar/osx.rb +145 -0
- data/ui/src/hokusai/blocks/toggle.rb +55 -0
- data/ui/src/hokusai/blocks/vblock.rb +35 -0
- data/ui/src/hokusai/commands/base.rb +22 -0
- data/ui/src/hokusai/commands/circle.rb +47 -0
- data/ui/src/hokusai/commands/image.rb +45 -0
- data/ui/src/hokusai/commands/rect.rb +158 -0
- data/ui/src/hokusai/commands/scissor.rb +22 -0
- data/ui/src/hokusai/commands/text.rb +92 -0
- data/ui/src/hokusai/commands.rb +87 -0
- data/ui/src/hokusai/diff.rb +124 -0
- data/ui/src/hokusai/error.rb +3 -0
- data/ui/src/hokusai/event.rb +54 -0
- data/ui/src/hokusai/events/keyboard.rb +84 -0
- data/ui/src/hokusai/events/mouse.rb +172 -0
- data/ui/src/hokusai/font.rb +280 -0
- data/ui/src/hokusai/meta.rb +152 -0
- data/ui/src/hokusai/mounting/loop_entry.rb +230 -0
- data/ui/src/hokusai/mounting/mount_entry.rb +74 -0
- data/ui/src/hokusai/mounting/update_entry.rb +101 -0
- data/ui/src/hokusai/node.rb +98 -0
- data/ui/src/hokusai/node_mounter.rb +102 -0
- data/ui/src/hokusai/painter.rb +214 -0
- data/ui/src/hokusai/publisher.rb +32 -0
- data/ui/src/hokusai/style.rb +72 -0
- data/ui/src/hokusai/types.rb +266 -0
- data/ui/src/hokusai/util/clamping_iterator.rb +202 -0
- data/ui/src/hokusai/util/piece_table.rb +111 -0
- data/ui/src/hokusai/util/selection.rb +145 -0
- data/ui/src/hokusai.rb +120 -0
- data/ui/vendor/.gitkeep +0 -0
- data/vendor/.gitkeep +0 -0
- data/xmake.lua +192 -0
- 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
|