Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions synodic_client/application/screen/action_card.py
Original file line number Diff line number Diff line change
Expand Up @@ -632,6 +632,7 @@ def __init__(self, parent: QWidget | None = None) -> None:

self._cards: list[ActionCard] = []
self._action_map: dict[SetupAction, ActionCard] = {}
self._index_map: dict[int, ActionCard] = {}

# ------------------------------------------------------------------
# Skeleton loading
Expand Down Expand Up @@ -683,6 +684,14 @@ def populate(
self._cards.append(card)
self._action_map[act] = card

# Build original-index → card mapping so callers can look up by
# the action index porringer emits, which is independent of the
# display sort order.
for original_index, act in enumerate(actions):
card = self._action_map.get(act)
if card is not None:
self._index_map[original_index] = card

# ------------------------------------------------------------------
# Card lookup
# ------------------------------------------------------------------
Expand All @@ -697,6 +706,15 @@ def card_count(self) -> int:
"""Return the number of cards (including skeletons)."""
return len(self._cards)

def card_for_action_index(self, action_index: int) -> ActionCard | None:
"""Return the card for the given original action index.

The index corresponds to the action's position in the unsorted
list passed to :meth:`populate`, matching the indices emitted
by porringer's ``ActionCompletedEvent``.
"""
return self._index_map.get(action_index)

def get_card(self, action: SetupAction) -> ActionCard | None:
"""Look up the card for a given action.

Expand Down Expand Up @@ -727,3 +745,4 @@ def clear(self) -> None:
card.deleteLater()
self._cards.clear()
self._action_map.clear()
self._index_map.clear()
21 changes: 13 additions & 8 deletions synodic_client/application/screen/install.py
Original file line number Diff line number Diff line change
Expand Up @@ -638,7 +638,7 @@ def _on_action_checked(self, row: int, result: SetupActionResult, status: str) -
# Update the card widget
if m.preview and 0 <= row < len(m.preview.actions):
action = m.preview.actions[row]
card = self._card_list.get_card(action)
card = self._card_list.card_for_action_index(row)
if card is not None:
card.set_check_result(result, status)

Expand Down Expand Up @@ -679,15 +679,20 @@ def _on_preview_finished(self) -> None:
finalized,
)

# Compute summary
# Compute summary using the shared operations-layer classifier
from collections import Counter

from synodic_client.operations.schema import classify_status

total = len(m.action_states)
needed = sum(1 for s in m.action_states if s.status == 'Needed')
counts = Counter(classify_status(s.status) for s in m.action_states)
needed = counts.get('needed', 0)
satisfied = counts.get('satisfied', 0)
pending = counts.get('pending', 0)
ready = counts.get('ready', 0)
unavailable = counts.get('unavailable', 0)
failed = counts.get('failed', 0)
upgradable = len(m.upgradable_keys)
unavailable = sum(1 for s in m.action_states if s.status == 'Not installed')
failed = sum(1 for s in m.action_states if s.status == 'Failed')
pending = sum(1 for s in m.action_states if s.status == 'Pending')
ready = sum(1 for s in m.action_states if s.status == 'Ready')
satisfied = total - needed - upgradable - unavailable - failed - pending - ready

parts: list[str] = []
_counts: list[tuple[int, str]] = [
Expand Down
27 changes: 19 additions & 8 deletions synodic_client/operations/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,19 +50,30 @@ def resolve_action_status(result: SetupActionResult, action: SetupAction) -> str
return 'Needed'


_STATUS_BUCKETS: dict[str, str] = {
'Needed': 'needed',
'Pending': 'pending',
'Ready': 'ready',
'Not installed': 'unavailable',
'Failed': 'failed',
'Already installed': 'satisfied',
'Already latest': 'satisfied',
}


def classify_status(status: str) -> str:
"""Classify a resolved status string into a summary bucket.

Returns one of ``'needed'``, ``'satisfied'``, ``'pending'``, or
``'unknown'``. Upgradability is determined separately from skip
reason, so it is not included here.
Returns one of ``'needed'``, ``'satisfied'``, ``'pending'``,
``'ready'``, ``'unavailable'``, ``'failed'``, or ``'unknown'``.
Upgradability is determined separately from skip reason, so it
is not included here.
"""
if status == 'Needed':
return 'needed'
if '\u2713' in status or status in {'Already installed', 'Already latest'}:
bucket = _STATUS_BUCKETS.get(status)
if bucket is not None:
return bucket
if '\u2713' in status:
return 'satisfied'
if status == 'Pending':
return 'pending'
return 'unknown'


Expand Down
Loading