Skip to content

Commit baf760d

Browse files
mballanceCopilot
andcommitted
sync: unify live display with final output; fix pypi labeling
- RichSyncTUI: single live table IS the final output — same columns (icon, Package, Branch, Status+commits, Δ, Time) used during both the spinner phase and the final display; render() does one last refresh then stops live and appends only attention+summary panels - Add _row_status() helper shared by _build_table() and removing duplicated status/delta logic - RichSyncTUI: pypi packages filtered from the live table (not just the final static table), so they never appear as 'not a VCS package' - package.py: base Package.sync() skipped_reason now uses src_type (e.g. 'pypi', 'url', 'dir') instead of 'not a VCS package' - TranscriptSyncTUI: render() already only shows attention items + summary (no separate full-table reprint) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent e19833c commit baf760d

File tree

2 files changed

+71
-93
lines changed

2 files changed

+71
-93
lines changed

src/ivpm/package.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -114,12 +114,13 @@ def status(self, pkgs_info):
114114

115115
def sync(self, sync_info):
116116
from .pkg_sync import PkgSyncResult, SyncOutcome
117+
src = str(getattr(self, "src_type", "") or "non-vcs")
117118
return PkgSyncResult(
118119
name=self.name,
119-
src_type=str(getattr(self, "src_type", "") or ""),
120+
src_type=src,
120121
path=os.path.join(sync_info.deps_dir, self.name),
121122
outcome=SyncOutcome.SKIPPED,
122-
skipped_reason="not a VCS package",
123+
skipped_reason=src,
123124
)
124125

125126
def update(self, update_info : ProjectUpdateInfo) -> 'ProjInfo':

src/ivpm/sync_tui.py

Lines changed: 68 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -68,12 +68,43 @@ def _dur(state: Optional[dict]) -> str:
6868
return ""
6969

7070

71+
def _row_status(r: PkgSyncResult):
72+
"""Return (delta_Text, status_Text) for a completed result row."""
73+
from rich.text import Text
74+
if r.outcome == SyncOutcome.SYNCED:
75+
status = Text("%s→%s" % (r.old_commit or "?", r.new_commit or "?"), style="green")
76+
delta = Text("↓%d" % r.commits_behind, style="green") if r.commits_behind else Text("")
77+
elif r.outcome == SyncOutcome.UP_TO_DATE:
78+
status = Text("up-to-date %s" % (r.old_commit or ""), style="dim")
79+
delta = Text("=", style="dim")
80+
elif r.outcome == SyncOutcome.CONFLICT:
81+
status = Text("conflict %s" % (r.old_commit or ""), style="bold red")
82+
delta = Text("↓%d" % r.commits_behind, style="red") if r.commits_behind else Text("")
83+
elif r.outcome == SyncOutcome.DIRTY:
84+
status = Text("dirty %s" % (r.old_commit or ""), style="bold yellow")
85+
delta = Text("")
86+
elif r.outcome == SyncOutcome.AHEAD:
87+
ahead_str = "↑%d" % r.commits_ahead if r.commits_ahead else "ahead"
88+
status = Text("ahead %s" % (r.old_commit or ""), style="bold yellow")
89+
delta = Text(ahead_str, style="bold yellow")
90+
elif r.outcome == SyncOutcome.ERROR:
91+
status = Text(r.error or "error", style="bold red")
92+
delta = Text("")
93+
elif r.outcome in _DRY_OUTCOMES:
94+
status = Text("%s %s" % (r.outcome.value, r.old_commit or ""), style="cyan")
95+
delta = Text("↓%d" % r.commits_behind, style="cyan") if r.commits_behind else Text("")
96+
else: # SKIPPED
97+
status = Text(r.skipped_reason or "skipped", style="dim")
98+
delta = Text("")
99+
return delta, status
100+
101+
71102
# ---------------------------------------------------------------------------
72103
# Rich TUI
73104
# ---------------------------------------------------------------------------
74105

75106
class RichSyncTUI(SyncProgressListener):
76-
"""Rich-based TUI: live spinner display during sync, final table after."""
107+
"""Rich-based TUI: single live table that becomes the final output."""
77108

