11from textual import on
22from textual .app import App , ComposeResult
3- from textual .containers import Horizontal , Vertical
4- from textual .widgets import Label , ListItem , ListView
3+ from textual .binding import Binding
4+ from textual .containers import Vertical , VerticalScroll
5+ from textual .widgets import ContentSwitcher , Label , ListItem , ListView , Static
56
67import plain_spec
78from partial_rendering import PartialRender , PartialRenderChoice
89from plain_modules import PlainModule
10+ from tui .components import CustomFooter , TUIComponents
911
1012
1113class PartialRenderTUI (App ):
12- def __init__ (self , plain_module : PlainModule , partial_render : PartialRender , ** kwargs ):
14+ BINDINGS = [
15+ Binding ("ctrl+o" , "toggle_expand" , "Expand/Collapse" , show = False ),
16+ Binding ("ctrl+c" , "quit" , "Quit" , show = False ),
17+ Binding ("ctrl+d" , "quit" , "Quit" , show = False ),
18+ ]
19+
20+ def __init__ (
21+ self ,
22+ plain_module : PlainModule ,
23+ partial_render : PartialRender ,
24+ state_machine_version : str ,
25+ render_id : str ,
26+ ** kwargs ,
27+ ):
1328 super ().__init__ (** kwargs )
1429 self .plain_module = plain_module
1530 self .partial_render = partial_render
1631 self .choices = {} # populated in on_mount
32+ self .state_machine_version = state_machine_version
33+ self .render_id = render_id
34+ self ._expandable_labels : list [dict ] = []
1735
1836 def compose (self ) -> ComposeResult :
19- yield Vertical (
20- Label ("[bold]--- Partial Render Detected ---[/bold]" , classes = "highlight" ),
21- Horizontal (
22- Vertical (id = "info-panel-left" ),
23- Vertical (id = "info-panel-right" ),
24- id = "info-panel-columns" ,
25- ),
26- id = "info-panel" ,
27- )
28- yield ListView (id = "choice-list" )
37+ with ContentSwitcher (id = "content-switcher" , initial = TUIComponents .DASHBOARD_VIEW .value ):
38+ with Vertical (id = TUIComponents .DASHBOARD_VIEW .value ):
39+ with VerticalScroll ():
40+ yield Static (
41+ f"[#FFFFFF]*codeplain[/#FFFFFF] [#888888](v{ self .state_machine_version } )[/#888888]" ,
42+ id = "codeplain-header" ,
43+ classes = "codeplain-header" ,
44+ )
45+ yield Vertical (id = "info-panel" )
46+ yield ListView (id = "choice-list" )
47+ yield CustomFooter (render_id = self .render_id )
2948
3049 def on_mount (self ) -> None :
3150 pr = self .partial_render
3251 pm = self .plain_module
3352
34- # Info labels — left side
35- left = self .query_one ("#info-panel-left" , Vertical )
36- left .mount (Label ("[#e0ff6e]Current state:[/]" ))
37- left .mount (Label (f"Target module: [{ '#79fc96' } ]{ pm .module_name } [/]" ))
53+ info_panel = self .query_one ("#info-panel" , Vertical )
54+ info_panel .mount (Label ("module status" , classes = "rendering-info-title" ))
3855
39- if pr .change_type == "spec_change" :
40- left .mount (Label (f"Detected spec change of module [{ '#79fc96' } ]{ pr .last_render_module .module_name } [/]" ))
41- elif pr .change_type == "code_change" :
42- left .mount (Label (f"Detected code change of module [{ '#79fc96' } ]{ pr .last_render_module .module_name } [/]" ))
56+ info_box = Vertical (classes = "rendering-info-box" )
57+ info_panel .mount (info_box )
4358
59+ info_box .mount (Label (f"module: { pr .last_render_module .module_name } " , classes = "rendering-info-row" ))
4460 if pr .last_render_frid is None or pr .last_render_module .is_module_fully_rendered ():
45- left .mount (Label (f"Module [ { '#79fc96' } ] { pr . last_render_module . module_name } [/] was fully rendered. " ))
61+ info_box .mount (Label ("module fully rendered" , classes = "rendering-info-row " ))
4662 else :
47- left .mount (
48- Label (
49- f"Module [{ '#79fc96' } ]{ pr .last_render_module .module_name } [/] is partially rendered. Last fully rendered functionality was [{ '#79fc96' } ]{ pr .last_render_frid } [/]"
50- )
51- )
63+ frid = pr .last_render_frid
64+ functionality = pr .last_render_module .plain_source [plain_spec .FUNCTIONAL_REQUIREMENTS ][int (frid ) - 1 ][
65+ "markdown"
66+ ]
67+ label = Label ("" , classes = "rendering-info-row" )
68+ info_box .mount (label )
69+ self ._register_expandable (label , f"functionality { frid } :" , functionality )
5270
5371 # Build choices (same logic as original)
5472 choice_idx = 1
5573 if pr .last_render_frid is not None :
5674 next_frid , next_module = pm .get_next_frid (pr .last_render_frid , pr .last_render_module .module_name )
5775
58- functionality = next_module .plain_source [plain_spec .FUNCTIONAL_REQUIREMENTS ][int (next_frid ) - 1 ]
59- # Placeholder — right side
60-
61- right = self .query_one ("#info-panel-right" , Vertical )
62- right .mount (Label ("[#e0ff6e]Next functionality:[/]" ))
63- right .mount (
64- Label (f"Module [{ '#79fc96' } ]{ next_module .module_name } [/], functionality [{ '#79fc96' } ]{ next_frid } [/]" )
65- )
66- right .mount (Label (functionality ["markdown" ]))
67-
6876 msg = f"Continue from next functionality (module { next_module .module_name } "
6977 if next_frid != plain_spec .get_first_frid (next_module .plain_source ):
7078 msg += f" functionality { next_frid } )"
@@ -77,14 +85,44 @@ def on_mount(self) -> None:
7785 )
7886 choice_idx += 1
7987
88+ change_box = Vertical (classes = "change-box" )
89+ info_panel .mount (change_box )
8090 if pr .change :
8191 reason = "spec change" if pr .change_type == "spec_change" else "code change"
92+
93+ title_start = "Spec changes" if pr .change_type == "spec_change" else "Code changes"
94+ change_box .mount (
95+ Label (
96+ f"--- { title_start } detected in required module [#5593FF]{ pr .change .module_name } [/] ---" ,
97+ classes = "rendering-info-row" ,
98+ )
99+ )
100+ change_box .mount (
101+ Label (f"{ title_start } in a required module may affect the current module" , classes = "rendering-info-row" )
102+ )
103+
82104 self .choices [str (choice_idx )] = PartialRenderChoice (
83105 module = pr .change ,
84106 render_range = None ,
85107 msg = f"Re-render { pr .change .module_name } from start due to { reason } " ,
86108 )
87109 choice_idx += 1
110+ elif pr .last_render_frid is None or pr .last_render_module .is_module_fully_rendered ():
111+ change_box .mount (Label ("--- Rendering interrupted ---" , classes = "rendering-info-row" ))
112+ change_box .mount (Label ("The current module was fully rendered." , classes = "rendering-info-row" ))
113+ else :
114+ change_box .mount (
115+ Label (
116+ f"--- Rendering interrupted during [#5593FF]functionality { pr .last_render_frid } [/] ---" ,
117+ classes = "rendering-info-row" ,
118+ )
119+ )
120+ change_box .mount (
121+ Label (
122+ "Resume from the last successfully rendered functionality or start over." ,
123+ classes = "rendering-info-row" ,
124+ )
125+ )
88126
89127 first_module = pm .all_required_modules [0 ]
90128 if first_module .module_name != pr .last_render_module .module_name and (
@@ -101,13 +139,25 @@ def on_mount(self) -> None:
101139
102140 # Populate the ListView
103141 lv = self .query_one ("#choice-list" , ListView )
104- self .mount (Label ("How would you like to start rendering ?" , classes = "partial-render-question" ), before = lv )
142+ self .mount (Label ("How would you like to continue ?" , classes = "partial-render-question" ), before = lv )
105143 for key , choice in self .choices .items ():
106144 lv .append (ListItem (Label (f"[bold]{ key } .[/bold] { choice .msg } " ), id = f"choice-{ key } " ))
145+ lv .focus ()
146+
147+ def _register_expandable (self , label : Label , prefix : str , full_text : str ) -> None :
148+ first_line = full_text [:20 ]
149+ short = f"{ prefix } { first_line } [#888](ctrl+o to expand)[/]"
150+ full = f"{ prefix } { full_text } [#888](ctrl+o to collapse)[/]"
151+ label .update (short )
152+ self ._expandable_labels .append ({"label" : label , "short" : short , "full" : full , "expanded" : False })
153+
154+ def action_toggle_expand (self ) -> None :
155+ for entry in self ._expandable_labels :
156+ entry ["expanded" ] = not entry ["expanded" ]
157+ entry ["label" ].update (entry ["full" ] if entry ["expanded" ] else entry ["short" ])
107158
108159 @on (ListView .Selected )
109160 def on_choice_selected (self , event : ListView .Selected ) -> None :
110- # Extract the key from the widget id ("choice-1" → "1")
111161 key = event .item .id .split ("-" , 1 )[1 ]
112162 self .selected_choice = self .choices [key ]
113163 self .exit (self .selected_choice )
0 commit comments