ruflet 0.0.8 → 0.0.9

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 (216) hide show
  1. checksums.yaml +4 -4
  2. data/bin/ruflet +6 -0
  3. data/lib/ruflet/cli/build_command.rb +372 -0
  4. data/lib/ruflet/cli/extra_command.rb +146 -0
  5. data/lib/ruflet/cli/flutter_sdk.rb +359 -0
  6. data/lib/ruflet/cli/new_command.rb +221 -0
  7. data/lib/ruflet/cli/run_command.rb +699 -0
  8. data/lib/ruflet/cli/templates.rb +68 -0
  9. data/lib/ruflet/cli/update_command.rb +111 -0
  10. data/lib/ruflet/cli.rb +85 -0
  11. data/lib/ruflet/version.rb +1 -1
  12. data/lib/ruflet_cli.rb +3 -0
  13. metadata +59 -209
  14. data/lib/ruflet.rb +0 -40
  15. data/lib/ruflet_protocol/ruflet/protocol.rb +0 -62
  16. data/lib/ruflet_protocol.rb +0 -4
  17. data/lib/ruflet_ui/ruflet/app.rb +0 -27
  18. data/lib/ruflet_ui/ruflet/colors.rb +0 -234
  19. data/lib/ruflet_ui/ruflet/control.rb +0 -281
  20. data/lib/ruflet_ui/ruflet/dsl.rb +0 -291
  21. data/lib/ruflet_ui/ruflet/event.rb +0 -42
  22. data/lib/ruflet_ui/ruflet/events/gesture_events.rb +0 -552
  23. data/lib/ruflet_ui/ruflet/icon_data.rb +0 -62
  24. data/lib/ruflet_ui/ruflet/icons/cupertino/cupertino_icons.rb +0 -54
  25. data/lib/ruflet_ui/ruflet/icons/cupertino_icon_lookup.rb +0 -112
  26. data/lib/ruflet_ui/ruflet/icons/material_icon_lookup.rb +0 -112
  27. data/lib/ruflet_ui/ruflet/icons/material_icons.rb +0 -55
  28. data/lib/ruflet_ui/ruflet/page.rb +0 -1222
  29. data/lib/ruflet_ui/ruflet/types/geometry.rb +0 -65
  30. data/lib/ruflet_ui/ruflet/types/text_style.rb +0 -191
  31. data/lib/ruflet_ui/ruflet/ui/control_factory.rb +0 -53
  32. data/lib/ruflet_ui/ruflet/ui/control_methods.rb +0 -17
  33. data/lib/ruflet_ui/ruflet/ui/control_registry.rb +0 -33
  34. data/lib/ruflet_ui/ruflet/ui/controls/cupertinos/cupertinoactionsheet_control.rb +0 -57
  35. data/lib/ruflet_ui/ruflet/ui/controls/cupertinos/cupertinoactionsheetaction_control.rb +0 -58
  36. data/lib/ruflet_ui/ruflet/ui/controls/cupertinos/cupertinoactivityindicator_control.rb +0 -57
  37. data/lib/ruflet_ui/ruflet/ui/controls/cupertinos/cupertinoalertdialog_control.rb +0 -39
  38. data/lib/ruflet_ui/ruflet/ui/controls/cupertinos/cupertinoappbar_control.rb +0 -44
  39. data/lib/ruflet_ui/ruflet/ui/controls/cupertinos/cupertinobottomsheet_control.rb +0 -38
  40. data/lib/ruflet_ui/ruflet/ui/controls/cupertinos/cupertinobutton_control.rb +0 -73
  41. data/lib/ruflet_ui/ruflet/ui/controls/cupertinos/cupertinocheckbox_control.rb +0 -70
  42. data/lib/ruflet_ui/ruflet/ui/controls/cupertinos/cupertinocontextmenu_control.rb +0 -34
  43. data/lib/ruflet_ui/ruflet/ui/controls/cupertinos/cupertinocontextmenuaction_control.rb +0 -36
  44. data/lib/ruflet_ui/ruflet/ui/controls/cupertinos/cupertinodatepicker_control.rb +0 -67
  45. data/lib/ruflet_ui/ruflet/ui/controls/cupertinos/cupertinodialogaction_control.rb +0 -35
  46. data/lib/ruflet_ui/ruflet/ui/controls/cupertinos/cupertinofilledbutton_control.rb +0 -73
  47. data/lib/ruflet_ui/ruflet/ui/controls/cupertinos/cupertinolisttile_control.rb +0 -67
  48. data/lib/ruflet_ui/ruflet/ui/controls/cupertinos/cupertinonavigationbar_control.rb +0 -61
  49. data/lib/ruflet_ui/ruflet/ui/controls/cupertinos/cupertinopicker_control.rb +0 -66
  50. data/lib/ruflet_ui/ruflet/ui/controls/cupertinos/cupertinoradio_control.rb +0 -66
  51. data/lib/ruflet_ui/ruflet/ui/controls/cupertinos/cupertinosegmentedbutton_control.rb +0 -63
  52. data/lib/ruflet_ui/ruflet/ui/controls/cupertinos/cupertinoslider_control.rb +0 -64
  53. data/lib/ruflet_ui/ruflet/ui/controls/cupertinos/cupertinoslidingsegmentedbutton_control.rb +0 -60
  54. data/lib/ruflet_ui/ruflet/ui/controls/cupertinos/cupertinoswitch_control.rb +0 -73
  55. data/lib/ruflet_ui/ruflet/ui/controls/cupertinos/cupertinotextfield_control.rb +0 -159
  56. data/lib/ruflet_ui/ruflet/ui/controls/cupertinos/cupertinotimerpicker_control.rb +0 -61
  57. data/lib/ruflet_ui/ruflet/ui/controls/cupertinos/cupertinotintedbutton_control.rb +0 -73
  58. data/lib/ruflet_ui/ruflet/ui/controls/cupertinos/ruflet_controls.rb +0 -89
  59. data/lib/ruflet_ui/ruflet/ui/controls/materials/alertdialog_control.rb +0 -58
  60. data/lib/ruflet_ui/ruflet/ui/controls/materials/appbar_control.rb +0 -53
  61. data/lib/ruflet_ui/ruflet/ui/controls/materials/audio_control.rb +0 -36
  62. data/lib/ruflet_ui/ruflet/ui/controls/materials/autocomplete_control.rb +0 -58
  63. data/lib/ruflet_ui/ruflet/ui/controls/materials/badge_control.rb +0 -31
  64. data/lib/ruflet_ui/ruflet/ui/controls/materials/banner_control.rb +0 -47
  65. data/lib/ruflet_ui/ruflet/ui/controls/materials/bottomappbar_control.rb +0 -62
  66. data/lib/ruflet_ui/ruflet/ui/controls/materials/bottomsheet_control.rb +0 -48
  67. data/lib/ruflet_ui/ruflet/ui/controls/materials/button_control.rb +0 -69
  68. data/lib/ruflet_ui/ruflet/ui/controls/materials/card_control.rb +0 -63
  69. data/lib/ruflet_ui/ruflet/ui/controls/materials/chart_controls.rb +0 -321
  70. data/lib/ruflet_ui/ruflet/ui/controls/materials/checkbox_control.rb +0 -76
  71. data/lib/ruflet_ui/ruflet/ui/controls/materials/chip_control.rb +0 -88
  72. data/lib/ruflet_ui/ruflet/ui/controls/materials/circleavatar_control.rb +0 -62
  73. data/lib/ruflet_ui/ruflet/ui/controls/materials/container_control.rb +0 -81
  74. data/lib/ruflet_ui/ruflet/ui/controls/materials/contextmenu_control.rb +0 -63
  75. data/lib/ruflet_ui/ruflet/ui/controls/materials/datacell_control.rb +0 -38
  76. data/lib/ruflet_ui/ruflet/ui/controls/materials/datacolumn_control.rb +0 -34
  77. data/lib/ruflet_ui/ruflet/ui/controls/materials/datarow_control.rb +0 -35
  78. data/lib/ruflet_ui/ruflet/ui/controls/materials/datatable_control.rb +0 -78
  79. data/lib/ruflet_ui/ruflet/ui/controls/materials/datepicker_control.rb +0 -55
  80. data/lib/ruflet_ui/ruflet/ui/controls/materials/daterangepicker_control.rb +0 -57
  81. data/lib/ruflet_ui/ruflet/ui/controls/materials/divider_control.rb +0 -36
  82. data/lib/ruflet_ui/ruflet/ui/controls/materials/dropdown_control.rb +0 -99
  83. data/lib/ruflet_ui/ruflet/ui/controls/materials/dropdownm2_control.rb +0 -119
  84. data/lib/ruflet_ui/ruflet/ui/controls/materials/dropdownoption_control.rb +0 -35
  85. data/lib/ruflet_ui/ruflet/ui/controls/materials/expansionpanel_control.rb +0 -61
  86. data/lib/ruflet_ui/ruflet/ui/controls/materials/expansionpanellist_control.rb +0 -60
  87. data/lib/ruflet_ui/ruflet/ui/controls/materials/expansiontile_control.rb +0 -82
  88. data/lib/ruflet_ui/ruflet/ui/controls/materials/filledbutton_control.rb +0 -69
  89. data/lib/ruflet_ui/ruflet/ui/controls/materials/fillediconbutton_control.rb +0 -81
  90. data/lib/ruflet_ui/ruflet/ui/controls/materials/filledtonalbutton_control.rb +0 -69
  91. data/lib/ruflet_ui/ruflet/ui/controls/materials/filledtonaliconbutton_control.rb +0 -81
  92. data/lib/ruflet_ui/ruflet/ui/controls/materials/floatingactionbutton_control.rb +0 -73
  93. data/lib/ruflet_ui/ruflet/ui/controls/materials/iconbutton_control.rb +0 -81
  94. data/lib/ruflet_ui/ruflet/ui/controls/materials/listtile_control.rb +0 -87
  95. data/lib/ruflet_ui/ruflet/ui/controls/materials/menubar_control.rb +0 -33
  96. data/lib/ruflet_ui/ruflet/ui/controls/materials/menuitembutton_control.rb +0 -67
  97. data/lib/ruflet_ui/ruflet/ui/controls/materials/navigationbar_control.rb +0 -67
  98. data/lib/ruflet_ui/ruflet/ui/controls/materials/navigationbardestination_control.rb +0 -35
  99. data/lib/ruflet_ui/ruflet/ui/controls/materials/navigationdrawer_control.rb +0 -41
  100. data/lib/ruflet_ui/ruflet/ui/controls/materials/navigationdrawerdestination_control.rb +0 -34
  101. data/lib/ruflet_ui/ruflet/ui/controls/materials/navigationrail_control.rb +0 -70
  102. data/lib/ruflet_ui/ruflet/ui/controls/materials/navigationraildestination_control.rb +0 -36
  103. data/lib/ruflet_ui/ruflet/ui/controls/materials/option_control.rb +0 -35
  104. data/lib/ruflet_ui/ruflet/ui/controls/materials/outlinedbutton_control.rb +0 -66
  105. data/lib/ruflet_ui/ruflet/ui/controls/materials/outlinediconbutton_control.rb +0 -81
  106. data/lib/ruflet_ui/ruflet/ui/controls/materials/popupmenubutton_control.rb +0 -74
  107. data/lib/ruflet_ui/ruflet/ui/controls/materials/popupmenuitem_control.rb +0 -38
  108. data/lib/ruflet_ui/ruflet/ui/controls/materials/progressbar_control.rb +0 -64
  109. data/lib/ruflet_ui/ruflet/ui/controls/materials/progressring_control.rb +0 -65
  110. data/lib/ruflet_ui/ruflet/ui/controls/materials/radio_control.rb +0 -70
  111. data/lib/ruflet_ui/ruflet/ui/controls/materials/radiogroup_control.rb +0 -33
  112. data/lib/ruflet_ui/ruflet/ui/controls/materials/rangeslider_control.rb +0 -67
  113. data/lib/ruflet_ui/ruflet/ui/controls/materials/reorderablelistview_control.rb +0 -80
  114. data/lib/ruflet_ui/ruflet/ui/controls/materials/ruflet_controls.rb +0 -252
  115. data/lib/ruflet_ui/ruflet/ui/controls/materials/searchbar_control.rb +0 -94
  116. data/lib/ruflet_ui/ruflet/ui/controls/materials/segment_control.rb +0 -33
  117. data/lib/ruflet_ui/ruflet/ui/controls/materials/segmentedbutton_control.rb +0 -63
  118. data/lib/ruflet_ui/ruflet/ui/controls/materials/selectionarea_control.rb +0 -32
  119. data/lib/ruflet_ui/ruflet/ui/controls/materials/slider_control.rb +0 -76
  120. data/lib/ruflet_ui/ruflet/ui/controls/materials/snackbar_control.rb +0 -51
  121. data/lib/ruflet_ui/ruflet/ui/controls/materials/submenubutton_control.rb +0 -66
  122. data/lib/ruflet_ui/ruflet/ui/controls/materials/switch_control.rb +0 -77
  123. data/lib/ruflet_ui/ruflet/ui/controls/materials/tab_control.rb +0 -35
  124. data/lib/ruflet_ui/ruflet/ui/controls/materials/tabbar_control.rb +0 -77
  125. data/lib/ruflet_ui/ruflet/ui/controls/materials/tabbarview_control.rb +0 -57
  126. data/lib/ruflet_ui/ruflet/ui/controls/materials/tabs_control.rb +0 -59
  127. data/lib/ruflet_ui/ruflet/ui/controls/materials/textbutton_control.rb +0 -66
  128. data/lib/ruflet_ui/ruflet/ui/controls/materials/textfield_control.rb +0 -148
  129. data/lib/ruflet_ui/ruflet/ui/controls/materials/timepicker_control.rb +0 -50
  130. data/lib/ruflet_ui/ruflet/ui/controls/materials/verticaldivider_control.rb +0 -36
  131. data/lib/ruflet_ui/ruflet/ui/controls/materials/video_control.rb +0 -39
  132. data/lib/ruflet_ui/ruflet/ui/controls/materials/webview_control.rb +0 -35
  133. data/lib/ruflet_ui/ruflet/ui/controls/ruflet_controls.rb +0 -447
  134. data/lib/ruflet_ui/ruflet/ui/controls/shared/animatedswitcher_control.rb +0 -59
  135. data/lib/ruflet_ui/ruflet/ui/controls/shared/arc_control.rb +0 -29
  136. data/lib/ruflet_ui/ruflet/ui/controls/shared/autofillgroup_control.rb +0 -32
  137. data/lib/ruflet_ui/ruflet/ui/controls/shared/basepage_control.rb +0 -44
  138. data/lib/ruflet_ui/ruflet/ui/controls/shared/browsercontextmenu_control.rb +0 -21
  139. data/lib/ruflet_ui/ruflet/ui/controls/shared/canvas_control.rb +0 -57
  140. data/lib/ruflet_ui/ruflet/ui/controls/shared/circle_control.rb +0 -25
  141. data/lib/ruflet_ui/ruflet/ui/controls/shared/color_control.rb +0 -23
  142. data/lib/ruflet_ui/ruflet/ui/controls/shared/column_control.rb +0 -67
  143. data/lib/ruflet_ui/ruflet/ui/controls/shared/dialogs_control.rb +0 -22
  144. data/lib/ruflet_ui/ruflet/ui/controls/shared/dismissible_control.rb +0 -66
  145. data/lib/ruflet_ui/ruflet/ui/controls/shared/draggable_control.rb +0 -39
  146. data/lib/ruflet_ui/ruflet/ui/controls/shared/dragtarget_control.rb +0 -36
  147. data/lib/ruflet_ui/ruflet/ui/controls/shared/fill_control.rb +0 -22
  148. data/lib/ruflet_ui/ruflet/ui/controls/shared/fletapp_control.rb +0 -62
  149. data/lib/ruflet_ui/ruflet/ui/controls/shared/gesturedetector_control.rb +0 -129
  150. data/lib/ruflet_ui/ruflet/ui/controls/shared/gridview_control.rb +0 -71
  151. data/lib/ruflet_ui/ruflet/ui/controls/shared/hero_control.rb +0 -56
  152. data/lib/ruflet_ui/ruflet/ui/controls/shared/icon_control.rb +0 -64
  153. data/lib/ruflet_ui/ruflet/ui/controls/shared/image_control.rb +0 -27
  154. data/lib/ruflet_ui/ruflet/ui/controls/shared/interactiveviewer_control.rb +0 -69
  155. data/lib/ruflet_ui/ruflet/ui/controls/shared/keyboardlistener_control.rb +0 -36
  156. data/lib/ruflet_ui/ruflet/ui/controls/shared/line_control.rb +0 -26
  157. data/lib/ruflet_ui/ruflet/ui/controls/shared/listview_control.rb +0 -71
  158. data/lib/ruflet_ui/ruflet/ui/controls/shared/markdown_control.rb +0 -70
  159. data/lib/ruflet_ui/ruflet/ui/controls/shared/mergesemantics_control.rb +0 -31
  160. data/lib/ruflet_ui/ruflet/ui/controls/shared/oval_control.rb +0 -26
  161. data/lib/ruflet_ui/ruflet/ui/controls/shared/overlay_control.rb +0 -22
  162. data/lib/ruflet_ui/ruflet/ui/controls/shared/page_control.rb +0 -74
  163. data/lib/ruflet_ui/ruflet/ui/controls/shared/pagelet_control.rb +0 -64
  164. data/lib/ruflet_ui/ruflet/ui/controls/shared/pageview_control.rb +0 -64
  165. data/lib/ruflet_ui/ruflet/ui/controls/shared/path_control.rb +0 -23
  166. data/lib/ruflet_ui/ruflet/ui/controls/shared/placeholder_control.rb +0 -58
  167. data/lib/ruflet_ui/ruflet/ui/controls/shared/points_control.rb +0 -24
  168. data/lib/ruflet_ui/ruflet/ui/controls/shared/rect_control.rb +0 -27
  169. data/lib/ruflet_ui/ruflet/ui/controls/shared/reorderabledraghandle_control.rb +0 -56
  170. data/lib/ruflet_ui/ruflet/ui/controls/shared/responsiverow_control.rb +0 -61
  171. data/lib/ruflet_ui/ruflet/ui/controls/shared/row_control.rb +0 -67
  172. data/lib/ruflet_ui/ruflet/ui/controls/shared/ruflet_controls.rb +0 -140
  173. data/lib/ruflet_ui/ruflet/ui/controls/shared/safearea_control.rb +0 -61
  174. data/lib/ruflet_ui/ruflet/ui/controls/shared/semantics_control.rb +0 -79
  175. data/lib/ruflet_ui/ruflet/ui/controls/shared/serviceregistry_control.rb +0 -21
  176. data/lib/ruflet_ui/ruflet/ui/controls/shared/shadermask_control.rb +0 -57
  177. data/lib/ruflet_ui/ruflet/ui/controls/shared/shadow_control.rb +0 -25
  178. data/lib/ruflet_ui/ruflet/ui/controls/shared/shimmer_control.rb +0 -60
  179. data/lib/ruflet_ui/ruflet/ui/controls/shared/stack_control.rb +0 -58
  180. data/lib/ruflet_ui/ruflet/ui/controls/shared/text_control.rb +0 -32
  181. data/lib/ruflet_ui/ruflet/ui/controls/shared/textspan_control.rb +0 -39
  182. data/lib/ruflet_ui/ruflet/ui/controls/shared/transparentpointer_control.rb +0 -54
  183. data/lib/ruflet_ui/ruflet/ui/controls/shared/view_control.rb +0 -77
  184. data/lib/ruflet_ui/ruflet/ui/controls/shared/window_control.rb +0 -56
  185. data/lib/ruflet_ui/ruflet/ui/controls/shared/windowdragarea_control.rb +0 -58
  186. data/lib/ruflet_ui/ruflet/ui/cupertino_control_factory.rb +0 -13
  187. data/lib/ruflet_ui/ruflet/ui/cupertino_control_methods.rb +0 -26
  188. data/lib/ruflet_ui/ruflet/ui/cupertino_control_registry.rb +0 -49
  189. data/lib/ruflet_ui/ruflet/ui/material_control_factory.rb +0 -13
  190. data/lib/ruflet_ui/ruflet/ui/material_control_methods.rb +0 -170
  191. data/lib/ruflet_ui/ruflet/ui/material_control_registry.rb +0 -168
  192. data/lib/ruflet_ui/ruflet/ui/services/ruflet/accelerometer_control.rb +0 -26
  193. data/lib/ruflet_ui/ruflet/ui/services/ruflet/barometer_control.rb +0 -26
  194. data/lib/ruflet_ui/ruflet/ui/services/ruflet/battery_control.rb +0 -22
  195. data/lib/ruflet_ui/ruflet/ui/services/ruflet/camera_control.rb +0 -58
  196. data/lib/ruflet_ui/ruflet/ui/services/ruflet/clipboard_control.rb +0 -21
  197. data/lib/ruflet_ui/ruflet/ui/services/ruflet/connectivity_control.rb +0 -22
  198. data/lib/ruflet_ui/ruflet/ui/services/ruflet/filepicker_control.rb +0 -23
  199. data/lib/ruflet_ui/ruflet/ui/services/ruflet/flashlight_control.rb +0 -22
  200. data/lib/ruflet_ui/ruflet/ui/services/ruflet/gyroscope_control.rb +0 -26
  201. data/lib/ruflet_ui/ruflet/ui/services/ruflet/hapticfeedback_control.rb +0 -21
  202. data/lib/ruflet_ui/ruflet/ui/services/ruflet/magnetometer_control.rb +0 -26
  203. data/lib/ruflet_ui/ruflet/ui/services/ruflet/screenbrightness_control.rb +0 -23
  204. data/lib/ruflet_ui/ruflet/ui/services/ruflet/screenshot_control.rb +0 -31
  205. data/lib/ruflet_ui/ruflet/ui/services/ruflet/semanticsservice_control.rb +0 -21
  206. data/lib/ruflet_ui/ruflet/ui/services/ruflet/shakedetector_control.rb +0 -26
  207. data/lib/ruflet_ui/ruflet/ui/services/ruflet/share_control.rb +0 -21
  208. data/lib/ruflet_ui/ruflet/ui/services/ruflet/sharedpreferences_control.rb +0 -21
  209. data/lib/ruflet_ui/ruflet/ui/services/ruflet/storagepaths_control.rb +0 -21
  210. data/lib/ruflet_ui/ruflet/ui/services/ruflet/urllauncher_control.rb +0 -21
  211. data/lib/ruflet_ui/ruflet/ui/services/ruflet/useraccelerometer_control.rb +0 -26
  212. data/lib/ruflet_ui/ruflet/ui/services/ruflet/wakelock_control.rb +0 -21
  213. data/lib/ruflet_ui/ruflet/ui/services/ruflet_services.rb +0 -66
  214. data/lib/ruflet_ui/ruflet/ui/shared_control_forwarders.rb +0 -129
  215. data/lib/ruflet_ui/ruflet/ui/widget_builder.rb +0 -64
  216. data/lib/ruflet_ui.rb +0 -124
