3434# ============================================================================
3535
3636# Fixed width for description column (prefix + label + detail text)
37- DESC_WIDTH = 24
37+ DESC_WIDTH = 30
3838
3939# Progress thresholds - fraction of progress bar reserved for checking phase
4040CHECKING_PROGRESS_END = 0.1
@@ -102,23 +102,13 @@ def render(self, task: RichTask) -> Text:
102102
103103
104104class PhaseAwareProgressColumn (TaskProgressColumn ):
105- """Task progress column that hides percentage during checking phase."""
105+ """Task progress column that shows dim placeholder during indeterminate phase."""
106106
107107 def render (self , task : RichTask ) -> Text :
108- phase = task .fields .get ("phase" , "checking" )
109- if phase == "checking" :
110- return Text ("" , style = "dim" )
111- return super ().render (task )
112-
113-
114- class PhaseAwareBarColumn (BarColumn ):
115- """Bar column that shows pulse animation during checking phase."""
116-
117- def render (self , task : RichTask ) -> Text :
118- phase = task .fields .get ("phase" , "checking" )
119- if phase == "checking" :
120- # Return empty/dim bar during checking
121- return Text (" " * (self .bar_width or 16 ), style = "dim" )
108+ # When total is None (indeterminate), show placeholder to maintain width
109+ if task .total is None :
110+ # Match width of "100%" (4 chars)
111+ return Text (" - " , style = "dim" )
122112 return super ().render (task )
123113
124114
@@ -133,13 +123,22 @@ def render(self, task: RichTask) -> Text:
133123
134124
135125class ETAColumn (ProgressColumn ):
136- """Shows ETA when available."""
126+ """Shows ETA when available with fixed width."""
127+
128+ # Fixed width for ETA column (e.g., "ETA 10m30s" = 10 chars)
129+ ETA_WIDTH = 10
137130
138131 def render (self , task : RichTask ) -> Text :
139132 eta = task .fields .get ("eta" , "" )
140133 if eta :
141- return Text (f"ETA { eta } " , style = "dim" )
142- return Text ("" , style = "dim" )
134+ text = f"ETA { eta } "
135+ # Pad or truncate to fixed width
136+ if len (text ) < self .ETA_WIDTH :
137+ text = text + " " * (self .ETA_WIDTH - len (text ))
138+ elif len (text ) > self .ETA_WIDTH :
139+ text = text [:self .ETA_WIDTH ]
140+ return Text (text , style = "dim" )
141+ return Text (" " * self .ETA_WIDTH , style = "dim" )
143142
144143
145144class SysUpdateCLI :
@@ -230,16 +229,9 @@ def _format_desc(self, prefix: str, label: str, detail: str = "") -> str:
230229 # Pad to fixed width
231230 return text + " " * (DESC_WIDTH - visible_len )
232231 elif visible_len > DESC_WIDTH :
233- # Truncate: find how much to trim from the end
234- excess = visible_len - DESC_WIDTH
235- # Remove markup, truncate, but keep the structure
236- if detail :
237- # Truncate detail portion
238- detail_visible = _MARKUP_PATTERN .sub ("" , detail )
239- if len (detail_visible ) > excess :
240- new_detail = detail_visible [:- excess - 1 ] + "…"
241- return f"{ prefix } { label } [dim]{ new_detail } [/]"
242- return text [:DESC_WIDTH ]
232+ # Truncate visible text and rebuild with markup
233+ truncated_visible = visible [:DESC_WIDTH - 1 ] + "…"
234+ return truncated_visible
243235 return text
244236
245237 def _create_progress_callback (
@@ -262,16 +254,16 @@ def on_progress(update: UpdateProgress) -> None:
262254 phase_value = update .phase .value if update .phase else "checking"
263255
264256 if update .phase == UpdatePhase .CHECKING :
265- # During checking, show only spinner and message (no progress bar/percentage )
257+ # During checking, show pulse animation (don't set total/completed )
266258 if update .message :
267- # Extract short status from message (increased limit to 25 chars)
259+ # Extract short status from message (limit to 25 chars)
268260 msg = update .message .rstrip ("." )
269261 if len (msg ) > 25 :
270262 msg = msg [:24 ] + "…"
271263 desc = self ._format_desc ("" , f"{ label } [dim]|[/] { msg } " )
272264 else :
273265 desc = self ._format_desc ("" , f"{ label } [dim]|[/] checking" )
274- # Don't update completed during checking - keeps bar indeterminate
266+ # Keep total=None for pulse animation, only update description
275267 progress .update (
276268 task_id ,
277269 description = desc ,
@@ -284,8 +276,10 @@ def on_progress(update: UpdateProgress) -> None:
284276 desc = self ._format_desc ("" , f"{ label } [dim]|[/] { pkg } " )
285277 else :
286278 desc = self ._format_desc ("" , f"{ label } [dim]|[/] { phase_text } " )
279+ # Transition to determinate progress: set total and completed
287280 progress .update (
288281 task_id ,
282+ total = 100 ,
289283 completed = pct ,
290284 description = desc ,
291285 phase = phase_value ,
@@ -296,6 +290,7 @@ def on_progress(update: UpdateProgress) -> None:
296290 desc = self ._format_desc ("" , label )
297291 progress .update (
298292 task_id ,
293+ total = 100 ,
299294 completed = pct ,
300295 description = desc ,
301296 phase = phase_value ,
@@ -329,7 +324,7 @@ async def _run_updates(self) -> int:
329324 TextColumn (" " ),
330325 StatusColumn (spinner_name = "dots" , style = "white" , use_ascii = self ._use_ascii ),
331326 TextColumn ("{task.description}" ),
332- PhaseAwareBarColumn (bar_width = BAR_WIDTH , style = "dim" , complete_style = "white" , finished_style = "green" ),
327+ BarColumn (bar_width = BAR_WIDTH , style = "dim" , complete_style = "white" , finished_style = "green" , pulse_style = "cyan " ),
333328 PhaseAwareProgressColumn (),
334329 TimeElapsedColumn (),
335330 SpeedColumn (),
@@ -343,7 +338,12 @@ async def _run_updates(self) -> int:
343338
344339 for cfg , is_available in available_updaters :
345340 if is_available :
346- task_id = progress .add_task (self ._format_desc ("" , cfg .label ), total = 100 )
341+ # Start with total=None for indeterminate pulse animation
342+ task_id = progress .add_task (
343+ self ._format_desc ("" , cfg .label ),
344+ total = None ,
345+ phase = "checking" ,
346+ )
347347 coroutines .append (self ._run_updater (progress , task_id , cfg ))
348348 labels .append (cfg .label )
349349 else :
@@ -385,8 +385,10 @@ async def _run_updater(
385385 dry_run = self .dry_run ,
386386 )
387387
388+ # Ensure we transition to determinate mode and mark complete
388389 progress .update (
389390 task_id ,
391+ total = 100 ,
390392 completed = 100 ,
391393 success = result .success ,
392394 description = self ._format_desc ("" , cfg .label )
0 commit comments