78109
def __init__(self):
79110
from rich.console import Console
@@ -100,135 +131,81 @@ def on_pkg_result(self, result: PkgSyncResult) -> None:
100131
# ── Lifecycle ─────────────────────────────────────────────────────────
101132

102133
def start(self):
103-
"""Begin the live display (call before triggering sync)."""
104134
from rich.live import Live
105135
from rich.table import Table
106-
tbl = Table(show_header=False, box=None, padding=(0, 1))
136+
tbl = Table(show_header=True, header_style="bold", box=None, padding=(0, 1))
107137
self._live = Live(tbl, console=self.console, refresh_per_second=10)
108138
self._live.start()
109139

110140
def stop(self):
111-
"""End the live display."""
112141
if self._live:
113142
self._live.stop()
114143
self._live = None
115144

116-
def _refresh(self):
117-
if not self._live:
118-
return
145+
def _build_table(self, spinner=True):
146+
"""Build the unified package table (used for both live and final display)."""
119147
from rich.spinner import Spinner
120148
from rich.table import Table
121149
from rich.text import Text
122150

123-
tbl = Table(show_header=False, box=None, padding=(0, 1))
124-
tbl.add_column("s", width=3)
125-
tbl.add_column("name", style="bold")
126-
tbl.add_column("info")
151+
tbl = Table(show_header=True, header_style="bold", box=None, padding=(0, 1))
152+
tbl.add_column("", width=3, no_wrap=True)
153+
tbl.add_column("Package", style="bold", no_wrap=True)
154+
tbl.add_column("Branch", no_wrap=True)
155+
tbl.add_column("Status", no_wrap=True)
156+
tbl.add_column("Δ", no_wrap=True)
157+
tbl.add_column("Time", no_wrap=True)
127158

128159
for name in self._order:
129160
state = self._pkg_states[name]
130161
if not state["done"]:
131-
marker = Spinner("dots", style="bold cyan")
132-
info = Text(name, style="dim")
162+
if spinner:
163+
marker = Spinner("dots", style="bold cyan")
164+
else:
165+
marker = Text("…", style="dim")
166+
tbl.add_row(marker, Text(name), Text(""), Text("", style="dim"),
167+
Text(""), Text(""))
133168
else:
134169
r = state["result"]
170+
# Skip pypi in the live/final table
171+
if r.src_type == "pypi":
172+
continue
135173
icon, style = _ICONS.get(r.outcome, ("?", "dim"))
136-
marker = Text(icon, style=style)
137-
dur = "%.1fs" % state.get("duration", 0)
138-
label = r.outcome.value
139-
if r.outcome == SyncOutcome.SYNCED and r.old_commit and r.new_commit:
140-
label = "%s→%s" % (r.old_commit, r.new_commit)
141-
elif r.outcome == SyncOutcome.SKIPPED:
142-
label = r.skipped_reason or "skipped"
143-
elif r.outcome == SyncOutcome.ERROR:
144-
label = r.error or "error"
145-
info = Text("%s %s" % (label, dur))
174+
marker = Text(icon, style=style)
175+
branch = Text(r.branch or "—", style="" if r.branch else "dim")
176+
dur = Text(_dur(state), style="dim")
177+
delta, status = _row_status(r)
178+
tbl.add_row(marker, Text(name), branch, status, delta, dur)
146179

147-
tbl.add_row(marker, Text(name), info)
180+
return tbl
148181

149-
self._live.update(tbl)
182+
def _refresh(self):
183+
if self._live:
184+
self._live.update(self._build_table(spinner=True))
150185

151186
# ── Final render ──────────────────────────────────────────────────────
152187

153188
def render(self, results: List[PkgSyncResult], dry_run: bool = False):
154-
from rich.console import Console
155-
from rich.table import Table
156189
from rich.text import Text
157190
from rich.panel import Panel
158191

159-
console = Console()
160-
161-
# ── Main table — one row per package with inline status + duration ─
162-
table = Table(show_header=True, header_style="bold", box=None,
163-
padding=(0, 1))
164-
table.add_column("", width=3, no_wrap=True)
165-
table.add_column("Package", style="bold", no_wrap=True)
166-
table.add_column("Branch", no_wrap=True)
167-
table.add_column("Status", no_wrap=True)
168-
table.add_column("Δ", no_wrap=True)
169-
table.add_column("Time", no_wrap=True)
192+
# Final refresh with complete data, then stop the live display.
193+
if self._live:
194+
self._live.update(self._build_table(spinner=False))
195+
self._live.stop()
196+
self._live = None
170197

