44``_show_action_menu`` / ``_fallback_select_and_up`` をこのモジュールへ移送し、
55メニュー部品は ``tui.menu`` に、ハンドラ委譲は ``tui.dispatch`` に一般化した。
66
7- PR1 で扱うのは既存の **一覧選択 → (running なら up/rebuild/down サブメニュー) →
8- それ以外は直接 up** までで、login/ps/logs/scale/build の追加は PR2 で行う。
7+ PR1 で **一覧選択 → (running なら操作サブメニュー) → それ以外は直接 up** を移送し、
8+ PR2 で running 操作サブメニューを **up/down/login/ps/logs/scale/build/rebuild の全操作**
9+ へ拡張した。login/ps/logs/scale は running 中コンテナを対象とするため running 行限定、
10+ stopped/unknown は従来どおり直接 up (PR1 非回帰)。引数を要する操作は ``tui.menu`` の
11+ 収集ヘルパで CLI と同じ属性値を集め、破壊的な down は ``menu.confirm`` で確認する
12+ (plan 2.3 契約表 / 3.4 破壊的操作確認)。
913
1014一覧表示・整形 (``list_projects`` / ``_build_menu_entries``) は ``commands/project``
1115の純粋ロジックを再利用する (TUI からも CLI(table) からも共有)。
@@ -41,36 +45,180 @@ def _select_project(rows: list[dict]):
4145 choices , back = True , search = True )
4246
4347
48+ # running 行で選べる操作 (表示順 = ハイライト既定順)。up を先頭に置き、PR1 同様
49+ # Enter 連打で再起動へ到達できるようにする。各 value は cmd_project のサブコマンド名。
50+ _RUNNING_OPS : list [tuple [str , str ]] = [
51+ ("再起動 (up)" , "up" ),
52+ ("停止 (down)" , "down" ),
53+ ("ログイン (login)" , "login" ),
54+ ("コンテナ状態 (ps)" , "ps" ),
55+ ("ログ表示 (logs)" , "logs" ),
56+ ("スケール変更 (scale)" , "scale" ),
57+ ("イメージビルド (build)" , "build" ),
58+ ("再ビルド (rebuild --no-cache)" , "rebuild" ),
59+ ]
60+
61+ # 引数収集を Esc/Ctrl-C で中止したことを示す番兵 (= サブメニューへ戻る)。
62+ # dispatch の rc (int) や ``None`` (= 全体中止) と区別する。
63+ _ARG_CANCEL = object ()
64+
65+
4466def _select_action (name : str ):
45- """running 中プロジェクトの操作 (up/rebuild/down) を選ぶサブメニュー 。
67+ """running 中プロジェクトの操作を選ぶサブメニュー 。
4668
47- 戻り値: action 文字列 / ``MENU_BACK`` (Esc・← → 一覧へ戻る) / ``None`` (Ctrl-C 中止)。
69+ 戻り値: サブコマンド文字列 / ``MENU_BACK`` (Esc・← → 一覧へ戻る) / ``None`` (Ctrl-C 中止)。
4870 """
49- choices = [
50- ("再起動 (up)" , "up" ),
51- ("再ビルド (rebuild --no-cache)" , "rebuild" ),
52- ("停止 (down)" , "down" ),
53- ]
5471 return menu .select (
5572 f"'{ name } ' は起動中です。操作を選択 "
5673 "(↑↓ 移動 / Enter 決定 / ←・Esc 戻る / Ctrl-C 中止):" ,
74+ list (_RUNNING_OPS ), back = True , search = False )
75+
76+
77+ def _optional_int (message : str , * , min_value : int = 0 ):
78+ """空入力を許す整数収集 (logs --tail 等)。
79+
80+ 戻り値: ``int`` / ``None`` (空入力 = 既定動作) / ``_ARG_CANCEL`` (Esc・Ctrl-C 中止)。
81+ 非数値・``min_value`` 未満は再入力を促す。``menu.integer`` は空入力で既定値を返す
82+ 仕様のため、空 = None を表現したい optional な数値はこちらで扱う。``min_value`` の
83+ 既定は 0 で、logs --tail に負数を渡して docker compose をエラーにするのを防ぐ。
84+ """
85+ while True :
86+ raw = menu .text (message , allow_empty = True )
87+ if raw is None :
88+ return _ARG_CANCEL
89+ if raw == "" :
90+ return None
91+ try :
92+ value = int (raw )
93+ except ValueError :
94+ logger .error ("整数で指定してください: %r" , raw )
95+ continue
96+ if value < min_value :
97+ logger .error ("%d 以上で指定してください。" , min_value )
98+ continue
99+ return value
100+
101+
102+ def _select_build_image (devbase_root : Path ):
103+ """build 対象イメージを選ぶ。``containers/<image>/Dockerfile`` を列挙する。
104+
105+ 戻り値: イメージ名 (``str``) / ``None`` (compose.yml 全体ビルド) / ``_ARG_CANCEL``
106+ (Esc・Ctrl-C 中止)。``containers/`` が無い / 空なら compose.yml 全体ビルド (None) に
107+ フォールバックする。
108+ """
109+ containers_dir = Path (devbase_root ) / "containers"
110+ images = sorted (
111+ d .name for d in containers_dir .iterdir ()
112+ if d .is_dir () and (d / "Dockerfile" ).exists ()
113+ ) if containers_dir .is_dir () else []
114+
115+ if not images :
116+ # 個別イメージが無ければ compose.yml 全体ビルド (image=None) のみ。
117+ return None
118+
119+ # value="" を「compose.yml 全体」に割り当て、選択メニューの None (Ctrl-C) と衝突
120+ # させない。呼び出し側で空文字 → None へ変換する。
121+ choices = [("compose.yml 全体をビルド" , "" )] + [(img , img ) for img in images ]
122+ sel = menu .select (
123+ "ビルドするイメージを選択 (↑↓ 移動 / Enter 決定 / ←・Esc 戻る / Ctrl-C 中止):" ,
57124 choices , back = True , search = False )
125+ if sel is menu .MENU_BACK or sel is None :
126+ return _ARG_CANCEL
127+ return sel or None # "" → None (compose 全体)
128+
129+
130+ def _run_operation (devbase_root : Path , name : str , op : str ):
131+ """選択された操作の引数を収集して ``dispatch_lifecycle`` で起動する。
132+
133+ 戻り値: dispatch の rc (``int``) / ``_ARG_CANCEL`` (引数収集を中止 = サブメニューへ戻る)。
134+ 引数を要さない up/rebuild は即実行。down は破壊的のため ``menu.confirm`` で確認する。
135+ """
136+ if op in ("up" , "rebuild" ):
137+ # up は scale 属性を参照する (常に None。他コマンドは無視する)。
138+ return dispatch_lifecycle (op , name , scale = None )
139+
140+ if op == "down" :
141+ ok = menu .confirm (f"'{ name } ' のコンテナを停止しますか?" , default = False )
142+ if not ok : # False (拒否) / None (中止) → 実行しない
143+ return _ARG_CANCEL
144+ return dispatch_lifecycle ("down" , name )
145+
146+ if op == "login" :
147+ # menu.text は空入力 (既定値を消して確定) で "" を返し、wrapper で --index=
148+ # と展開されてコマンドが失敗する。menu.integer なら空入力は default=1 を返し、
149+ # min_value=1 で正の整数を保証する。cmd_login の index は文字列契約なので str 化。
150+ index = menu .integer ("ログインするコンテナ番号" , default = 1 , min_value = 1 )
151+ if index is None :
152+ return _ARG_CANCEL
153+ return dispatch_lifecycle ("login" , name , index = str (index ))
154+
155+ if op == "ps" :
156+ all_c = menu .confirm ("停止中も含め全コンテナを表示しますか (--all)?" , default = False )
157+ if all_c is None :
158+ return _ARG_CANCEL
159+ return dispatch_lifecycle ("ps" , name , all = all_c )
160+
161+ if op == "logs" :
162+ follow = menu .confirm ("ログを追従表示しますか (--follow)?" , default = False )
163+ if follow is None :
164+ return _ARG_CANCEL
165+ tail = _optional_int ("末尾何行を表示しますか (空で全件)" )
166+ if tail is _ARG_CANCEL :
167+ return _ARG_CANCEL
168+ return dispatch_lifecycle ("logs" , name , follow = follow , tail = tail )
169+
170+ if op == "scale" :
171+ new_scale = menu .integer (f"'{ name } ' の新しいコンテナ数" , min_value = 1 )
172+ if new_scale is None :
173+ return _ARG_CANCEL
174+ return dispatch_lifecycle ("scale" , name , new_scale = new_scale )
175+
176+ if op == "build" :
177+ image = _select_build_image (devbase_root )
178+ if image is _ARG_CANCEL :
179+ return _ARG_CANCEL
180+ return dispatch_lifecycle ("build" , name , image = image )
181+
182+ # 到達しない (メニュー値は _RUNNING_OPS に限定される)。保守的に no-op。
183+ logger .error ("未知の操作です: %s" , op )
184+ return _ARG_CANCEL
185+
186+
187+ def _operation_menu (devbase_root : Path , name : str ):
188+ """running 行の操作サブメニューを回す。
189+
190+ 戻り値プロトコル (run と同じ ``is`` 同一性判定):
191+ - dispatch の rc (``int``): 操作を実行 → 呼び出し元へ (最終的にトップへ復帰)。
192+ - ``menu.MENU_BACK``: Esc/← で一覧へ戻る。
193+ - ``None``: Ctrl-C で全体中止。
194+
195+ 引数収集を中止 (``_ARG_CANCEL``) した場合はサブメニューを再表示する。
196+ """
197+ while True :
198+ op = _select_action (name )
199+ if op is menu .MENU_BACK :
200+ return menu .MENU_BACK
201+ if op is None :
202+ return None
203+ rc = _run_operation (devbase_root , name , op )
204+ if rc is _ARG_CANCEL :
205+ continue # 引数収集を中止 → サブメニューへ戻る
206+ return rc # 実行 rc → 呼び出し元へ
58207
59208
60209def run (devbase_root : Path ):
61- """プロジェクト操作カテゴリ。一覧選択 → up/rebuild/down を起動する 。
210+ """プロジェクト操作カテゴリ。一覧選択 → (running は操作サブメニュー / 他は up) 。
62211
63212 戻り値プロトコル (トップループが ``is`` 同一性で判定する):
64213 - **操作を実行した場合**: ``dispatch_lifecycle`` の rc (``int``) を返す。
65214 「実行したのでトップへ戻る、rc は呼び出し側が記憶」の意味。これにより
66- `` project up/down/rebuild`` の失敗が ``devbase list`` の終了コードへ伝搬する。
215+ project 操作の失敗が ``devbase list`` の終了コードへ伝搬する。
67216 - ``menu.MENU_BACK``: 一覧で Esc/← (操作なしでトップへ) / プロジェクト無し。
68217 - ``None``: 一覧・サブメニューで Ctrl-C による全体中止。
69218
70- 選択行が running 中なら ``_select_action`` で up/rebuild/down を選ばせ、それ以外
71- (stopped / unknown 等) は従来どおり直接 ``project up`` を起動する。サブメニューで
72- Esc/← を押すと (``MENU_BACK``) 一覧へ戻る。操作完了後はトップメニューへ復帰する
73- (plan 3.5 状態遷移: Exec → Top)。
219+ 選択行が running 中なら ``_operation_menu`` で全操作を選ばせ、それ以外
220+ (stopped / unknown) は従来どおり直接 ``project up`` を起動する (PR1 非回帰)。
221+ 操作完了後はトップメニューへ復帰する (plan 3.5 状態遷移: Exec → Top)。
74222 """
75223 projects_dir = Path (devbase_root ) / "projects"
76224 while True :
@@ -88,12 +236,11 @@ def run(devbase_root: Path):
88236 row = rows [idx ]
89237 name = row ["name" ]
90238 if str (row .get ("status" , "" )).startswith ("running" ):
91- action = _select_action ( name )
92- if action is menu .MENU_BACK :
239+ rc = _operation_menu ( devbase_root , name )
240+ if rc is menu .MENU_BACK :
93241 continue # 一覧へ戻る
94- if action is None :
242+ if rc is None :
95243 return None # Ctrl-C → 全体中止
96- rc = dispatch_lifecycle (action , name , scale = None )
97244 else :
98245 rc = dispatch_lifecycle ("up" , name , scale = None )
99246
0 commit comments