1- """Main Textual application for the mfbt TUI."""
1+ """Main Textual application for the mfbt TUI.
2+
3+ Uses a panel-swapping model: the chrome (K9sHeader, CommandBar,
4+ StatusBar) stays mounted at all times. Content panels (project list, module
5+ list, feature list) are swapped inside #main-content. Modal overlays (feature
6+ detail, info modal, help) are pushed as ModalScreen on top.
7+ """
28
39from __future__ import annotations
410
1117
1218from mfbt .tui .data_provider import TUIDataProvider
1319from mfbt .tui .navigation import NavigationItem , NavigationLevel , NavigationState
14- from mfbt .tui .widgets .breadcrumb_bar import BreadcrumbBar
1520from mfbt .tui .widgets .command_bar import CommandBar
1621from mfbt .tui .widgets .k9s_header import K9sHeader
1722from mfbt .tui .widgets .status_bar import StatusBar
@@ -59,6 +64,10 @@ class MFBTApp(App):
5964 BINDINGS = [
6065 Binding ("q" , "quit" , "Quit" , priority = True ),
6166 Binding ("question_mark" , "show_help" , "Help" , show = False ),
67+ Binding ("escape" , "go_back" , "Back" , show = False ),
68+ Binding ("backspace" , "go_back" , "Back" , show = False ),
69+ Binding ("r" , "refresh" , "Refresh" , show = False ),
70+ Binding ("d" , "describe" , "Describe" , show = False ),
6271 ]
6372
6473 def __init__ (
@@ -79,15 +88,12 @@ def __init__(
7988
8089 def compose (self ) -> ComposeResult :
8190 yield K9sHeader (id = "k9s-header" )
82- yield BreadcrumbBar (id = "breadcrumb-bar" )
8391 yield Container (id = "main-content" )
8492 yield CommandBar (id = "command-bar" )
8593 yield StatusBar (id = "status-bar" )
8694
8795 def on_mount (self ) -> None :
88- from mfbt .tui .screens .project_list import ProjectListScreen
89-
90- self .push_screen (ProjectListScreen (self .data_provider ))
96+ self ._show_project_list ()
9197 self ._update_nav_display ()
9298
9399 # Wire up transport change callback
@@ -104,6 +110,227 @@ def _on_transport(mode: TransportMode) -> None:
104110 # Start background token monitor
105111 self ._start_token_monitor ()
106112
113+ # ---- Content panel swapping ----------------------------------------
114+
115+ def _clear_main_content (self ) -> None :
116+ """Remove all children from #main-content."""
117+ container = self .query_one ("#main-content" , Container )
118+ container .remove_children ()
119+
120+ def _show_project_list (self ) -> None :
121+ """Mount the project list panel."""
122+ from mfbt .tui .screens .project_list import ProjectListPanel
123+
124+ self ._clear_main_content ()
125+ container = self .query_one ("#main-content" , Container )
126+ container .mount (ProjectListPanel (self .data_provider ))
127+
128+ def _show_module_list (
129+ self ,
130+ project_id : str ,
131+ project_name : str ,
132+ project_key : str ,
133+ project_metadata : dict [str , Any ],
134+ ) -> None :
135+ """Mount the module list panel for a project."""
136+ from mfbt .tui .screens .module_list import ModuleListPanel
137+
138+ self ._clear_main_content ()
139+ container = self .query_one ("#main-content" , Container )
140+ container .mount (
141+ ModuleListPanel (
142+ data_provider = self .data_provider ,
143+ project_id = project_id ,
144+ project_name = project_name ,
145+ project_key = project_key ,
146+ project_metadata = project_metadata ,
147+ )
148+ )
149+
150+ def _show_feature_list (
151+ self ,
152+ project_id : str ,
153+ project_name : str ,
154+ project_key : str ,
155+ project_metadata : dict [str , Any ],
156+ module_id : str ,
157+ module_name : str ,
158+ module_key : str ,
159+ module_metadata : dict [str , Any ],
160+ ) -> None :
161+ """Mount the feature list panel for a module."""
162+ from mfbt .tui .screens .feature_list import FeatureListPanel
163+
164+ self ._clear_main_content ()
165+ container = self .query_one ("#main-content" , Container )
166+ container .mount (
167+ FeatureListPanel (
168+ data_provider = self .data_provider ,
169+ project_id = project_id ,
170+ project_name = project_name ,
171+ module_id = module_id ,
172+ module_name = module_name ,
173+ module_key = module_key ,
174+ module_metadata = module_metadata ,
175+ )
176+ )
177+
178+ # ---- Message handlers from content panels --------------------------
179+
180+ def on_project_list_panel_project_selected (
181+ self , event : Any
182+ ) -> None :
183+ """Handle project selection: navigate to modules."""
184+ data = event .data
185+ project_id = data .get ("id" , "" )
186+ project_name = data .get ("name" , "Unknown" )
187+ project_key = data .get ("key" , data .get ("short_url_id" , "" ))
188+ tech_stack = data .get ("tech_stack" , "" )
189+ if isinstance (tech_stack , list ):
190+ tech_stack = ", " .join (tech_stack [:3 ])
191+
192+ project_metadata = {
193+ "tech_stack" : tech_stack ,
194+ "status" : data .get ("status" , "" ),
195+ "description" : data .get ("description" , "" ),
196+ }
197+
198+ # Push nav state
199+ self .nav_state .drill_down (
200+ NavigationItem (
201+ id = project_id ,
202+ name = project_name ,
203+ level = NavigationLevel .PROJECTS ,
204+ key = project_key ,
205+ metadata = project_metadata ,
206+ )
207+ )
208+
209+ self ._show_module_list (project_id , project_name , project_key , project_metadata )
210+ self ._update_nav_display ()
211+
212+ def on_module_list_panel_module_selected (
213+ self , event : Any
214+ ) -> None :
215+ """Handle module selection: navigate to features."""
216+ data = event .data
217+ module_id = data .get ("id" , "" )
218+ module_name = data .get ("title" , data .get ("name" , "Unknown" ))
219+ module_key = data .get ("module_key" , data .get ("key" , "" ))
220+ feature_count = data .get ("feature_count" , 0 )
221+
222+ module_metadata = {
223+ "feature_count" : feature_count or 0 ,
224+ }
225+
226+ # The project info is carried in the event
227+ project_id = event .project_id
228+ project_name = event .project_name
229+ project_key = event .project_key
230+ project_metadata = event .project_metadata
231+
232+ # Push nav state
233+ self .nav_state .drill_down (
234+ NavigationItem (
235+ id = module_id ,
236+ name = module_name ,
237+ level = NavigationLevel .MODULES ,
238+ key = module_key ,
239+ metadata = module_metadata ,
240+ )
241+ )
242+
243+ self ._show_feature_list (
244+ project_id = project_id ,
245+ project_name = project_name ,
246+ project_key = project_key ,
247+ project_metadata = project_metadata ,
248+ module_id = module_id ,
249+ module_name = module_name ,
250+ module_key = module_key ,
251+ module_metadata = module_metadata ,
252+ )
253+ self ._update_nav_display ()
254+
255+ def on_feature_list_panel_feature_selected (
256+ self , event : Any
257+ ) -> None :
258+ """Handle feature selection: open detail modal."""
259+ from mfbt .tui .screens .feature_detail import FeatureDetailScreen
260+
261+ feature_id = event .data .get ("id" , "" )
262+ self .push_screen (
263+ FeatureDetailScreen (
264+ data_provider = self .data_provider ,
265+ feature_id = feature_id ,
266+ feature_data = event .data ,
267+ )
268+ )
269+
270+ # ---- Describe handlers from content panels -------------------------
271+
272+ def _handle_describe (self , data : dict [str , Any ] | None , resource_type : str ) -> None :
273+ """Open info modal for the given resource."""
274+ if data :
275+ from mfbt .tui .screens .info_modal import InfoModal
276+
277+ self .push_screen (InfoModal (data = data , resource_type = resource_type ))
278+
279+ # ---- Navigation actions -------------------------------------------
280+
281+ def action_go_back (self ) -> None :
282+ """Navigate back one level."""
283+ level = self .nav_state .level
284+ if level == NavigationLevel .PROJECTS :
285+ return # Already at root, do nothing
286+ elif level == NavigationLevel .MODULES :
287+ self .nav_state .go_up ()
288+ self ._show_project_list ()
289+ elif level == NavigationLevel .FEATURES :
290+ self .nav_state .go_up ()
291+ # Restore module list
292+ if self .nav_state .stack :
293+ proj = self .nav_state .stack [0 ]
294+ self ._show_module_list (
295+ project_id = proj .id ,
296+ project_name = proj .name ,
297+ project_key = proj .key ,
298+ project_metadata = proj .metadata ,
299+ )
300+ self ._update_nav_display ()
301+
302+ def action_refresh (self ) -> None :
303+ """Refresh the current content panel."""
304+ from mfbt .tui .screens .feature_list import FeatureListPanel
305+ from mfbt .tui .screens .module_list import ModuleListPanel
306+ from mfbt .tui .screens .project_list import ProjectListPanel
307+
308+ container = self .query_one ("#main-content" , Container )
309+ for child in container .children :
310+ if isinstance (child , (ProjectListPanel , ModuleListPanel , FeatureListPanel )):
311+ child .refresh_data ()
312+ break
313+
314+ def action_describe (self ) -> None :
315+ """Show info modal for the selected item."""
316+ from mfbt .tui .screens .feature_list import FeatureListPanel
317+ from mfbt .tui .screens .module_list import ModuleListPanel
318+ from mfbt .tui .screens .project_list import ProjectListPanel
319+
320+ container = self .query_one ("#main-content" , Container )
321+ for child in container .children :
322+ if isinstance (child , ProjectListPanel ):
323+ self ._handle_describe (child .get_selected_data (), "project" )
324+ return
325+ if isinstance (child , ModuleListPanel ):
326+ self ._handle_describe (child .get_selected_data (), "module" )
327+ return
328+ if isinstance (child , FeatureListPanel ):
329+ self ._handle_describe (child .get_selected_data (), "feature" )
330+ return
331+
332+ # ---- Token monitor -------------------------------------------------
333+
107334 def _start_token_monitor (self ) -> None :
108335 """Start the background token refresh monitor."""
109336 from mfbt .token_manager import BackgroundTokenMonitor
@@ -150,8 +377,7 @@ def _update_transport(self, mode: TransportMode) -> None:
150377 except Exception :
151378 pass
152379
153- def on_screen_resume (self ) -> None :
154- self ._update_nav_display ()
380+ # ---- Display updates -----------------------------------------------
155381
156382 def on_resize (self ) -> None :
157383 if self .size .width < 80 :
@@ -171,13 +397,6 @@ def action_show_help(self) -> None:
171397
172398 def _update_nav_display (self ) -> None :
173399 """Update breadcrumb, header, command bar, and status bar from nav state."""
174- # Breadcrumb bar
175- try :
176- breadcrumb = self .query_one ("#breadcrumb-bar" , BreadcrumbBar )
177- breadcrumb .path_rich = self .nav_state .breadcrumb_path_rich
178- except Exception :
179- pass
180-
181400 # Command bar hints
182401 try :
183402 command_bar = self .query_one ("#command-bar" , CommandBar )
@@ -191,6 +410,7 @@ def _update_nav_display(self) -> None:
191410 # K9s header context
192411 try :
193412 header = self .query_one ("#k9s-header" , K9sHeader )
413+ header .breadcrumb = self .nav_state .breadcrumb_path_rich
194414 level = self .nav_state .level
195415 header .context_level = level .value
196416
@@ -227,43 +447,3 @@ def _update_nav_display(self) -> None:
227447 status_bar .resource_type = "Features"
228448 except Exception :
229449 pass
230-
231- def push_screen ( # type: ignore[override]
232- self , screen : Any , * args : Any , ** kwargs : Any
233- ) -> Any :
234- """Override to track navigation state on screen push."""
235- # Track navigation for non-modal screens
236- from mfbt .tui .screens .feature_list import FeatureListScreen
237- from mfbt .tui .screens .module_list import ModuleListScreen
238-
239- if isinstance (screen , ModuleListScreen ):
240- self .nav_state .drill_down (
241- NavigationItem (
242- id = screen .project_id ,
243- name = screen .project_name ,
244- level = NavigationLevel .PROJECTS ,
245- key = getattr (screen , "project_key" , "" ),
246- metadata = getattr (screen , "project_metadata" , {}),
247- )
248- )
249- elif isinstance (screen , FeatureListScreen ):
250- self .nav_state .drill_down (
251- NavigationItem (
252- id = screen .module_id ,
253- name = screen .module_name ,
254- level = NavigationLevel .MODULES ,
255- key = getattr (screen , "module_key" , "" ),
256- metadata = getattr (screen , "module_metadata" , {}),
257- )
258- )
259-
260- result = super ().push_screen (screen , * args , ** kwargs )
261- self ._update_nav_display ()
262- return result
263-
264- def pop_screen (self ) -> Any :
265- """Override to track navigation state on screen pop."""
266- self .nav_state .go_up ()
267- result = super ().pop_screen ()
268- self ._update_nav_display ()
269- return result
0 commit comments