171198
counts = {o: 0 for o in SyncOutcome}
172199
pypi_count = 0
173200
attention_items = []
174-
175201
for r in results:
176202
counts[r.outcome] += 1
177-
178-
# Hide pypi packages (sync is a no-op for them); tally for summary.
179203
if r.src_type == "pypi":
180204
pypi_count += 1
181205
continue
182-
183-
icon, style = _ICONS.get(r.outcome, ("?", "dim"))
184-
marker = Text(icon, style=style)
185-
branch_text = Text(r.branch or "—", style="" if r.branch else "dim")
186-
dur_text = Text(_dur(self._pkg_states.get(r.name)), style="dim")
187-
188-
if r.outcome == SyncOutcome.SYNCED:
189-
status = Text("%s→%s" % (r.old_commit or "?", r.new_commit or "?"),
190-
style="green")
191-
delta = Text("↓%d" % r.commits_behind, style="green") if r.commits_behind else Text("")
192-
193-
elif r.outcome == SyncOutcome.UP_TO_DATE:
194-
status = Text("up-to-date %s" % (r.old_commit or ""), style="dim")
195-
delta = Text("=", style="dim")
196-
197-
elif r.outcome == SyncOutcome.CONFLICT:
198-
status = Text("conflict %s" % (r.old_commit or ""), style="bold red")
199-
delta = Text("↓%d" % r.commits_behind, style="red") if r.commits_behind else Text("")
200-
attention_items.append(r)
201-
202-
elif r.outcome == SyncOutcome.DIRTY:
203-
status = Text("dirty %s" % (r.old_commit or ""), style="bold yellow")
204-
delta = Text("")
205-
attention_items.append(r)
206-
207-
elif r.outcome == SyncOutcome.AHEAD:
208-
ahead_str = "↑%d" % r.commits_ahead if r.commits_ahead else "ahead"
209-
status = Text("ahead %s" % (r.old_commit or ""), style="bold yellow")
210-
delta = Text(ahead_str, style="bold yellow")
206+
if r.outcome in _ATTENTION_OUTCOMES:
211207
attention_items.append(r)
212208

213-
elif r.outcome == SyncOutcome.ERROR:
214-
status = Text(r.error or "error", style="bold red")
215-
delta = Text("")
216-
attention_items.append(r)
217-
218-
elif r.outcome in _DRY_OUTCOMES:
219-
status = Text("%s %s" % (r.outcome.value, r.old_commit or ""), style="cyan")
220-
delta = Text("↓%d" % r.commits_behind, style="cyan") if r.commits_behind else Text("")
221-
if r.outcome in (SyncOutcome.DRY_DIRTY, SyncOutcome.DRY_WOULD_CONFLICT):
222-
attention_items.append(r)
223-
224-
else: # SKIPPED
225-
status = Text(r.skipped_reason or "skipped", style="dim")
226-
delta = Text("")
227-
228-
table.add_row(marker, Text(r.name), branch_text, status, delta, dur_text)
229-
230-
console.print(table)
231-
232209
# ── Attention panel ───────────────────────────────────────────────
233210
if attention_items:
234211
lines = []
@@ -269,7 +246,7 @@ def render(self, results: List[PkgSyncResult], dry_run: bool = False):
269246
has_error = any(r.outcome in (SyncOutcome.CONFLICT, SyncOutcome.ERROR)
270247
for r in attention_items)
271248
border = "red" if has_error else "yellow"
272-
console.print(Panel(
249+
self.console.print(Panel(
273250
"\n".join(lines).rstrip(),
274251
title="Attention", border_style=border,
275252
))
@@ -316,7 +293,7 @@ def render(self, results: List[PkgSyncResult], dry_run: bool = False):
316293
else:
317294
border = "green"
318295

319-
console.print(Panel(summary, title="Sync", border_style=border))
296+
self.console.print(Panel(summary, title="Sync", border_style=border))
320297

321298

322299
# ---------------------------------------------------------------------------

0 commit comments

Comments
 (0)