"""
The Dashboard-wide status palette (Qt-free, plain data).
This module maps a calibration step's ``(required, status, in_pipeline)``
to a color + glyph + label, exactly as tabulated in
``pypeit_dashboard_design.md`` (the *Status color key* / Calibrations
button table). It returns **plain data** (hex strings, a glyph, a label) so
it stays Qt-free and unit-testable; the *view* converts the hex to a
``QColor``. Both a light and a dark variant are provided (R10: keep
equivalent contrast); the view picks one by theme.
Generated by JXP and Claude.
"""
from dataclasses import dataclass
# Status-category keys (the rows of the design's palette table).
SUCCESS = 'success'
RUNNING = 'running'
FAIL = 'fail'
REQUIRED_UNDONE = 'required_undone'
OPTIONAL = 'optional'
NOT_USED = 'not_used'
# Per-slit-only: a slit intentionally skipped (flats SKIPFLATCALIB). Distinct
# from "fail" (a real problem) and "optional" (a not-required step).
SKIP = 'skip'
# Glyph + human label per category (theme-independent), paired with color so
# status never depends on color alone (R10).
GLYPHS = {
SUCCESS: ('✓', 'success'), # check mark
RUNNING: ('⏳', 'running'), # hourglass
FAIL: ('✗', 'fail'), # ballot X
REQUIRED_UNDONE: ('○', 'required'), # open circle
OPTIONAL: ('–', 'optional'), # en dash
NOT_USED: ('–', 'n/a'), # en dash
SKIP: ('⊘', 'skipped'), # circled slash
}
# Light-theme hex colors (verbatim from the design doc where given).
LIGHT_COLORS = {
SUCCESS: '#2E7D32',
RUNNING: '#EF6C00',
FAIL: '#C62828',
REQUIRED_UNDONE: '#FFFFFF', # needs an outline to read on light bg
OPTIONAL: '#9E9E9E',
NOT_USED: '#BDBDBD',
SKIP: '#607D8B', # blue-grey: intentionally skipped
}
# Dark-theme hex colors: brighter fills to keep equivalent contrast on a
# dark background (first pass; tunable). White/greys still read on dark.
DARK_COLORS = {
SUCCESS: '#66BB6A',
RUNNING: '#FFA726',
FAIL: '#EF5350',
REQUIRED_UNDONE: '#FFFFFF',
OPTIONAL: '#9E9E9E',
NOT_USED: '#616161',
SKIP: '#90A4AE', # lighter blue-grey for dark theme
}
# Statuses (from ``pypeit.state``) that count as "generated successfully".
_SUCCESS_STATUS = ('success', 'complete')
# Distinct, bright control colors — deliberately **not** status colors (so a
# button is never mistaken for a "success"/"fail" status). The (Re)Build
# *action* is blue (C10/X1); "Inspect output" is teal (a different bright hue,
# so the two primary actions are easy to tell apart). The magenta selection
# ring stays "selected"; the status palette stays "status".
ACTION_COLORS = {'light': '#1565C0', 'dark': '#42A5F5'}
INSPECT_COLORS = {'light': '#00838F', 'dark': '#26C6DA'}
# Neutral **row-selection** fill for the dashboard tables (Stage 6 Round-1 #2).
# A soft blue-grey, applied explicitly so a selected row never inherits the
# desktop theme's Highlight color (red on some systems → reads as "failed").
SELECTION_COLORS = {'light': '#CFD8DC', 'dark': '#37474F'}
[docs]
def selection_style(theme='light'):
"""
Return a Qt stylesheet giving a table a **neutral** selected-row fill
(Stage 6 Round-1 #2), so a selected frame reads as "selected", never as a
failure. Applied to the selectable dashboard tables in place of the
desktop theme's (possibly red) Highlight color.
Generated by JXP and Claude.
Args:
theme (:obj:`str`, optional): ``'light'`` (default) or ``'dark'``.
Returns:
str: A ``QTableWidget``/``QListWidget`` ``::item:selected`` stylesheet.
"""
color = SELECTION_COLORS['dark'] if theme == 'dark' \
else SELECTION_COLORS['light']
text = '#FFFFFF' if theme == 'dark' else '#000000'
return (f'QTableWidget::item:selected, QListWidget::item:selected '
f'{{ background-color: {color}; color: {text}; }}')
[docs]
def action_color(theme='light'):
"""
Return the (Re)Build action-control color for a theme (design C10/X1).
Generated by JXP and Claude.
Args:
theme (:obj:`str`, optional): ``'light'`` (default) or ``'dark'``.
Returns:
str: The action hex color (a blue, distinct from any status color).
"""
return ACTION_COLORS['dark'] if theme == 'dark' else ACTION_COLORS['light']
[docs]
def inspect_color(theme='light'):
"""
Return the "Inspect output" control color for a theme.
Generated by JXP and Claude.
Args:
theme (:obj:`str`, optional): ``'light'`` (default) or ``'dark'``.
Returns:
str: The inspect hex color (a teal, distinct from the action blue and
any status color).
"""
return INSPECT_COLORS['dark'] if theme == 'dark' \
else INSPECT_COLORS['light']
[docs]
@dataclass
class StepStyle:
"""
The visual style for one calibration step's state.
Generated by JXP and Claude.
Attributes:
category (str):
One of the palette category keys (e.g. ``success``, ``fail``).
color (str):
Hex color string (e.g. ``#2E7D32``) for the chosen theme.
glyph (str):
A single-character status glyph (paired with color, R10).
label (str):
A short text label for the status.
"""
category: str
color: str
glyph: str
label: str
[docs]
def classify(required, status, in_pipeline):
"""
Map a step's ``(required, status, in_pipeline)`` to a palette category.
Generated by JXP and Claude.
Args:
required (:obj:`bool`, optional):
Whether the step is required. May be ``None`` when unknown
(treated as not required).
status (:obj:`str`, optional):
The step status from ``pypeit.state`` (``success``,
``complete``, ``running``, ``fail``, ``undone``), or a
not-present sentinel (e.g. ``absent``/``None``).
in_pipeline (:obj:`bool`):
Whether the step is part of the active spectrograph's
``default_steps()``.
Returns:
str: The palette category key.
"""
# A step the spectrograph never runs: dimmed, regardless of status.
if not in_pipeline:
return NOT_USED
if status in _SUCCESS_STATUS:
return SUCCESS
if status == RUNNING:
return RUNNING
if status == FAIL:
return FAIL
# Not yet generated (undone / absent / None): required vs optional.
return REQUIRED_UNDONE if required else OPTIONAL
# Severity order (most → least) for summarizing a set of steps into one
# "worst" category, e.g. coloring a configuration-overview navigator cell
# (R17). optional/not_used rank *below* success so they never worsen a cell
# that is otherwise successful.
_SEVERITY = [FAIL, RUNNING, REQUIRED_UNDONE, SUCCESS, OPTIONAL, NOT_USED]
[docs]
def worst_category(categories):
"""
Return the most severe palette category in ``categories``.
Used to color a navigator cell by the worst status among its steps
(precedence ``fail > running > required_undone > success``; ``optional``
and ``not_used`` never worsen an otherwise-successful cell).
Generated by JXP and Claude.
Args:
categories (iterable):
Palette category keys (e.g. from :func:`classify`).
Returns:
str: The most severe category present, or :data:`NOT_USED` if the
input is empty.
"""
present = set(categories)
for category in _SEVERITY:
if category in present:
return category
return NOT_USED
[docs]
def step_style(required, status, in_pipeline, theme='light'):
"""
Return the :class:`StepStyle` for a step's state and theme.
Generated by JXP and Claude.
Args:
required (:obj:`bool`, optional):
Whether the step is required (``None`` treated as not required).
status (:obj:`str`, optional):
The step status (see :func:`classify`).
in_pipeline (:obj:`bool`):
Whether the step is in the spectrograph's ``default_steps()``.
theme (:obj:`str`, optional):
``'light'`` (default) or ``'dark'``.
Returns:
:class:`StepStyle`: The color + glyph + label for the step.
"""
category = classify(required, status, in_pipeline)
colors = DARK_COLORS if theme == 'dark' else LIGHT_COLORS
glyph, label = GLYPHS[category]
return StepStyle(category=category, color=colors[category],
glyph=glyph, label=label)
# Per-slit status → palette category (used by the Calibrations view's
# per-slit/order drill-down). Unlike step_style, a slit has only a status —
# no required/in_pipeline — and may be 'skip' (flats SKIPFLATCALIB).
_SLIT_CATEGORY = {
'success': SUCCESS,
'complete': SUCCESS,
'running': RUNNING,
'fail': FAIL,
'skip': SKIP,
'undone': REQUIRED_UNDONE,
}
[docs]
def slit_style(status, theme='light'):
"""
Return the :class:`StepStyle` for one per-slit/order status.
Generated by JXP and Claude.
Args:
status (:obj:`str`):
The per-slit status (``success``/``complete``/``running``/
``fail``/``skip``/``undone``).
theme (:obj:`str`, optional):
``'light'`` (default) or ``'dark'``.
Returns:
:class:`StepStyle`: The color + glyph + label for the slit.
"""
category = _SLIT_CATEGORY.get(status, REQUIRED_UNDONE)
colors = DARK_COLORS if theme == 'dark' else LIGHT_COLORS
glyph, label = GLYPHS[category]
return StepStyle(category=category, color=colors[category],
glyph=glyph, label=label)