picoride 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.claude/launch.json +11 -0
- data/CHANGELOG.md +5 -0
- data/CLAUDE.md +1518 -0
- data/CODE_OF_CONDUCT.md +10 -0
- data/Dockerfile +76 -0
- data/LICENSE.txt +21 -0
- data/README.md +76 -0
- data/Rakefile +12 -0
- data/docker/esp32/Dockerfile +82 -0
- data/docker/pico/Dockerfile +77 -0
- data/exe/picoride +16 -0
- data/lib/picoride/background_job.rb +71 -0
- data/lib/picoride/project_store.rb +133 -0
- data/lib/picoride/r2p2_state.rb +137 -0
- data/lib/picoride/server.rb +1156 -0
- data/lib/picoride/version.rb +5 -0
- data/lib/picoride.rb +11 -0
- data/sig/picoruby/esp32/ide.rbs +8 -0
- data/web/funicular/README.md +111 -0
- data/web/funicular/ruby/components/directory_browser_dialog.rb +212 -0
- data/web/funicular/ruby/components/editor_app.rb +1560 -0
- data/web/funicular/ruby/components/log_panel.rb +102 -0
- data/web/funicular/ruby/components/menu_bar.rb +266 -0
- data/web/funicular/ruby/components/project_settings_dialog.rb +203 -0
- data/web/funicular/ruby/components/prompt_dialog.rb +76 -0
- data/web/funicular/ruby/components/toolbar.rb +30 -0
- data/web/funicular/ruby/components/tree_view.rb +112 -0
- data/web/funicular/ruby/main.rb +3 -0
- data/web/public/css/style.css +886 -0
- data/web/public/js/editor.js +257 -0
- data/web/views/index.erb +94 -0
- metadata +132 -0
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# ビルドログとプラットフォームセットアップログをまとめたタブ切り替えパネル。
|
|
2
|
+
#
|
|
3
|
+
# 以前は BuildPanel / PlatformPanel という別々のパネルを常時両方表示していたが、
|
|
4
|
+
# 同時に見る場面は少なく画面を圧迫するだけだったので、タブで切り替える1つの
|
|
5
|
+
# パネルに統合した。どちらのタブを表示中かは親(EditorApp)のstate(log_tab)に
|
|
6
|
+
# 持たせてあるので、タブを切り替えてもポーリングによるログ更新自体は
|
|
7
|
+
# バックグラウンドで両方継続する。
|
|
8
|
+
#
|
|
9
|
+
# 他のパネルと同じく表示専用。タブ切り替え/ログ更新は
|
|
10
|
+
# props[:on_tab_change] / props[:on_build_refresh] / props[:on_platform_refresh]
|
|
11
|
+
# 経由で親(EditorApp)に委譲する。
|
|
12
|
+
class LogPanel < Funicular::Component
|
|
13
|
+
BUILD_STATUS_LABELS = {
|
|
14
|
+
'idle' => 'ビルド未実行',
|
|
15
|
+
'running' => 'ビルド中…',
|
|
16
|
+
'success' => 'ビルド成功',
|
|
17
|
+
'failed' => 'ビルド失敗'
|
|
18
|
+
}.freeze
|
|
19
|
+
|
|
20
|
+
PLATFORM_STATUS_LABELS = {
|
|
21
|
+
'idle' => '未実行',
|
|
22
|
+
'running' => 'セットアップ中…',
|
|
23
|
+
'success' => 'セットアップ完了',
|
|
24
|
+
'failed' => 'セットアップ失敗'
|
|
25
|
+
}.freeze
|
|
26
|
+
|
|
27
|
+
def render
|
|
28
|
+
div(class: 'log-panel') do
|
|
29
|
+
div(class: 'log-panel-header') do
|
|
30
|
+
render_tab_button('build', 'R2P2 Build', :handle_select_build_tab)
|
|
31
|
+
render_tab_button('platform', 'Platform Setup', :handle_select_platform_tab)
|
|
32
|
+
div(class: 'log-panel-spacer')
|
|
33
|
+
span(class: 'platform-target') { props[:selected_platform] || '未選択' } unless build_tab?
|
|
34
|
+
render_refresh_button
|
|
35
|
+
span(class: "build-status #{status}") { status_label }
|
|
36
|
+
end
|
|
37
|
+
tag(:pre, class: 'build-log') { log_text }
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def handle_select_build_tab(event)
|
|
42
|
+
event.preventDefault
|
|
43
|
+
select_tab('build')
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def handle_select_platform_tab(event)
|
|
47
|
+
event.preventDefault
|
|
48
|
+
select_tab('platform')
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def handle_refresh(event)
|
|
52
|
+
event.preventDefault
|
|
53
|
+
on_refresh = build_tab? ? props[:on_build_refresh] : props[:on_platform_refresh]
|
|
54
|
+
on_refresh.call if on_refresh
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private
|
|
58
|
+
|
|
59
|
+
def select_tab(tab)
|
|
60
|
+
on_tab_change = props[:on_tab_change]
|
|
61
|
+
on_tab_change.call(tab) if on_tab_change
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def render_tab_button(key, label_text, handler)
|
|
65
|
+
classes = key == active_tab ? 'log-panel-tab active' : 'log-panel-tab'
|
|
66
|
+
button(class: classes, onclick: handler) { label_text }
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def render_refresh_button
|
|
70
|
+
button(class: 'build-refresh', onclick: :handle_refresh) { 'ログを更新' }
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def active_tab
|
|
74
|
+
props[:active_tab] == 'platform' ? 'platform' : 'build'
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def build_tab?
|
|
78
|
+
active_tab == 'build'
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def status
|
|
82
|
+
build_tab? ? props[:build_status] : props[:platform_status]
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def status_label
|
|
86
|
+
if build_tab?
|
|
87
|
+
BUILD_STATUS_LABELS[props[:build_status]] || props[:build_status].to_s
|
|
88
|
+
else
|
|
89
|
+
PLATFORM_STATUS_LABELS[props[:platform_status]] || props[:platform_status].to_s
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def log_text
|
|
94
|
+
if build_tab?
|
|
95
|
+
log = props[:build_log].to_s
|
|
96
|
+
props[:build_log_truncated] ? "…(冒頭は省略)\n#{log}" : log
|
|
97
|
+
else
|
|
98
|
+
log = props[:platform_log].to_s
|
|
99
|
+
props[:platform_log_truncated] ? "…(冒頭は省略)\n#{log}" : log
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
# 最上部のメニュー/グローバルステータスバー。
|
|
2
|
+
#
|
|
3
|
+
# 常設ドロップダウンは「プロジェクト」の切り替え用。プロジェクトが1つも
|
|
4
|
+
# 開かれていない(props[:known_projects]が空)間は、この場所に実質的に
|
|
5
|
+
# 「プロジェクトを開く」ボタンを出す(ドロップダウンの代わりにbuttonを
|
|
6
|
+
# 描画するだけで、同じprops[:on_open_project]を使う)。アプリ起動時に自動で
|
|
7
|
+
# ダイアログを出したりはしない。ユーザーがこのボタン/ファイルメニューから
|
|
8
|
+
# 明示的に開く操作をするまで、プロジェクトは開かれない。
|
|
9
|
+
#
|
|
10
|
+
# 以前は「複数プロジェクトを束ねるワークスペース」という上位概念があり、
|
|
11
|
+
# このドロップダウンはワークスペース選択用、プロジェクトの切り替えは別の
|
|
12
|
+
# ダイアログ(OpenProjectDialog)、という2階層構成だった。しかし
|
|
13
|
+
# 「ワークスペース=プロジェクトだった」というフィードバックにより2階層は
|
|
14
|
+
# 不要と判明し、1階層に統合した。このドロップダウンはそのままプロジェクト
|
|
15
|
+
# 選択用になり、OpenProjectDialog・「新しいプロジェクト」メニュー項目(旧・
|
|
16
|
+
# ワークスペース配下に名前でサブディレクトリを作る機能)は削除した
|
|
17
|
+
# (「新しいプロジェクト」はDirectoryBrowserDialogの「新しいフォルダ」+
|
|
18
|
+
# 「このディレクトリを開く」に統合されている)。
|
|
19
|
+
#
|
|
20
|
+
# ターゲットチップ・VM・USB Consoleは以前ここに常設セレクト/ビルド時ダイアログで
|
|
21
|
+
# 出していたが、「プロジェクトの設定としてプロジェクトに含めたい」という
|
|
22
|
+
# フィードバックを受けてプロジェクトごとの設定(`~/.picoride/project.toml`)に
|
|
23
|
+
# 持たせる方式に変更した。そのため、ここには設定値を編集する「設定」ボタンと、
|
|
24
|
+
# その設定値をそのまま使って実行するだけの「セットアップ」「ビルド開始」
|
|
25
|
+
# ボタンだけが残っている。
|
|
26
|
+
# 左端には「ファイル」メニュー(プロジェクトを開く/mrbgemを追加/新規ファイル/
|
|
27
|
+
# 新しいフォルダ)を置いてある。開閉状態はここでは持たず(理由は
|
|
28
|
+
# editor_app.rbの「子コンポーネントは親の再描画のたびに作り直される」の節参照)、
|
|
29
|
+
# props[:file_menu_open]として親(EditorApp)のstateをそのまま反映するだけにしてある。
|
|
30
|
+
# 他のパネルと同じく表示専用で、実処理(状態管理・API呼び出し)は
|
|
31
|
+
# props[:on_project_select] / props[:on_settings] / props[:on_setup] /
|
|
32
|
+
# props[:on_build] / props[:on_file_menu_toggle] / props[:on_open_project] /
|
|
33
|
+
# props[:on_add_mrbgem] / props[:on_new_file] / props[:on_new_folder]
|
|
34
|
+
# 経由で親(EditorApp)に委譲する。
|
|
35
|
+
#
|
|
36
|
+
# props[:project] は現在開いているプロジェクトの絶対パス(未設定ならnil)。
|
|
37
|
+
# props[:known_projects] はこのセッションで一度でも開いたプロジェクトの
|
|
38
|
+
# 絶対パスの配列(ドロップダウンの選択肢)。
|
|
39
|
+
class MenuBar < Funicular::Component
|
|
40
|
+
STATUS_LABELS = {
|
|
41
|
+
'idle' => 'ビルド未実行',
|
|
42
|
+
'running' => 'ビルド中…',
|
|
43
|
+
'success' => 'ビルド成功',
|
|
44
|
+
'failed' => 'ビルド失敗'
|
|
45
|
+
}.freeze
|
|
46
|
+
|
|
47
|
+
def render
|
|
48
|
+
div(class: 'menu-bar') do
|
|
49
|
+
span(class: 'menu-bar-title') { 'PicoRIDE' }
|
|
50
|
+
render_file_menu
|
|
51
|
+
render_project_select
|
|
52
|
+
div(class: 'menu-bar-spacer')
|
|
53
|
+
render_settings_button
|
|
54
|
+
render_setup_button
|
|
55
|
+
render_build_button
|
|
56
|
+
render_status_pill
|
|
57
|
+
render_install_button
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def handle_file_menu_toggle(event)
|
|
62
|
+
event.preventDefault
|
|
63
|
+
on_file_menu_toggle = props[:on_file_menu_toggle]
|
|
64
|
+
on_file_menu_toggle.call if on_file_menu_toggle
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def handle_open_project(event)
|
|
68
|
+
event.preventDefault
|
|
69
|
+
on_open_project = props[:on_open_project]
|
|
70
|
+
on_open_project.call if on_open_project
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def handle_add_mrbgem(event)
|
|
74
|
+
event.preventDefault
|
|
75
|
+
on_add_mrbgem = props[:on_add_mrbgem]
|
|
76
|
+
on_add_mrbgem.call if on_add_mrbgem
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def handle_new_file(event)
|
|
80
|
+
event.preventDefault
|
|
81
|
+
on_new_file = props[:on_new_file]
|
|
82
|
+
on_new_file.call if on_new_file
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def handle_new_folder(event)
|
|
86
|
+
event.preventDefault
|
|
87
|
+
on_new_folder = props[:on_new_folder]
|
|
88
|
+
on_new_folder.call if on_new_folder
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# <select> の変更イベント。選ばれたプロジェクトの絶対パスをそのまま渡す。
|
|
92
|
+
def handle_project_change(event)
|
|
93
|
+
node = refs[:project_select]
|
|
94
|
+
return unless node
|
|
95
|
+
|
|
96
|
+
value = node[:value].to_s
|
|
97
|
+
return if value.empty?
|
|
98
|
+
|
|
99
|
+
on_project_select = props[:on_project_select]
|
|
100
|
+
on_project_select.call(value) if on_project_select
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def handle_settings(event)
|
|
104
|
+
event.preventDefault
|
|
105
|
+
on_settings = props[:on_settings]
|
|
106
|
+
on_settings.call if on_settings
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def handle_setup(event)
|
|
110
|
+
event.preventDefault
|
|
111
|
+
on_setup = props[:on_setup]
|
|
112
|
+
on_setup.call if on_setup
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def handle_build(event)
|
|
116
|
+
event.preventDefault
|
|
117
|
+
on_build = props[:on_build]
|
|
118
|
+
on_build.call if on_build
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
private
|
|
122
|
+
|
|
123
|
+
def render_file_menu
|
|
124
|
+
div(class: 'file-menu') do
|
|
125
|
+
button(class: 'file-menu-toggle', onclick: :handle_file_menu_toggle) { 'ファイル' }
|
|
126
|
+
render_file_menu_dropdown if props[:file_menu_open]
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def render_file_menu_dropdown
|
|
131
|
+
div(class: 'file-menu-dropdown') do
|
|
132
|
+
button(class: 'file-menu-item', onclick: :handle_open_project) { 'プロジェクトを開く…' }
|
|
133
|
+
render_add_mrbgem_menu_item
|
|
134
|
+
render_new_file_menu_item
|
|
135
|
+
render_new_folder_menu_item
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# mrbgem追加・新規ファイル作成はプロジェクトを開いていないと行き先が無いので、
|
|
140
|
+
# 未選択時は押せないようにする(disabled: false を渡す書き方はこのDSLでは
|
|
141
|
+
# 効かないため、分岐で二通りの要素を書き分ける。toolbar.rbのrender_save_button参照)。
|
|
142
|
+
def render_add_mrbgem_menu_item
|
|
143
|
+
if props[:project].to_s.empty?
|
|
144
|
+
button(class: 'file-menu-item', disabled: true) { 'mrbgemを追加…' }
|
|
145
|
+
else
|
|
146
|
+
button(class: 'file-menu-item', onclick: :handle_add_mrbgem) { 'mrbgemを追加…' }
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def render_new_file_menu_item
|
|
151
|
+
if props[:project].to_s.empty?
|
|
152
|
+
button(class: 'file-menu-item', disabled: true) { '新規ファイル…' }
|
|
153
|
+
else
|
|
154
|
+
button(class: 'file-menu-item', onclick: :handle_new_file) { '新規ファイル…' }
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def render_new_folder_menu_item
|
|
159
|
+
if props[:project].to_s.empty?
|
|
160
|
+
button(class: 'file-menu-item', disabled: true) { '新しいフォルダ…' }
|
|
161
|
+
else
|
|
162
|
+
button(class: 'file-menu-item', onclick: :handle_new_folder) { '新しいフォルダ…' }
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# プロジェクトを1つも開いたことが無ければ(known_projectsが空)、<select>の
|
|
167
|
+
# 代わりに「プロジェクトを開く」ボタンを出す(props[:on_open_project]、
|
|
168
|
+
# ファイルメニューの項目と同じコールバック)。1つ以上開いていれば、それらを
|
|
169
|
+
# 選択肢に持つ<select>にする(選ぶとon_project_select経由で即座に切り替わる)。
|
|
170
|
+
def render_project_select
|
|
171
|
+
known = props[:known_projects] || []
|
|
172
|
+
|
|
173
|
+
if known.empty?
|
|
174
|
+
button(class: 'menu-project-open', onclick: :handle_open_project) { 'プロジェクトを開く…' }
|
|
175
|
+
else
|
|
176
|
+
tag(:select, class: 'menu-project-select', ref: :project_select, onchange: :handle_project_change,
|
|
177
|
+
title: props[:project].to_s) { render_project_options(known) }
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def render_project_options(known)
|
|
182
|
+
known.each do |path|
|
|
183
|
+
label = project_label(path)
|
|
184
|
+
if path == props[:project]
|
|
185
|
+
tag(:option, value: path, selected: true) { label }
|
|
186
|
+
else
|
|
187
|
+
tag(:option, value: path) { label }
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# プロジェクト名は既定でディレクトリ名(パスの最後のセグメント)にする。
|
|
193
|
+
# PicoRuby.wasm上ではFile.basenameのようなFile系メソッドの対応が不確かなため
|
|
194
|
+
# (editor_app.rbのextensionヘルパーと同じ理由)、素の文字列分割で求める。
|
|
195
|
+
def project_label(path)
|
|
196
|
+
segments = path.to_s.split('/')
|
|
197
|
+
name = segments.last
|
|
198
|
+
(name.nil? || name.empty?) ? path.to_s : name
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# 設定はプロジェクト単位(project_store.rb参照)なので、プロジェクト未設定の
|
|
202
|
+
# 間は保存先が無く押せない。
|
|
203
|
+
def render_settings_button
|
|
204
|
+
if props[:project].to_s.empty?
|
|
205
|
+
button(class: 'menu-settings', disabled: true) { '設定' }
|
|
206
|
+
else
|
|
207
|
+
button(class: 'menu-settings', onclick: :handle_settings) { '設定' }
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
# ターゲットが未設定なら押せない(設定ダイアログで選んでもらう)。
|
|
212
|
+
def render_setup_button
|
|
213
|
+
platform = value_of(props[:project_config], 'platform')
|
|
214
|
+
|
|
215
|
+
if props[:platform_building]
|
|
216
|
+
button(class: 'menu-setup', disabled: true) { 'セットアップ中…' }
|
|
217
|
+
elsif platform.to_s.empty?
|
|
218
|
+
button(class: 'menu-setup', disabled: true) { 'ターゲット未設定' }
|
|
219
|
+
else
|
|
220
|
+
button(class: 'menu-setup', onclick: :handle_setup) { "#{platform}をセットアップ" }
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def render_build_button
|
|
225
|
+
if props[:building]
|
|
226
|
+
button(class: 'menu-build', disabled: true) { 'ビルド中…' }
|
|
227
|
+
else
|
|
228
|
+
button(class: 'menu-build', onclick: :handle_build) { 'ビルド開始' }
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
def render_status_pill
|
|
233
|
+
span(class: "menu-bar-status #{props[:build_status]}") { status_label }
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
# ESP32: 実際の書き込み処理はブラウザのWeb Serial API経由でESP Web Tools
|
|
237
|
+
# (esp-web-install-button カスタム要素)が行う。旧 install_panel.rb と同じ使い方。
|
|
238
|
+
# Raspberry Pi Pico: ブラウザから直接書き込む仕組みは使わず、ビルド成果物の
|
|
239
|
+
# .uf2をダウンロードさせる(BOOTSELを押しながら接続して現れるドライブへ
|
|
240
|
+
# ユーザーがコピーする)。<a download>をボタン風に見せている。
|
|
241
|
+
def render_install_button
|
|
242
|
+
if props[:project].to_s.empty?
|
|
243
|
+
button(class: 'menu-install', disabled: true) { 'デバイスにインストール' }
|
|
244
|
+
elsif ProjectSettingsDialog.pico?(value_of(props[:project_config], 'platform'))
|
|
245
|
+
tag(:a, class: 'menu-install menu-download', href: '/api/firmware/uf2', download: '',
|
|
246
|
+
title: 'BOOTSELボタンを押しながらPicoを接続し、現れたドライブへUF2ファイルをコピーしてください') do
|
|
247
|
+
'UF2をダウンロード'
|
|
248
|
+
end
|
|
249
|
+
else
|
|
250
|
+
tag(:'esp-web-install-button', manifest: '/api/firmware/manifest.json') do
|
|
251
|
+
button(slot: 'activate', class: 'menu-install') { 'デバイスにインストール' }
|
|
252
|
+
span(slot: 'unsupported', class: 'menu-bar-hint') { '未対応ブラウザ' }
|
|
253
|
+
span(slot: 'not-allowed', class: 'menu-bar-hint') { 'HTTPS必須' }
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
def status_label
|
|
259
|
+
STATUS_LABELS[props[:build_status]] || props[:build_status].to_s
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
def value_of(data, key)
|
|
263
|
+
return nil unless data
|
|
264
|
+
data[key] || data[key.to_sym]
|
|
265
|
+
end
|
|
266
|
+
end
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
# プロジェクトごとの設定(ターゲットチップ・VM・USB Console)を編集するダイアログ。
|
|
2
|
+
#
|
|
3
|
+
# 以前はターゲットをメニューバー常設のセレクトで、VM/USB Consoleをビルド開始の
|
|
4
|
+
# たびに出るダイアログで選ばせていたが、「プロジェクトの設定としてプロジェクトに
|
|
5
|
+
# 含めたい」というフィードバックを受けて永続化する方式に変更した。永続化先は
|
|
6
|
+
# プロジェクトごとの隠しファイル(`.config.yml`)→(一時期あった「複数プロジェクトを
|
|
7
|
+
# 束ねるワークスペース」という上位概念の下で)`~/.picoride/workspace.toml`、と
|
|
8
|
+
# 経緯を経て、現在は`~/.picoride/project.toml`(`project_store.rb`)に落ち着いている。
|
|
9
|
+
# このダイアログはその値を編集して保存するためだけのもので、保存後は
|
|
10
|
+
# 「プラットフォームをセットアップ」「ビルド開始」の各ボタンがこの設定値を
|
|
11
|
+
# そのまま使う(都度選び直す必要はない)。
|
|
12
|
+
#
|
|
13
|
+
# 他のパネルと同じく表示専用。値の変更/保存/キャンセルは
|
|
14
|
+
# props[:on_platform_change] / props[:on_vm_change] / props[:on_usb_console_change] /
|
|
15
|
+
# props[:on_save] / props[:on_cancel] 経由で親(EditorApp)に委譲する。
|
|
16
|
+
#
|
|
17
|
+
# ターゲットはESP32系とRaspberry Pi Pico系の2ファミリーに分かれていて、
|
|
18
|
+
# どちらを選んだかでサーバー側のビルド用Dockerイメージ・ビルド手順が切り替わる
|
|
19
|
+
# (lib/picoride/server.rb の PLATFORM_FAMILIES / BUILDER_IMAGES)。
|
|
20
|
+
# Picoでは「USB Console」はESP32専用の設定なので出さず、VMはボードによって
|
|
21
|
+
# 選べるものが違う(RP2040のpico/pico_wはFemtoRubyのみ)。
|
|
22
|
+
class ProjectSettingsDialog < Funicular::Component
|
|
23
|
+
# [optgroupのラベル, [[値, 表示名], ...]] の並び。値はサーバー側の
|
|
24
|
+
# PLATFORM_FAMILIES と一致させる。
|
|
25
|
+
PLATFORM_GROUPS = [
|
|
26
|
+
['ESP32', [
|
|
27
|
+
['esp32', 'esp32'],
|
|
28
|
+
['esp32c3', 'esp32c3'],
|
|
29
|
+
['esp32c6', 'esp32c6'],
|
|
30
|
+
['esp32h2', 'esp32h2'],
|
|
31
|
+
['esp32p4', 'esp32p4'],
|
|
32
|
+
['esp32s3', 'esp32s3']
|
|
33
|
+
]],
|
|
34
|
+
['Raspberry Pi Pico', [
|
|
35
|
+
['pico', 'Pico (RP2040)'],
|
|
36
|
+
['pico_w', 'Pico W (RP2040)'],
|
|
37
|
+
['pico2', 'Pico 2 (RP2350)'],
|
|
38
|
+
['pico2_w', 'Pico 2 W (RP2350)']
|
|
39
|
+
]]
|
|
40
|
+
].freeze
|
|
41
|
+
|
|
42
|
+
# 空文字は「デフォルトVM」を意味する(ESP32: idf.py buildにVM未指定、
|
|
43
|
+
# Pico: FemtoRuby)。
|
|
44
|
+
VM_OPTIONS = [
|
|
45
|
+
['', 'デフォルト'],
|
|
46
|
+
['femtoruby', 'FemtoRuby (mruby/c)'],
|
|
47
|
+
['picoruby', 'PicoRuby (mruby)']
|
|
48
|
+
].freeze
|
|
49
|
+
|
|
50
|
+
# Picoの各ボードで選べるVM(サーバー側のPICO_BOARD_VMSと一致させる)。
|
|
51
|
+
PICO_BOARD_VMS = {
|
|
52
|
+
'pico' => ['femtoruby'],
|
|
53
|
+
'pico_w' => ['femtoruby'],
|
|
54
|
+
'pico2' => ['femtoruby', 'picoruby'],
|
|
55
|
+
'pico2_w' => ['femtoruby', 'picoruby']
|
|
56
|
+
}.freeze
|
|
57
|
+
|
|
58
|
+
PICO_NOTE = 'ビルドするとUF2ファイルができます。BOOTSELボタンを押しながらPicoをUSB接続し、' +
|
|
59
|
+
'現れたドライブ(RPI-RP2 / RP2350)へコピーすると書き込めます。' +
|
|
60
|
+
'app/ はターミナルタブの「app/ をアップロード」で転送してください。'
|
|
61
|
+
|
|
62
|
+
def self.pico?(platform)
|
|
63
|
+
PICO_BOARD_VMS.key?(platform.to_s)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# そのターゲットで選べるVMかどうか(空文字=デフォルトは常に可)。
|
|
67
|
+
# ターゲットを切り替えたときに、選べなくなったVMをデフォルトに戻すのに使う
|
|
68
|
+
# (EditorAppのon_platform_change)。
|
|
69
|
+
def self.vm_allowed?(platform, vm)
|
|
70
|
+
return true if vm.to_s.empty?
|
|
71
|
+
return true unless pico?(platform)
|
|
72
|
+
|
|
73
|
+
PICO_BOARD_VMS[platform.to_s].include?(vm.to_s)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def render
|
|
77
|
+
div(class: 'dialog-overlay') do
|
|
78
|
+
div(class: 'dialog settings-dialog') do
|
|
79
|
+
h2 { "プロジェクト設定(#{props[:project_path]})" }
|
|
80
|
+
div(class: 'build-options') do
|
|
81
|
+
render_platform_select
|
|
82
|
+
render_vm_select
|
|
83
|
+
if ProjectSettingsDialog.pico?(props[:platform])
|
|
84
|
+
render_pico_note
|
|
85
|
+
else
|
|
86
|
+
render_usb_console_checkbox
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
div(class: 'dialog-actions') do
|
|
90
|
+
button(class: 'dialog-cancel', onclick: :handle_cancel) { 'キャンセル' }
|
|
91
|
+
button(class: 'dialog-confirm', onclick: :handle_save) { '保存' }
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def handle_cancel(event)
|
|
98
|
+
event.preventDefault
|
|
99
|
+
on_cancel = props[:on_cancel]
|
|
100
|
+
on_cancel.call if on_cancel
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def handle_save(event)
|
|
104
|
+
event.preventDefault
|
|
105
|
+
on_save = props[:on_save]
|
|
106
|
+
on_save.call if on_save
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def handle_platform_change(event)
|
|
110
|
+
node = refs[:platform_select]
|
|
111
|
+
return unless node
|
|
112
|
+
value = node[:value].to_s
|
|
113
|
+
on_platform_change = props[:on_platform_change]
|
|
114
|
+
on_platform_change.call(value.empty? ? nil : value) if on_platform_change
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def handle_vm_change(event)
|
|
118
|
+
node = refs[:vm_select]
|
|
119
|
+
return unless node
|
|
120
|
+
on_vm_change = props[:on_vm_change]
|
|
121
|
+
on_vm_change.call(node[:value].to_s) if on_vm_change
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def handle_usb_console_change(event)
|
|
125
|
+
node = refs[:usb_console_checkbox]
|
|
126
|
+
return unless node
|
|
127
|
+
on_usb_console_change = props[:on_usb_console_change]
|
|
128
|
+
on_usb_console_change.call(truthy?(node[:checked])) if on_usb_console_change
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
private
|
|
132
|
+
|
|
133
|
+
def render_platform_select
|
|
134
|
+
label(class: 'build-option') do
|
|
135
|
+
span { 'ターゲット' }
|
|
136
|
+
tag(:select, ref: :platform_select, onchange: :handle_platform_change) { render_platform_options }
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def render_platform_options
|
|
141
|
+
if props[:platform].to_s.empty?
|
|
142
|
+
tag(:option, value: '', selected: true) { '未選択' }
|
|
143
|
+
else
|
|
144
|
+
tag(:option, value: '') { '未選択' }
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
PLATFORM_GROUPS.each do |group_label, options|
|
|
148
|
+
tag(:optgroup, label: group_label) do
|
|
149
|
+
options.each do |value, label_text|
|
|
150
|
+
if value == props[:platform]
|
|
151
|
+
tag(:option, value: value, selected: true) { label_text }
|
|
152
|
+
else
|
|
153
|
+
tag(:option, value: value) { label_text }
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def render_vm_select
|
|
161
|
+
label(class: 'build-option') do
|
|
162
|
+
span { 'VM' }
|
|
163
|
+
tag(:select, ref: :vm_select, onchange: :handle_vm_change) { render_vm_options }
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def render_vm_options
|
|
168
|
+
VM_OPTIONS.each do |value, label_text|
|
|
169
|
+
next unless ProjectSettingsDialog.vm_allowed?(props[:platform], value)
|
|
170
|
+
|
|
171
|
+
if value == props[:vm]
|
|
172
|
+
tag(:option, value: value, selected: true) { label_text }
|
|
173
|
+
else
|
|
174
|
+
tag(:option, value: value) { label_text }
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def render_usb_console_checkbox
|
|
180
|
+
label(class: 'build-option') do
|
|
181
|
+
render_usb_console_input
|
|
182
|
+
span { 'USB Console(外部UART変換チップなしのボード向け)' }
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def render_pico_note
|
|
187
|
+
div(class: 'build-option-note') do
|
|
188
|
+
span { PICO_NOTE }
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def render_usb_console_input
|
|
193
|
+
if props[:usb_console]
|
|
194
|
+
tag(:input, type: 'checkbox', ref: :usb_console_checkbox, checked: true, onchange: :handle_usb_console_change)
|
|
195
|
+
else
|
|
196
|
+
tag(:input, type: 'checkbox', ref: :usb_console_checkbox, onchange: :handle_usb_console_change)
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def truthy?(js_value)
|
|
201
|
+
js_value.to_s == 'true'
|
|
202
|
+
end
|
|
203
|
+
end
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# 1行のテキスト入力を伴う汎用的な確認ダイアログ。
|
|
2
|
+
#
|
|
3
|
+
# 「新しいプロジェクト」「mrbgemを追加」「新規ファイル」はどれも
|
|
4
|
+
# 名前(パス)を1つ入力させて確定/キャンセルさせるだけなので、用途ごとに
|
|
5
|
+
# 別コンポーネントを作らずタイトル/ラベル/プレースホルダ/確定ボタンの
|
|
6
|
+
# ラベルをpropsで差し替えて使い回す。
|
|
7
|
+
#
|
|
8
|
+
# 入力欄はref経由でDOM値を直接読む非制御コンポーネントにしてある。
|
|
9
|
+
# ビルドログのポーリング等、ダイアログを開いたまま親(EditorApp)が
|
|
10
|
+
# 再描画される場面があるため、value を props 経由で毎回書き戻す制御方式にすると
|
|
11
|
+
# その再描画のたびに入力中の文字が消えてしまう(他のフォーム系コンポーネントと
|
|
12
|
+
# 同じ理由。project_settings_dialog.rb / editor_app.rb 参照)。
|
|
13
|
+
#
|
|
14
|
+
# props:
|
|
15
|
+
# title: ダイアログの見出し
|
|
16
|
+
# label: 入力欄のラベル
|
|
17
|
+
# placeholder: 入力欄のプレースホルダ
|
|
18
|
+
# confirm_label: 確定ボタンのラベル(省略時 'OK')
|
|
19
|
+
# error: エラーメッセージ(あれば表示)
|
|
20
|
+
# on_confirm: 入力値(String)を渡して呼ばれるlambda
|
|
21
|
+
# on_cancel: キャンセル時に呼ばれるlambda
|
|
22
|
+
class PromptDialog < Funicular::Component
|
|
23
|
+
def render
|
|
24
|
+
div(class: 'dialog-overlay') do
|
|
25
|
+
div(class: 'dialog prompt-dialog') do
|
|
26
|
+
h2 { props[:title].to_s }
|
|
27
|
+
label(class: 'prompt-field') do
|
|
28
|
+
span { props[:label].to_s }
|
|
29
|
+
tag(:input, type: 'text', ref: :value_input, placeholder: props[:placeholder].to_s, onkeydown: :handle_keydown)
|
|
30
|
+
end
|
|
31
|
+
render_error
|
|
32
|
+
div(class: 'dialog-actions') do
|
|
33
|
+
button(class: 'dialog-cancel', onclick: :handle_cancel) { 'キャンセル' }
|
|
34
|
+
button(class: 'dialog-confirm', onclick: :handle_confirm) { confirm_label }
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def handle_cancel(event)
|
|
41
|
+
event.preventDefault
|
|
42
|
+
on_cancel = props[:on_cancel]
|
|
43
|
+
on_cancel.call if on_cancel
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def handle_confirm(event)
|
|
47
|
+
event.preventDefault
|
|
48
|
+
confirm
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Enterキーでも確定できるようにする(<form>で囲んでいないのでブラウザ標準の
|
|
52
|
+
# submit挙動とは衝突しない)。
|
|
53
|
+
def handle_keydown(event)
|
|
54
|
+
confirm if event[:key].to_s == 'Enter'
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private
|
|
58
|
+
|
|
59
|
+
def confirm
|
|
60
|
+
node = refs[:value_input]
|
|
61
|
+
return unless node
|
|
62
|
+
|
|
63
|
+
on_confirm = props[:on_confirm]
|
|
64
|
+
on_confirm.call(node[:value].to_s) if on_confirm
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def confirm_label
|
|
68
|
+
label = props[:confirm_label].to_s
|
|
69
|
+
label.empty? ? 'OK' : label
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def render_error
|
|
73
|
+
return if props[:error].to_s.empty?
|
|
74
|
+
div(class: 'prompt-error') { props[:error].to_s }
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# エディタ上部のツールバー(ファイル名 / 保存ボタン / ステータス表示)。
|
|
2
|
+
#
|
|
3
|
+
# こちらも表示専用。保存の実処理は親が持ち、props[:on_save] 経由で呼び出す。
|
|
4
|
+
class Toolbar < Funicular::Component
|
|
5
|
+
def render
|
|
6
|
+
div(class: 'toolbar') do
|
|
7
|
+
span(class: 'current-file') { props[:current_path] || 'ファイルを選択してください' }
|
|
8
|
+
render_save_button
|
|
9
|
+
span(class: "status #{props[:status_kind]}") { props[:status].to_s }
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def handle_save(event)
|
|
14
|
+
event.preventDefault
|
|
15
|
+
on_save = props[:on_save]
|
|
16
|
+
on_save.call if on_save
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
# disabled は「属性を付けない/付ける」で切り替える。
|
|
22
|
+
# false を渡す書き方に依存しないよう、分岐で二通りの要素を書き分けている。
|
|
23
|
+
def render_save_button
|
|
24
|
+
if props[:dirty]
|
|
25
|
+
button(class: 'save', onclick: :handle_save) { '保存 (Ctrl+S)' }
|
|
26
|
+
else
|
|
27
|
+
button(class: 'save', disabled: true) { '保存 (Ctrl+S)' }
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|