@@ -0,0 +1,699 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "optparse"
4
+ require "rbconfig"
5
+ require "socket"
6
+ require "timeout"
7
+ require "tmpdir"
8
+ require "fileutils"
9
+ require "json"
10
+ require "net/http"
11
+ require "uri"
12
+ require "thread"
13
+ require "io/console"
14
+ require "time"
15
+
16
+ module Ruflet
17
+ module CLI
18
+ module RunCommand
19
+ def command_run(args)
20
+ options = { target: "mobile", requested_port: 8550 }
21
+ parser = OptionParser.new do |o|
22
+ o.on("--web") { options[:target] = "web" }
23
+ o.on("--desktop") { options[:target] = "desktop" }
24
+ o.on("--port PORT", Integer) { |v| options[:requested_port] = v }
25
+ end
26
+ parser.parse!(args)
27
+
28
+ script_token = args.shift || "main"
29
+ script_path = resolve_script(script_token)
30
+ unless script_path
31
+ warn "Script not found: #{script_token}"
32
+ warn "Expected: ./#{script_token}.rb, ./#{script_token}, or explicit file path."
33
+ return 1
34
+ end
35
+
36
+ selected_port = resolve_backend_port(options[:target], requested_port: options[:requested_port])
37
+ return 1 unless selected_port
38
+ env = {
39
+ "RUFLET_TARGET" => options[:target],
40
+ "RUFLET_SUPPRESS_SERVER_BANNER" => "1",
41
+ "RUFLET_PORT" => selected_port.to_s
42
+ }
43
+ assets_dir = File.join(File.dirname(script_path), "assets")
44
+ env["RUFLET_ASSETS_DIR"] = assets_dir if File.directory?(assets_dir)
45
+
46
+ print_run_banner(target: options[:target], requested_port: options[:requested_port], port: selected_port)
47
+ print_mobile_qr_hint(port: selected_port) if options[:target] == "mobile"
48
+
49
+ gemfile_path = find_nearest_gemfile(Dir.pwd)
50
+ cmd = build_runtime_command(script_path, gemfile_path: gemfile_path, env: env)
51
+ return 1 unless cmd
52
+
53
+ child_pid = Process.spawn(env, *cmd, pgroup: true)
54
+ launched_client_pids = launch_target_client(options[:target], selected_port)
55
+ forward_signal = lambda do |signal|
56
+ begin
57
+ Process.kill(signal, -child_pid)
58
+ rescue Errno::ESRCH
59
+ nil
60
+ end
61
+ end
62
+
63
+ previous_int = Signal.trap("INT") { forward_signal.call("INT") }
64
+ previous_term = Signal.trap("TERM") { forward_signal.call("TERM") }
65
+
66
+ _pid, status = Process.wait2(child_pid)
67
+ status.success? ? 0 : (status.exitstatus || 1)
68
+ ensure
69
+ Signal.trap("INT", previous_int) if defined?(previous_int) && previous_int
70
+ Signal.trap("TERM", previous_term) if defined?(previous_term) && previous_term
71
+
72
+ if defined?(child_pid) && child_pid
73
+ begin
74
+ Process.kill("TERM", -child_pid)
75
+ rescue Errno::ESRCH
76
+ nil
77
+ end
78
+ end
79
+
80
+ Array(defined?(launched_client_pids) ? launched_client_pids : nil).compact.each do |pid|
81
+ begin
82
+ Process.kill("TERM", -pid)
83
+ rescue Errno::ESRCH
84
+ begin
85
+ Process.kill("TERM", pid)
86
+ rescue Errno::ESRCH
87
+ nil
88
+ end
89
+ end
90
+ end
91
+
92
+ end
93
+
94
+ private
95
+
96
+ def build_runtime_command(script_path, gemfile_path:, env:)
97
+ if gemfile_path
98
+ env["BUNDLE_GEMFILE"] = gemfile_path
99
+ bundle_ready = system(env, RbConfig.ruby, "-S", "bundle", "check", out: File::NULL, err: File::NULL)
100
+ return nil unless bundle_ready || system(env, RbConfig.ruby, "-S", "bundle", "install")
101
+
102
+ return [RbConfig.ruby, "-rbundler/setup", script_path]
103
+ end
104
+
105
+ [RbConfig.ruby, script_path]
106
+ end
107
+
108
+ def resolve_script(token)
109
+ path = File.expand_path(token, Dir.pwd)
110
+ return path if File.file?(path)
111
+
112
+ candidate = File.expand_path("#{token}.rb", Dir.pwd)
113
+ return candidate if File.file?(candidate)
114
+
115
+ nil
116
+ end
117
+
118
+ def find_nearest_gemfile(start_dir)
119
+ current = File.expand_path(start_dir)
120
+ loop do
121
+ candidate = File.join(current, "Gemfile")
122
+ return candidate if File.file?(candidate)
123
+
124
+ parent = File.expand_path("..", current)
125
+ return nil if parent == current
126
+
127
+ current = parent
128
+ end
129
+ end
130
+
131
+ def print_run_banner(target:, requested_port:, port:)
132
+ if port != requested_port.to_i
133
+ puts "Requested port #{requested_port} is busy; bound to #{port}"
134
+ end
135
+ if target == "desktop"
136
+ puts "Ruflet desktop URL: http://localhost:#{port}"
137
+ elsif target == "mobile"
138
+ puts "Ruflet target: #{target}"
139
+ else
140
+ puts "Ruflet target: #{target}"
141
+ puts "Ruflet URL: http://localhost:#{port}"
142
+ end
143
+ end
144
+
145
+ def launch_target_client(target, port)
146
+ wait_for_server_boot(port)
147
+
148
+ case target
149
+ when "web"
150
+ launch_web_client(port)
151
+ when "desktop"
152
+ launch_desktop_client("http://localhost:#{port}")
153
+ else
154
+ []
155
+ end
156
+ end
157
+
158
+ def launch_web_client(port)
159
+ web_dir = detect_web_client_dir
160
+ unless web_dir
161
+ warn "Web client build not found and prebuilt download failed."
162
+ return []
163
+ end
164
+
165
+ web_port = find_available_port(port + 1)
166
+ web_pid = Process.spawn("python3", "-m", "http.server", web_port.to_s, "--bind", "127.0.0.1", chdir: web_dir, out: File::NULL, err: File::NULL)
167
+ Process.detach(web_pid)
168
+ wait_for_server_boot(web_port)
169
+ backend_url = "http://localhost:#{port}"
170
+ web_url = "http://localhost:#{web_port}/?#{URI.encode_www_form(url: backend_url)}"
171
+ browser_pid = open_in_browser_app_mode(web_url)
172
+ open_in_browser(web_url) if browser_pid.nil?
173
+ puts "Ruflet web client: #{web_url}"
174
+ puts "Ruflet backend ws: ws://localhost:#{port}/ws"
175
+ [web_pid, browser_pid].compact
176
+ rescue Errno::ENOENT
177
+ warn "python3 is required to host web client locally."
178
+ warn "Install Python 3 and rerun."
179
+ []
180
+ rescue StandardError => e
181
+ warn "Failed to launch web client: #{e.class}: #{e.message}"
182
+ []
183
+ end
184
+
185
+ def wait_for_server_boot(port, timeout_seconds: 10)
186
+ Timeout.timeout(timeout_seconds) do
187
+ loop do
188
+ begin
189
+ sock = TCPSocket.new("127.0.0.1", port)
190
+ sock.write("GET / HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n")
191
+ sock.close
192
+ break
193
+ rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH
194
+ sleep 0.15
195
+ end
196
+ end
197
+ end
198
+ rescue Timeout::Error
199
+ warn "Server did not become reachable at http://localhost:#{port} yet."
200
+ end
201
+
202
+ def open_in_browser(url)
203
+ cmd =
204
+ case RbConfig::CONFIG["host_os"]
205
+ when /darwin/i
206
+ ["open", url]
207
+ when /mswin|mingw|cygwin/i
208
+ ["cmd", "/c", "start", "", url]
209
+ else
210
+ ["xdg-open", url]
211
+ end
212
+ if system(*cmd, out: File::NULL, err: File::NULL)
213
+ puts "Opened browser at #{url}"
214
+ else
215
+ warn "Could not auto-open browser. Open manually: #{url}"
216
+ end
217
+ end
218
+
219
+ def open_in_browser_app_mode(url)
220
+ host_os = RbConfig::CONFIG["host_os"]
221
+ if host_os.match?(/darwin/i)
222
+ chrome = "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"
223
+ chromium = "/Applications/Chromium.app/Contents/MacOS/Chromium"
224
+ browser = [chrome, chromium].find { |p| File.file?(p) && File.executable?(p) }
225
+ return nil unless browser
226
+
227
+ profile_dir = Dir.mktmpdir("ruflet-webapp-")
228
+ pid = Process.spawn(
229
+ browser,
230
+ "--new-window",
231
+ "--no-first-run",
232
+ "--no-default-browser-check",
233
+ "--user-data-dir=#{profile_dir}",
234
+ "--app=#{url}",
235
+ pgroup: true,
236
+ out: File::NULL,
237
+ err: File::NULL
238
+ )
239
+ Process.detach(pid)
240
+ return pid
241
+ end
242
+
243
+ if host_os.match?(/linux/i)
244
+ browser = %w[google-chrome chromium chromium-browser].find { |cmd| system("which", cmd, out: File::NULL, err: File::NULL) }
245
+ return nil unless browser
246
+
247
+ profile_dir = Dir.mktmpdir("ruflet-webapp-")
248
+ pid = Process.spawn(
249
+ browser,
250
+ "--new-window",
251
+ "--no-first-run",
252
+ "--no-default-browser-check",
253
+ "--user-data-dir=#{profile_dir}",
254
+ "--app=#{url}",
255
+ pgroup: true,
256
+ out: File::NULL,
257
+ err: File::NULL
258
+ )
259
+ Process.detach(pid)
260
+ return pid
261
+ end
262
+
263
+ nil
264
+ rescue StandardError
265
+ nil
266
+ end
267
+
268
+ def launch_desktop_client(url)
269
+ cmd = detect_desktop_client_command(url)
270
+ unless cmd
271
+ warn "Desktop client executable not found."
272
+ warn "Set RUFLET_CLIENT_DIR to your client path."
273
+ warn "Example: export RUFLET_CLIENT_DIR=/path/to/ruflet_client"
274
+ return
275
+ end
276
+
277
+ pid = Process.spawn(*cmd, out: File::NULL, err: File::NULL)
278
+ Process.detach(pid)
279
+ if !pid
280
+ warn "Failed to launch desktop client: #{cmd.first}"
281
+ warn "Start it manually with URL: #{url}"
282
+ end
283
+ [pid]
284
+ rescue StandardError => e
285
+ warn "Failed to launch desktop client: #{e.class}: #{e.message}"
286
+ warn "Start it manually with URL: #{url}"
287
+ []
288
+ end
289
+
290
+ def detect_desktop_client_command(url)
291
+ root = ENV["RUFLET_CLIENT_DIR"]
292
+ root = File.expand_path("ruflet_client", Dir.pwd) if root.to_s.strip.empty?
293
+ root = nil unless Dir.exist?(root)
294
+ root ||= ensure_prebuilt_client(desktop: true)
295
+ return nil unless root && Dir.exist?(root)
296
+
297
+ host_os = RbConfig::CONFIG["host_os"]
298
+ if host_os.match?(/darwin/i)
299
+ release_bin = File.join(root, "build", "macos", "Build", "Products", "Release", "ruflet_client.app", "Contents", "MacOS", "ruflet_client")
300
+ debug_bin = File.join(root, "build", "macos", "Build", "Products", "Debug", "ruflet_client.app", "Contents", "MacOS", "ruflet_client")
301
+ prebuilt_bin = File.join(root, "desktop", "ruflet_client.app", "Contents", "MacOS", "ruflet_client")
302
+ executable = [release_bin, debug_bin].find { |p| File.file?(p) && File.executable?(p) }
303
+ executable ||= prebuilt_bin if File.file?(prebuilt_bin) && File.executable?(prebuilt_bin)
304
+ return [executable, url] if executable
305
+ elsif host_os.match?(/mswin|mingw|cygwin/i)
306
+ exe = File.join(root, "build", "windows", "x64", "runner", "Release", "ruflet_client.exe")
307
+ prebuilt = File.join(root, "desktop", "ruflet_client.exe")
308
+ exe = prebuilt if !File.file?(exe) && File.file?(prebuilt)
309
+ return [exe, url] if File.file?(exe)
310
+ else
311
+ direct = File.join(root, "build", "linux", "x64", "release", "bundle", "ruflet_client")
312
+ prebuilt_direct = File.join(root, "desktop", "ruflet_client")
313
+ direct = prebuilt_direct if !File.file?(direct) && File.file?(prebuilt_direct)
314
+ return [direct, url] if File.file?(direct)
315
+ bundle_dir = File.join(root, "build", "linux", "x64", "release", "bundle")
316
+ if Dir.exist?(bundle_dir)
317
+ candidate = Dir.children(bundle_dir).map { |f| File.join(bundle_dir, f) }
318
+ .find { |path| File.file?(path) && File.executable?(path) }
319
+ return [candidate, url] if candidate
320
+ end
321
+ end
322
+
323
+ nil
324
+ end
325
+
326
+ def detect_web_client_dir
327
+ root = ENV["RUFLET_CLIENT_DIR"]
328
+ root = File.expand_path("ruflet_client", Dir.pwd) if root.to_s.strip.empty?
329
+ root = nil unless Dir.exist?(root)
330
+ root ||= ensure_prebuilt_client(web: true)
331
+ return nil unless root && Dir.exist?(root)
332
+
333
+ built = File.join(root, "build", "web")
334
+ return built if Dir.exist?(built) && File.file?(File.join(built, "index.html"))
335
+ prebuilt = File.join(root, "web")
336
+ return prebuilt if Dir.exist?(prebuilt) && File.file?(File.join(prebuilt, "index.html"))
337
+
338
+ nil
339
+ end
340
+
341
+ def ensure_prebuilt_client(web: false, desktop: false, platform: nil, force: false)
342
+ platform ||= host_platform_name
343
+ return nil if platform.nil?
344
+
345
+ cache_root = client_cache_root_for(platform)
346
+ FileUtils.mkdir_p(cache_root)
347
+
348
+ wanted_assets = []
349
+ wanted_assets << { kind: :web, name: "ruflet_client-web.tar.gz" } if web
350
+ if desktop
351
+ desktop_asset = desktop_asset_name_for(platform)
352
+ return nil if desktop_asset.nil?
353
+ wanted_assets << { kind: :desktop, name: desktop_asset, platform: platform }
354
+ end
355
+ if !force && (wanted_assets.empty? || prebuilt_assets_present?(cache_root, web: web, desktop: desktop, platform: platform))
356
+ ensure_client_manifest(cache_root, platform: platform)
357
+ return cache_root
358
+ end
359
+
360
+ release = fetch_release_for_version
361
+ return nil unless release
362
+
363
+ assets = release.fetch("assets", [])
364
+ asset_names = assets.map { |a| a["name"].to_s }
365
+ installed_assets = []
366
+ Dir.mktmpdir("ruflet-prebuilt-") do |tmpdir|
367
+ wanted_assets.each do |wanted|
368
+ asset_name = wanted.fetch(:name)
369
+ asset = assets.find { |a| a["name"] == asset_name }
370
+ asset ||= fallback_release_asset(assets, wanted)
371
+ unless asset
372
+ warn "Missing release asset: #{asset_name}"
373
+ warn "Available assets: #{asset_names.join(', ')}" unless asset_names.empty?
374
+ return nil
375
+ end
376
+ resolved_name = asset.fetch("name")
377
+ puts "Downloading prebuilt client asset: #{resolved_name}"
378
+ archive_path = File.join(tmpdir, resolved_name)
379
+ download_file(asset.fetch("browser_download_url"), archive_path)
380
+ subdir = wanted[:kind] == :web ? "web" : "desktop"
381
+ target = File.join(cache_root, subdir)
382
+ FileUtils.rm_rf(target) if force && Dir.exist?(target)
383
+ FileUtils.mkdir_p(target)
384
+ unless extract_archive(archive_path, target)
385
+ warn "Failed to extract asset: #{resolved_name}"
386
+ return nil
387
+ end
388
+ installed_assets << {
389
+ "kind" => wanted[:kind].to_s,
390
+ "platform" => wanted[:platform] || platform,
391
+ "asset_name" => resolved_name,
392
+ "download_url" => asset.fetch("browser_download_url")
393
+ }
394
+ end
395
+ end
396
+
397
+ if prebuilt_assets_present?(cache_root, web: web, desktop: desktop, platform: platform)
398
+ write_client_manifest(cache_root, platform: platform, release: release, assets: installed_assets)
399
+ return cache_root
400
+ end
401
+
402
+ nil
403
+ rescue StandardError => e
404
+ warn "Prebuilt client bootstrap failed: #{e.class}: #{e.message}"
405
+ nil
406
+ end
407
+
408
+ def prebuilt_assets_present?(root, web:, desktop:, platform: nil)
409
+ ok_web = !web || File.file?(File.join(root, "web", "index.html"))
410
+ ok_desktop = !desktop || prebuilt_desktop_present?(root, platform: platform)
411
+ ok_web && ok_desktop
412
+ end
413
+
414
+ def prebuilt_desktop_present?(root, platform: nil)
415
+ platform ||= host_platform_name
416
+ return false if platform.nil?
417
+
418
+ case platform
419
+ when "macos"
420
+ File.file?(File.join(root, "desktop", "ruflet_client.app", "Contents", "MacOS", "ruflet_client"))
421
+ when "linux"
422
+ File.file?(File.join(root, "desktop", "ruflet_client"))
423
+ when "windows"
424
+ File.file?(File.join(root, "desktop", "ruflet_client.exe"))
425
+ else
426
+ false
427
+ end
428
+ end
429
+
430
+ def host_platform_name
431
+ host_os = RbConfig::CONFIG["host_os"]
432
+ return "macos" if host_os.match?(/darwin/i)
433
+ return "linux" if host_os.match?(/linux/i)
434
+ return "windows" if host_os.match?(/mswin|mingw|cygwin/i)
435
+
436
+ nil
437
+ end
438
+
439
+ def desktop_asset_name_for(platform)
440
+ case platform
441
+ when "macos" then "ruflet_client-macos-universal.zip"
442
+ when "linux" then "ruflet_client-linux-x64.tar.gz"
443
+ when "windows" then "ruflet_client-windows-x64.zip"
444
+ end
445
+ end
446
+
447
+ def client_cache_root_for(platform)
448
+ File.join(Dir.home, ".ruflet", "client", ruflet_version, platform.to_s)
449
+ end
450
+
451
+ def fetch_release_for_version
452
+ release_by_tag("v#{ruflet_version}") ||
453
+ release_by_tag(ruflet_version) ||
454
+ release_by_tag("prebuild") ||
455
+ release_by_tag("prebuild-main") ||
456
+ release_latest
457
+ end
458
+
459
+ def ruflet_version
460
+ return Ruflet::VERSION if Ruflet.const_defined?(:VERSION)
461
+
462
+ require_relative "../version"
463
+ Ruflet::VERSION
464
+ end
465
+
466
+ def release_latest
467
+ github_get_json("https://api.github.com/repos/AdamMusa/Ruflet/releases/latest")
468
+ end
469
+
470
+ def release_by_tag(tag)
471
+ github_get_json("https://api.github.com/repos/AdamMusa/Ruflet/releases/tags/#{tag}")
472
+ rescue StandardError
473
+ nil
474
+ end
475
+
476
+ def fallback_release_asset(assets, wanted)
477
+ kind = wanted[:kind]
478
+ platform = wanted[:platform]
479
+ candidates = assets.select { |asset| release_asset_matches?(asset.fetch("name", ""), kind, platform) }
480
+ candidates.first
481
+ end
482
+
483
+ def release_asset_matches?(name, kind, platform)
484
+ n = name.to_s.downcase
485
+ return false unless n.include?("ruflet_client")
486
+
487
+ if kind == :web
488
+ return n.include?("web") && (n.end_with?(".tar.gz") || n.end_with?(".zip"))
489
+ end
490
+
491
+ case platform
492
+ when "macos"
493
+ n.include?("macos") && n.end_with?(".zip")
494
+ when "linux"
495
+ n.include?("linux") && (n.end_with?(".tar.gz") || n.end_with?(".tgz"))
496
+ when "windows"
497
+ n.include?("windows") && n.end_with?(".zip")
498
+ else
499
+ false
500
+ end
501
+ end
502
+
503
+ def github_get_json(url)
504
+ uri = URI(url)
505
+ response = Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
506
+ req = Net::HTTP::Get.new(uri)
507
+ req["Accept"] = "application/vnd.github+json"
508
+ req["User-Agent"] = "ruflet-cli"
509
+ http.request(req)
510
+ end
511
+ return JSON.parse(response.body) if response.is_a?(Net::HTTPSuccess)
512
+
513
+ raise "GitHub API failed (#{response.code})"
514
+ end
515
+
516
+ def download_file(url, destination, limit: 5)
517
+ raise "Too many redirects while downloading #{url}" if limit <= 0
518
+
519
+ uri = URI(url)
520
+ Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
521
+ req = Net::HTTP::Get.new(uri)
522
+ req["User-Agent"] = "ruflet-cli"
523
+ http.request(req) do |res|
524
+ case res
525
+ when Net::HTTPSuccess
526
+ File.open(destination, "wb") { |f| res.read_body { |chunk| f.write(chunk) } }
527
+ return destination
528
+ when Net::HTTPRedirection
529
+ return download_file(res["location"], destination, limit: limit - 1)
530
+ else
531
+ raise "Download failed (#{res.code})"
532
+ end
533
+ end
534
+ end
535
+ end
536
+
537
+ def extract_archive(archive, destination)
538
+ if archive.end_with?(".tar.gz")
539
+ return system("tar", "-xzf", archive, "-C", destination, out: File::NULL, err: File::NULL)
540
+ end
541
+ if archive.end_with?(".zip")
542
+ host_os = RbConfig::CONFIG["host_os"]
543
+ if host_os.match?(/darwin/i)
544
+ return system("ditto", "-x", "-k", archive, destination, out: File::NULL, err: File::NULL)
545
+ end
546
+ return system("unzip", "-oq", archive, "-d", destination, out: File::NULL, err: File::NULL)
547
+ end
548
+
549
+ false
550
+ end
551
+
552
+ def client_manifest_path(root)
553
+ File.join(root, "manifest.json")
554
+ end
555
+
556
+ def read_client_manifest(root)
557
+ path = client_manifest_path(root)
558
+ return nil unless File.file?(path)
559
+
560
+ JSON.parse(File.read(path))
561
+ rescue StandardError
562
+ nil
563
+ end
564
+
565
+ def ensure_client_manifest(root, platform:)
566
+ return if read_client_manifest(root)
567
+
568
+ assets = []
569
+ assets << { "kind" => "web", "platform" => platform, "asset_name" => nil } if File.file?(File.join(root, "web", "index.html"))
570
+ if prebuilt_desktop_present?(root, platform: platform)
571
+ assets << { "kind" => "desktop", "platform" => platform, "asset_name" => nil }
572
+ end
573
+ return if assets.empty?
574
+
575
+ write_client_manifest(root, platform: platform, release: nil, assets: assets)
576
+ end
577
+
578
+ def write_client_manifest(root, platform:, release:, assets:)
579
+ FileUtils.mkdir_p(root)
580
+ payload = {
581
+ "schema" => 1,
582
+ "ruflet_version" => ruflet_version,
583
+ "platform" => platform,
584
+ "release_tag" => release && release["tag_name"],
585
+ "released_at" => release && release["published_at"],
586
+ "installed_at" => Time.now.utc.iso8601,
587
+ "targets" => assets
588
+ }
589
+ File.write(client_manifest_path(root), JSON.pretty_generate(payload))
590
+ end
591
+
592
+ def print_mobile_qr_hint(port: 8550)
593
+ host = best_lan_host
594
+ payload = "http://#{host}:#{port}"
595
+
596
+ puts
597
+ puts "Ruflet mobile connect URL:"
598
+ puts " #{payload}"
599
+ puts "Scan this QR from ruflet_client (Connect -> Scan QR):"
600
+ print_ascii_qr(payload)
601
+ puts
602
+ rescue StandardError => e
603
+ warn "QR setup failed: #{e.class}: #{e.message}"
604
+ end
605
+
606
+ def find_available_port(start_port, max_attempts: 100)
607
+ port = start_port.to_i
608
+
609
+ max_attempts.times do
610
+ begin
611
+ begin
612
+ probe = TCPServer.new("0.0.0.0", port)
613
+ rescue Errno::EACCES, Errno::EPERM
614
+ probe = TCPServer.new("127.0.0.1", port)
615
+ end
616
+ probe.close
617
+ return port
618
+ rescue Errno::EADDRINUSE, Errno::EACCES, Errno::EPERM
619
+ port += 1
620
+ end
621
+ end
622
+
623
+ start_port
624
+ end
625
+
626
+ def resolve_backend_port(_target, requested_port: 8550)
627
+ base = requested_port.to_i
628
+ base = 8550 if base <= 0
629
+ find_available_port(base)
630
+ end
631
+
632
+ def port_available?(port)
633
+ probe = nil
634
+ begin
635
+ begin
636
+ probe = TCPServer.new("0.0.0.0", port)
637
+ rescue Errno::EACCES, Errno::EPERM
638
+ probe = TCPServer.new("127.0.0.1", port)
639
+ end
640
+ true
641
+ rescue Errno::EADDRINUSE
642
+ false
643
+ ensure
644
+ probe&.close
645
+ end
646
+ end
647
+
648
+ def best_lan_host
649
+ ips = Socket.ip_address_list
650
+ addr = ips.find { |ip| ip.ipv4_private? && !ip.ipv4_loopback? }
651
+ return addr.ip_address if addr
652
+
653
+ "127.0.0.1"
654
+ end
655
+
656
+ def print_ascii_qr(payload)
657
+ begin
658
+ require "rqrcode"
659
+ rescue LoadError
660
+ puts "(Install 'rqrcode' gem in CLI package for terminal QR rendering.)"
661
+ return
662
+ end
663
+
664
+ q = RQRCode::QRCode.new(payload)
665
+ border = 1
666
+ core = q.modules
667
+ size = core.length + (2 * border)
668
+
669
+ matrix = Array.new(size) do |y|
670
+ Array.new(size) do |x|
671
+ cy = y - border
672
+ cx = x - border
673
+ cy >= 0 && cx >= 0 && cy < core.length && cx < core.length && core[cy][cx]
674
+ end
675
+ end
676
+
677
+ y = 0
678
+ while y < size
679
+ line = +""
680
+ (0...size).each do |x|
681
+ top = matrix[y][x]
682
+ bottom = (y + 1 < size) ? matrix[y + 1][x] : false
683
+ line << if top && bottom
684
+ "\u2588"
685
+ elsif top
686
+ "\u2580"
687
+ elsif bottom
688
+ "\u2584"
689
+ else
690
+ " "
691
+ end
692
+ end
693
+ puts line
694
+ y += 2
695
+ end
696
+ end
697
+ end
698
+ end
699
+ end