shikiru-cli 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- shikiru/__init__.py +1 -0
- shikiru/__main__.py +8 -0
- shikiru/agents/__init__.py +3 -0
- shikiru/agents/contracts.py +590 -0
- shikiru/agents/registry.py +87 -0
- shikiru/agents/sdk/__init__.py +17 -0
- shikiru/agents/sdk/reset_time.py +137 -0
- shikiru/agents/sdk/turn.py +335 -0
- shikiru/agents/settings.py +151 -0
- shikiru/agents/testing/__init__.py +26 -0
- shikiru/agents/testing/contract_suite.py +653 -0
- shikiru/agents/testing/resume.py +15 -0
- shikiru/bootstrap.py +1277 -0
- shikiru/cli/__init__.py +1 -0
- shikiru/cli/app.py +770 -0
- shikiru/cli/context.py +63 -0
- shikiru/cli/render.py +1475 -0
- shikiru/cli/report.py +156 -0
- shikiru/cli/session_prompt.py +63 -0
- shikiru/cli/view.py +131 -0
- shikiru/clock.py +41 -0
- shikiru/config/__init__.py +8 -0
- shikiru/config/defaults.toml +131 -0
- shikiru/config/loader.py +178 -0
- shikiru/config/settings.py +392 -0
- shikiru/dashboard/__init__.py +3 -0
- shikiru/dashboard/actions_list.py +799 -0
- shikiru/dashboard/activity.py +150 -0
- shikiru/dashboard/app.py +1765 -0
- shikiru/dashboard/blocker_evidence.py +122 -0
- shikiru/dashboard/blocker_rows.py +84 -0
- shikiru/dashboard/briefing_view.py +871 -0
- shikiru/dashboard/contract.py +1086 -0
- shikiru/dashboard/covered.py +32 -0
- shikiru/dashboard/detail.py +1209 -0
- shikiru/dashboard/detail_fold.py +244 -0
- shikiru/dashboard/detail_hint.py +68 -0
- shikiru/dashboard/details_list_view.py +150 -0
- shikiru/dashboard/diagnostics.py +80 -0
- shikiru/dashboard/diagnostics_sections.py +186 -0
- shikiru/dashboard/evidence_table.py +123 -0
- shikiru/dashboard/extra_details.py +214 -0
- shikiru/dashboard/fault_guidance.py +216 -0
- shikiru/dashboard/fold_bodies.py +276 -0
- shikiru/dashboard/help_keys.py +166 -0
- shikiru/dashboard/help_screen.py +204 -0
- shikiru/dashboard/interaction.py +17 -0
- shikiru/dashboard/ladder_editor.py +544 -0
- shikiru/dashboard/live_settings.py +62 -0
- shikiru/dashboard/motion.py +368 -0
- shikiru/dashboard/orchestration_fields.py +201 -0
- shikiru/dashboard/palette.py +1606 -0
- shikiru/dashboard/palette_app.py +89 -0
- shikiru/dashboard/palette_drafts.py +33 -0
- shikiru/dashboard/resolution.py +573 -0
- shikiru/dashboard/resolution_view.py +302 -0
- shikiru/dashboard/row_list.py +256 -0
- shikiru/dashboard/section_rows.py +173 -0
- shikiru/dashboard/services.py +665 -0
- shikiru/dashboard/session_history.py +402 -0
- shikiru/dashboard/shikiru_init_ascii_art.txt +40 -0
- shikiru/dashboard/task_picker.py +358 -0
- shikiru/dashboard/tick.py +59 -0
- shikiru/dashboard/timeline_filter.py +129 -0
- shikiru/dashboard/timeline_section.py +385 -0
- shikiru/dashboard/usage_body.py +269 -0
- shikiru/dashboard/views.py +2314 -0
- shikiru/dashboard/wizard.py +1389 -0
- shikiru/domain/__init__.py +1 -0
- shikiru/domain/acceptance.py +130 -0
- shikiru/domain/activity.py +28 -0
- shikiru/domain/attempts.py +333 -0
- shikiru/domain/briefing.py +257 -0
- shikiru/domain/commands.py +219 -0
- shikiru/domain/decisions.py +231 -0
- shikiru/domain/docker.py +262 -0
- shikiru/domain/engine_uptime.py +87 -0
- shikiru/domain/escalation.py +194 -0
- shikiru/domain/escalation_evidence.py +186 -0
- shikiru/domain/escalation_panel.py +144 -0
- shikiru/domain/events.py +71 -0
- shikiru/domain/evidence_export.py +89 -0
- shikiru/domain/faults.py +86 -0
- shikiru/domain/feedback.py +180 -0
- shikiru/domain/feedback_rounds.py +172 -0
- shikiru/domain/fencing.py +31 -0
- shikiru/domain/ladder.py +102 -0
- shikiru/domain/ladder_text.py +44 -0
- shikiru/domain/levels.py +44 -0
- shikiru/domain/motion.py +199 -0
- shikiru/domain/packet_evidence.py +377 -0
- shikiru/domain/pushes.py +47 -0
- shikiru/domain/quota_blocks.py +44 -0
- shikiru/domain/quota_waits.py +219 -0
- shikiru/domain/recovery.py +194 -0
- shikiru/domain/reports.py +498 -0
- shikiru/domain/results.py +217 -0
- shikiru/domain/runs.py +85 -0
- shikiru/domain/scheduling.py +231 -0
- shikiru/domain/status_tasks.py +40 -0
- shikiru/domain/steps.py +325 -0
- shikiru/domain/tasks.py +174 -0
- shikiru/domain/timeline.py +208 -0
- shikiru/domain/timeline_phrases.py +271 -0
- shikiru/domain/versions.py +18 -0
- shikiru/domain/workflow.py +372 -0
- shikiru/engine/__init__.py +0 -0
- shikiru/engine/acceptance.py +90 -0
- shikiru/engine/agents.py +61 -0
- shikiru/engine/cosmetic_decorator.py +99 -0
- shikiru/engine/daemon.py +409 -0
- shikiru/engine/dependency.py +37 -0
- shikiru/engine/escalation_filing.py +110 -0
- shikiru/engine/escalations.py +401 -0
- shikiru/engine/events.py +285 -0
- shikiru/engine/evidence.py +81 -0
- shikiru/engine/fault_reporter.py +144 -0
- shikiru/engine/feedback.py +577 -0
- shikiru/engine/feedback_audit.py +80 -0
- shikiru/engine/feedback_intake.py +95 -0
- shikiru/engine/feedback_snapshot.py +148 -0
- shikiru/engine/flow.py +87 -0
- shikiru/engine/handlers/__init__.py +33 -0
- shikiru/engine/handlers/base.py +148 -0
- shikiru/engine/handlers/engine.py +24 -0
- shikiru/engine/handlers/escalations.py +174 -0
- shikiru/engine/handlers/operator.py +81 -0
- shikiru/engine/handlers/tasks.py +237 -0
- shikiru/engine/history.py +128 -0
- shikiru/engine/integration.py +1234 -0
- shikiru/engine/integration_turns.py +337 -0
- shikiru/engine/layout_guard.py +362 -0
- shikiru/engine/lifecycle.py +60 -0
- shikiru/engine/monitor.py +317 -0
- shikiru/engine/notification_delivery.py +124 -0
- shikiru/engine/notification_feed.py +57 -0
- shikiru/engine/notifier.py +91 -0
- shikiru/engine/pull_request_closing.py +117 -0
- shikiru/engine/push_service.py +177 -0
- shikiru/engine/quota_probe_threads.py +79 -0
- shikiru/engine/quota_reconciler.py +210 -0
- shikiru/engine/quota_schedule.py +24 -0
- shikiru/engine/quota_waits.py +102 -0
- shikiru/engine/reconcile.py +91 -0
- shikiru/engine/recovery.py +284 -0
- shikiru/engine/retention.py +102 -0
- shikiru/engine/root_preparation.py +86 -0
- shikiru/engine/scheduler.py +320 -0
- shikiru/engine/session_guard.py +46 -0
- shikiru/engine/sessions.py +95 -0
- shikiru/engine/signalled.py +75 -0
- shikiru/engine/step_runner.py +140 -0
- shikiru/engine/supervisor.py +101 -0
- shikiru/engine/takeover.py +223 -0
- shikiru/engine/task_facts.py +46 -0
- shikiru/engine/tick_guard.py +95 -0
- shikiru/engine/titles.py +297 -0
- shikiru/engine/transitions.py +79 -0
- shikiru/engine/turn_context.py +206 -0
- shikiru/engine/turn_execution.py +30 -0
- shikiru/engine/turn_loop.py +230 -0
- shikiru/engine/turn_processes.py +79 -0
- shikiru/engine/turns.py +457 -0
- shikiru/errors.py +144 -0
- shikiru/helper/__init__.py +3 -0
- shikiru/helper/helper.py +182 -0
- shikiru/helper/models.py +11 -0
- shikiru/mcp/__init__.py +1 -0
- shikiru/mcp/server.py +196 -0
- shikiru/operator/__init__.py +33 -0
- shikiru/operator/attachment.py +24 -0
- shikiru/operator/commands.py +319 -0
- shikiru/operator/detectors.py +374 -0
- shikiru/operator/doctor.py +351 -0
- shikiru/operator/events.py +57 -0
- shikiru/operator/evidence_export.py +498 -0
- shikiru/operator/evidence_writer.py +78 -0
- shikiru/operator/lifecycle.py +294 -0
- shikiru/operator/plugin_checks.py +194 -0
- shikiru/operator/probe.py +287 -0
- shikiru/operator/queries.py +1601 -0
- shikiru/operator/services.py +36 -0
- shikiru/operator/sessions.py +378 -0
- shikiru/operator/strip_views.py +162 -0
- shikiru/operator/timeline.py +416 -0
- shikiru/operator/toml_text.py +62 -0
- shikiru/palette.py +18 -0
- shikiru/paths.py +91 -0
- shikiru/plugins/__init__.py +0 -0
- shikiru/plugins/agents/__init__.py +1 -0
- shikiru/plugins/agents/claude/__init__.py +5 -0
- shikiru/plugins/agents/claude/agent.py +251 -0
- shikiru/plugins/agents/claude/context.py +116 -0
- shikiru/plugins/agents/claude/headless.py +92 -0
- shikiru/plugins/agents/claude/hook.py +57 -0
- shikiru/plugins/agents/claude/model_catalog.py +105 -0
- shikiru/plugins/agents/claude/options.py +62 -0
- shikiru/plugins/agents/claude/turn.py +1018 -0
- shikiru/plugins/agents/codex/__init__.py +5 -0
- shikiru/plugins/agents/codex/agent.py +486 -0
- shikiru/plugins/agents/codex/headless.py +166 -0
- shikiru/plugins/agents/codex/home.py +14 -0
- shikiru/plugins/agents/codex/model_catalog.py +128 -0
- shikiru/plugins/agents/codex/options.py +94 -0
- shikiru/plugins/agents/codex/quota.py +195 -0
- shikiru/plugins/agents/codex/sandbox.py +31 -0
- shikiru/plugins/agents/codex/sandbox_probe.py +97 -0
- shikiru/plugins/agents/codex/turn.py +767 -0
- shikiru/plugins/agents/codex/usage.py +106 -0
- shikiru/plugins/agents/shared_settings.py +62 -0
- shikiru/plugins/widgets/__init__.py +3 -0
- shikiru/plugins/widgets/dialogue.py +144 -0
- shikiru/plugins/widgets/disk.py +123 -0
- shikiru/plugins/widgets/docker.py +225 -0
- shikiru/plugins/widgets/escalations.py +402 -0
- shikiru/plugins/widgets/faults.py +204 -0
- shikiru/plugins/widgets/header.py +321 -0
- shikiru/plugins/widgets/queue.py +1140 -0
- shikiru/plugins/widgets/queue_hint.py +93 -0
- shikiru/plugins/widgets/resources.py +105 -0
- shikiru/plugins/widgets/strips.py +82 -0
- shikiru/plugins/widgets/usage.py +176 -0
- shikiru/private.py +94 -0
- shikiru/process.py +535 -0
- shikiru/prompts/__init__.py +3 -0
- shikiru/prompts/renderer.py +85 -0
- shikiru/prompts/templates/_base.md.j2 +17 -0
- shikiru/prompts/templates/_macros.md.j2 +61 -0
- shikiru/prompts/templates/conflicts.md.j2 +22 -0
- shikiru/prompts/templates/consult.md.j2 +5 -0
- shikiru/prompts/templates/continue.md.j2 +4 -0
- shikiru/prompts/templates/feedback_analysis.md.j2 +29 -0
- shikiru/prompts/templates/feedback_rework.md.j2 +33 -0
- shikiru/prompts/templates/feedback_verify.md.j2 +33 -0
- shikiru/prompts/templates/handoff.md.j2 +23 -0
- shikiru/prompts/templates/implement.md.j2 +13 -0
- shikiru/prompts/templates/plan.md.j2 +30 -0
- shikiru/prompts/templates/rework.md.j2 +13 -0
- shikiru/prompts/templates/triage.md.j2 +20 -0
- shikiru/prompts/templates/verify.md.j2 +29 -0
- shikiru/store/__init__.py +1 -0
- shikiru/store/commands.py +242 -0
- shikiru/store/database.py +297 -0
- shikiru/store/escalations.py +240 -0
- shikiru/store/events.py +213 -0
- shikiru/store/feedback.py +266 -0
- shikiru/store/migrations/001_initial.sql +63 -0
- shikiru/store/migrations/002_headless_turns.sql +31 -0
- shikiru/store/migrations/003_drop_command_handled_at.sql +1 -0
- shikiru/store/migrations/004_repo_base_ref.sql +3 -0
- shikiru/store/migrations/005_complexity_explicit.sql +3 -0
- shikiru/store/migrations/006_events_kind_index.sql +1 -0
- shikiru/store/migrations/007_notification_cursor.sql +1 -0
- shikiru/store/migrations/008_reported_usage.sql +1 -0
- shikiru/store/migrations/009_billing_declarations.sql +2 -0
- shikiru/store/migrations/010_canonical_quota_windows.sql +3 -0
- shikiru/store/migrations/012_drop_task_quota_evidence.sql +2 -0
- shikiru/store/migrations/013_push_targets.sql +7 -0
- shikiru/store/migrations/014_agent_block_time.sql +6 -0
- shikiru/store/migrations/015_repair_orphan_agent_blocks.sql +3 -0
- shikiru/store/migrations/016_feedback_rounds.sql +29 -0
- shikiru/store/migrations/017_feedback_round_comments.sql +5 -0
- shikiru/store/migrations/__init__.py +1 -0
- shikiru/store/notification_cursor.py +29 -0
- shikiru/store/pushes.py +117 -0
- shikiru/store/quotas.py +153 -0
- shikiru/store/reader.py +310 -0
- shikiru/store/retention.py +92 -0
- shikiru/store/runs.py +227 -0
- shikiru/store/tasks.py +949 -0
- shikiru/telemetry/__init__.py +1 -0
- shikiru/telemetry/disk.py +167 -0
- shikiru/telemetry/docker.py +164 -0
- shikiru/telemetry/resources.py +296 -0
- shikiru/tmux/__init__.py +3 -0
- shikiru/tmux/client.py +637 -0
- shikiru/tmux/decorator.py +225 -0
- shikiru/tmux/layout.py +200 -0
- shikiru/tmux/session.py +381 -0
- shikiru/ui/__init__.py +9 -0
- shikiru/ui/age.py +45 -0
- shikiru/ui/bubbles.py +41 -0
- shikiru/ui/compact.py +1209 -0
- shikiru/ui/console.py +49 -0
- shikiru/ui/fold.py +50 -0
- shikiru/ui/gated_static.py +117 -0
- shikiru/ui/gauge.py +40 -0
- shikiru/ui/glyphs.py +109 -0
- shikiru/ui/keys.py +57 -0
- shikiru/ui/live_interval.py +42 -0
- shikiru/ui/popup.py +311 -0
- shikiru/ui/quota_status.py +26 -0
- shikiru/ui/recent_window.py +78 -0
- shikiru/ui/screen.py +57 -0
- shikiru/ui/section.py +193 -0
- shikiru/ui/session.py +65 -0
- shikiru/ui/size.py +17 -0
- shikiru/ui/splash.py +42 -0
- shikiru/ui/status.py +47 -0
- shikiru/ui/status_strip.py +149 -0
- shikiru/ui/stream.py +152 -0
- shikiru/ui/strip.py +332 -0
- shikiru/ui/theme.py +83 -0
- shikiru/ui/tokens.py +51 -0
- shikiru/view_log.py +126 -0
- shikiru/workspace/__init__.py +3 -0
- shikiru/workspace/base_refs.py +160 -0
- shikiru/workspace/caches.py +136 -0
- shikiru/workspace/facts.py +118 -0
- shikiru/workspace/git.py +601 -0
- shikiru/workspace/github.py +744 -0
- shikiru/workspace/messages.py +107 -0
- shikiru/workspace/reports.py +145 -0
- shikiru/workspace/roots.py +180 -0
- shikiru_cli-0.2.0.dist-info/METADATA +428 -0
- shikiru_cli-0.2.0.dist-info/RECORD +319 -0
- shikiru_cli-0.2.0.dist-info/WHEEL +4 -0
- shikiru_cli-0.2.0.dist-info/entry_points.txt +18 -0
- shikiru_cli-0.2.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
"""Renderables of the escalation resolution screen: title, Decision, Choose, choices, evidence."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from enum import StrEnum, auto
|
|
7
|
+
from typing import TYPE_CHECKING
|
|
8
|
+
|
|
9
|
+
from rich.console import Group, RenderableType
|
|
10
|
+
from rich.table import Table
|
|
11
|
+
from rich.text import Text
|
|
12
|
+
|
|
13
|
+
from shikiru.domain.attempts import AttemptItem, AttemptKind, AttemptTone
|
|
14
|
+
from shikiru.domain.escalation_panel import ChoiceKind
|
|
15
|
+
from shikiru.ui.fold import fold_arrow, fold_toggle_hint
|
|
16
|
+
from shikiru.ui.keys import key_hint
|
|
17
|
+
from shikiru.ui.section import (
|
|
18
|
+
BOLD_STEEL,
|
|
19
|
+
GROUP_SEPARATOR,
|
|
20
|
+
Row,
|
|
21
|
+
Section,
|
|
22
|
+
grey,
|
|
23
|
+
key_label,
|
|
24
|
+
labelled_rows,
|
|
25
|
+
row_name,
|
|
26
|
+
)
|
|
27
|
+
from shikiru.ui.tokens import (
|
|
28
|
+
AMBER,
|
|
29
|
+
DUSTY_ROSE,
|
|
30
|
+
PRIMARY_TEXT_COLOR,
|
|
31
|
+
SAGE,
|
|
32
|
+
SECONDARY_TEXT_COLOR,
|
|
33
|
+
STEEL_BLUE,
|
|
34
|
+
STYLE_BOLD,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
if TYPE_CHECKING:
|
|
38
|
+
from rich.console import Console, ConsoleOptions, RenderResult
|
|
39
|
+
|
|
40
|
+
from shikiru.domain.escalation_panel import PanelChoice, ResolutionPanel
|
|
41
|
+
|
|
42
|
+
NARROW_MAX_COLUMNS = 48
|
|
43
|
+
FOLDED_MAX_COLUMNS = 40
|
|
44
|
+
TINY_MAX_ROWS = 8
|
|
45
|
+
_LABEL_WIDTH = 9
|
|
46
|
+
_MARK_WIDTH = 3
|
|
47
|
+
_RECOMMENDED_STAR = "\N{BLACK STAR}"
|
|
48
|
+
_OWN_ANSWER_LABEL = "Write your own answer\N{HORIZONTAL ELLIPSIS}"
|
|
49
|
+
_ACCEPT_LABEL = "Accept"
|
|
50
|
+
_RETRY_LABEL = "Retry"
|
|
51
|
+
_CANCEL_LABEL = "Cancel task"
|
|
52
|
+
_CANCEL_CONFIRM_LABEL = "Press Enter again to cancel"
|
|
53
|
+
_DECISION_TITLE = "Decision"
|
|
54
|
+
_CHOOSE_TITLE = "Choose"
|
|
55
|
+
_SELECT_HINT = "\N{UPWARDS ARROW}\N{DOWNWARDS ARROW} select"
|
|
56
|
+
_SEND_HINT = "Enter send"
|
|
57
|
+
_EXPAND_HINT = "Enter expand"
|
|
58
|
+
_BACK_HINT = "\N{UPWARDS ARROW} choices"
|
|
59
|
+
_COLLAPSE_HINT = "Enter collapse"
|
|
60
|
+
_EDIT_HINT = "Enter edit"
|
|
61
|
+
_CHOICE_HINTS = frozenset({_SEND_HINT, _EDIT_HINT})
|
|
62
|
+
_BOX_HINTS = ("Ctrl+S send", "Enter new line", "Esc back")
|
|
63
|
+
_NEEDS_INPUT_MARK = "\N{LOWER RIGHT PENCIL}"
|
|
64
|
+
_NEEDS_INPUT_TEXT = "needs your input"
|
|
65
|
+
_DETAILS_TITLE = "Details"
|
|
66
|
+
EXTRA_DETAILS_KEY = "o"
|
|
67
|
+
_EXTRA_DETAILS_LABEL = "extra details"
|
|
68
|
+
_TURNS_LABEL = "Turns"
|
|
69
|
+
_PART_SEPARATOR = " \N{MIDDLE DOT} "
|
|
70
|
+
_TURN_KINDS = frozenset({AttemptKind.TURN_OK, AttemptKind.TURN_FAILED, AttemptKind.VERDICT})
|
|
71
|
+
_DETAIL_LABELS = {
|
|
72
|
+
AttemptKind.REASON: "Reason",
|
|
73
|
+
AttemptKind.ROUNDS: "Rounds",
|
|
74
|
+
AttemptKind.RECOVERY: "Recovery",
|
|
75
|
+
AttemptKind.PRODUCED: "Produced",
|
|
76
|
+
AttemptKind.OPERATOR: "Operator",
|
|
77
|
+
}
|
|
78
|
+
_TONE_STYLES = {
|
|
79
|
+
AttemptTone.PLAIN: PRIMARY_TEXT_COLOR,
|
|
80
|
+
AttemptTone.STEP: STEEL_BLUE,
|
|
81
|
+
AttemptTone.SUCCESS: SAGE,
|
|
82
|
+
AttemptTone.FAILURE: DUSTY_ROSE,
|
|
83
|
+
AttemptTone.REWORK: AMBER,
|
|
84
|
+
}
|
|
85
|
+
_WAITING_WORD = "waiting"
|
|
86
|
+
_TITLE_WIDE = "Resolve escalation"
|
|
87
|
+
_TITLE_NARROW = "Resolve"
|
|
88
|
+
_LABEL_TASK = "Task"
|
|
89
|
+
_LABEL_QUESTION = "Question"
|
|
90
|
+
_LABEL_REASON = "Reason"
|
|
91
|
+
|
|
92
|
+
_LABEL_STYLES = {
|
|
93
|
+
ChoiceKind.ANSWER_OPTION: STEEL_BLUE,
|
|
94
|
+
ChoiceKind.OWN_ANSWER: STEEL_BLUE,
|
|
95
|
+
ChoiceKind.RETRY: STEEL_BLUE,
|
|
96
|
+
ChoiceKind.ACCEPT: SAGE,
|
|
97
|
+
ChoiceKind.CANCEL: DUSTY_ROSE,
|
|
98
|
+
}
|
|
99
|
+
_FIXED_LABELS = {
|
|
100
|
+
ChoiceKind.OWN_ANSWER: _OWN_ANSWER_LABEL,
|
|
101
|
+
ChoiceKind.ACCEPT: _ACCEPT_LABEL,
|
|
102
|
+
ChoiceKind.RETRY: _RETRY_LABEL,
|
|
103
|
+
ChoiceKind.CANCEL: _CANCEL_LABEL,
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class ResolutionLayout(StrEnum):
|
|
108
|
+
"""How much of the panel a pane size leaves room for."""
|
|
109
|
+
|
|
110
|
+
WIDE = auto()
|
|
111
|
+
NARROW = auto()
|
|
112
|
+
FOLDED = auto()
|
|
113
|
+
TINY = auto()
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
_FOLDED_LAYOUTS = frozenset({ResolutionLayout.FOLDED, ResolutionLayout.TINY})
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def layout_for(width: int, height: int) -> ResolutionLayout:
|
|
120
|
+
"""Pick the layout for a screen of `width` by `height` cells."""
|
|
121
|
+
if height <= TINY_MAX_ROWS:
|
|
122
|
+
return ResolutionLayout.TINY
|
|
123
|
+
if width <= FOLDED_MAX_COLUMNS:
|
|
124
|
+
return ResolutionLayout.FOLDED
|
|
125
|
+
if width <= NARROW_MAX_COLUMNS:
|
|
126
|
+
return ResolutionLayout.NARROW
|
|
127
|
+
return ResolutionLayout.WIDE
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def resolution_title(
|
|
131
|
+
escalation_id: str, panel: ResolutionPanel | None, layout: ResolutionLayout
|
|
132
|
+
) -> str:
|
|
133
|
+
"""Return the popup title: ids, with task and reason folded in when the pane is small."""
|
|
134
|
+
if panel is None:
|
|
135
|
+
return GROUP_SEPARATOR.join((_TITLE_NARROW, escalation_id))
|
|
136
|
+
if layout in _FOLDED_LAYOUTS:
|
|
137
|
+
return GROUP_SEPARATOR.join((panel.task_id, panel.reason_label))
|
|
138
|
+
head = _TITLE_WIDE if layout is ResolutionLayout.WIDE else _TITLE_NARROW
|
|
139
|
+
return GROUP_SEPARATOR.join((head, escalation_id, panel.task_id))
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
@dataclass(frozen=True)
|
|
143
|
+
class _DecisionRows:
|
|
144
|
+
panel: ResolutionPanel
|
|
145
|
+
folded: bool
|
|
146
|
+
|
|
147
|
+
def __rich_console__(self, console: Console, options: ConsoleOptions) -> RenderResult:
|
|
148
|
+
del console
|
|
149
|
+
panel = self.panel
|
|
150
|
+
if self.folded:
|
|
151
|
+
yield row_name(panel.question) if panel.question else grey(panel.cause or "")
|
|
152
|
+
return
|
|
153
|
+
rows: list[Row] = [
|
|
154
|
+
(grey(_LABEL_TASK), (row_name(panel.task_id), Text(panel.task_title))),
|
|
155
|
+
]
|
|
156
|
+
if panel.question:
|
|
157
|
+
rows.append((grey(_LABEL_QUESTION), (row_name(panel.question),)))
|
|
158
|
+
if panel.cause or not panel.question:
|
|
159
|
+
reason = Text()
|
|
160
|
+
reason.append_text(row_name(panel.reason_label))
|
|
161
|
+
if panel.cause:
|
|
162
|
+
reason.append("\n")
|
|
163
|
+
reason.append(panel.cause, style=SECONDARY_TEXT_COLOR)
|
|
164
|
+
rows.append((grey(_LABEL_REASON), (reason,)))
|
|
165
|
+
yield labelled_rows(rows, options.max_width, _LABEL_WIDTH)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def decision_renderable(
|
|
169
|
+
panel: ResolutionPanel, age: str, layout: ResolutionLayout
|
|
170
|
+
) -> RenderableType:
|
|
171
|
+
"""Return the Decision section, or only its question when the pane is folded."""
|
|
172
|
+
folded = layout in _FOLDED_LAYOUTS
|
|
173
|
+
rows = _DecisionRows(panel, folded)
|
|
174
|
+
if folded:
|
|
175
|
+
return rows
|
|
176
|
+
return Section(_DECISION_TITLE, rows, grey(f"{_WAITING_WORD} {age}"), ruled=False)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _hints(selected: PanelChoice, details_open: bool | None, *, editing: bool) -> tuple[str, ...]:
|
|
180
|
+
if editing:
|
|
181
|
+
return _BOX_HINTS
|
|
182
|
+
if details_open is not None:
|
|
183
|
+
return (_COLLAPSE_HINT if details_open else _EXPAND_HINT, _BACK_HINT)
|
|
184
|
+
editable = selected.kind is ChoiceKind.OWN_ANSWER or selected.needs_input
|
|
185
|
+
return (_SELECT_HINT, _EDIT_HINT if editable else _SEND_HINT)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def choose_renderable(
|
|
189
|
+
selected: PanelChoice,
|
|
190
|
+
*,
|
|
191
|
+
details_open: bool | None = None,
|
|
192
|
+
editing: bool = False,
|
|
193
|
+
choosing_disabled: bool = False,
|
|
194
|
+
) -> RenderableType:
|
|
195
|
+
"""Return the Choose heading with its key hint.
|
|
196
|
+
|
|
197
|
+
`details_open` is `None` while the list has focus, else whether the focused Details is open.
|
|
198
|
+
`choosing_disabled` greys the `Enter` key of the list hint, which then does nothing.
|
|
199
|
+
"""
|
|
200
|
+
hints = _hints(selected, details_open, editing=editing)
|
|
201
|
+
inert = choosing_disabled and details_open is None and not editing
|
|
202
|
+
parts = [
|
|
203
|
+
key_hint(hint, SECONDARY_TEXT_COLOR, enabled=not (inert and hint in _CHOICE_HINTS))
|
|
204
|
+
for hint in hints
|
|
205
|
+
]
|
|
206
|
+
hint = Text(GROUP_SEPARATOR, style=SECONDARY_TEXT_COLOR).join(parts)
|
|
207
|
+
return Section(_CHOOSE_TITLE, Group(), hint, title_style=BOLD_STEEL)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def _toned(item: AttemptItem) -> Text:
|
|
211
|
+
if item.kind is AttemptKind.EARLIER:
|
|
212
|
+
return grey(item.text)
|
|
213
|
+
return Text.assemble(*((part.text, _TONE_STYLES[part.tone]) for part in item.parts))
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def _turn_row(item: AttemptItem) -> Text:
|
|
217
|
+
styled = [(part.text, _TONE_STYLES[part.tone]) for part in item.parts]
|
|
218
|
+
role = (item.label, PRIMARY_TEXT_COLOR)
|
|
219
|
+
step = item.parts[0]
|
|
220
|
+
if step.tone is AttemptTone.STEP and item.text.startswith(step.text + _PART_SEPARATOR):
|
|
221
|
+
return Text.assemble(styled[0], _PART_SEPARATOR, role, *styled[1:])
|
|
222
|
+
return Text.assemble(role, _PART_SEPARATOR, *styled)
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def _detail_rows(items: tuple[AttemptItem, ...]) -> RenderableType:
|
|
226
|
+
grid = Table.grid()
|
|
227
|
+
grid.add_column(width=_LABEL_WIDTH + 1, no_wrap=True)
|
|
228
|
+
grid.add_column(ratio=1)
|
|
229
|
+
turns = [item for item in items if item.kind in _TURN_KINDS]
|
|
230
|
+
for item in items:
|
|
231
|
+
if item.kind in _TURN_KINDS:
|
|
232
|
+
if item is turns[0]:
|
|
233
|
+
grid.add_row(grey(_TURNS_LABEL), Group(*map(_turn_row, turns)))
|
|
234
|
+
elif item.kind is not AttemptKind.EARLIER:
|
|
235
|
+
grid.add_row(grey(_DETAIL_LABELS[item.kind]), _toned(item))
|
|
236
|
+
for item in items:
|
|
237
|
+
if item.kind is AttemptKind.EARLIER:
|
|
238
|
+
grid.add_row(Text(), _toned(item))
|
|
239
|
+
return grid
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def tried_renderable(
|
|
243
|
+
items: tuple[AttemptItem, ...], *, expanded: bool, extra_details: bool = False
|
|
244
|
+
) -> RenderableType:
|
|
245
|
+
"""Return the collapsible `Details` section with its toggle hint, without its rule.
|
|
246
|
+
|
|
247
|
+
An expanded section ends with the `extra details` hint when `extra_details` is set.
|
|
248
|
+
"""
|
|
249
|
+
arrow = fold_arrow(expanded=expanded)
|
|
250
|
+
body: RenderableType = Group()
|
|
251
|
+
if expanded:
|
|
252
|
+
rows = _detail_rows(items)
|
|
253
|
+
body = (
|
|
254
|
+
Group(rows, key_label(EXTRA_DETAILS_KEY, _EXTRA_DETAILS_LABEL))
|
|
255
|
+
if extra_details
|
|
256
|
+
else rows
|
|
257
|
+
)
|
|
258
|
+
return Section(
|
|
259
|
+
f"{_DETAILS_TITLE} {arrow}",
|
|
260
|
+
body,
|
|
261
|
+
trailing=fold_toggle_hint(),
|
|
262
|
+
ruled=False,
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def starred_indexes(panel: ResolutionPanel) -> frozenset[int]:
|
|
267
|
+
"""Return the rows to star: recommended ones, the own-answer row only when it stands alone."""
|
|
268
|
+
has_alternatives = any(c.kind is ChoiceKind.ANSWER_OPTION for c in panel.choices)
|
|
269
|
+
return frozenset(
|
|
270
|
+
index
|
|
271
|
+
for index, choice in enumerate(panel.choices)
|
|
272
|
+
if choice.recommended and not (has_alternatives and choice.kind is ChoiceKind.OWN_ANSWER)
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def _consequence(choice: PanelChoice, *, disabled: bool) -> Text:
|
|
277
|
+
if not choice.needs_input:
|
|
278
|
+
return grey(choice.consequence)
|
|
279
|
+
lead = Text(_NEEDS_INPUT_TEXT, style=SECONDARY_TEXT_COLOR if disabled else PRIMARY_TEXT_COLOR)
|
|
280
|
+
lead.append(GROUP_SEPARATOR + choice.consequence, style=SECONDARY_TEXT_COLOR)
|
|
281
|
+
return lead
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def choice_renderable(
|
|
285
|
+
choice: PanelChoice, *, starred: bool, armed: bool, disabled: bool
|
|
286
|
+
) -> RenderableType:
|
|
287
|
+
"""Return one choice row: star beside the label, its consequence beneath in grey."""
|
|
288
|
+
style = SECONDARY_TEXT_COLOR if disabled else _LABEL_STYLES[choice.kind]
|
|
289
|
+
if armed:
|
|
290
|
+
label = key_hint(_CANCEL_CONFIRM_LABEL, f"{STYLE_BOLD} {DUSTY_ROSE}")
|
|
291
|
+
else:
|
|
292
|
+
label = Text(choice.label or _FIXED_LABELS[choice.kind], style=style)
|
|
293
|
+
if choice.needs_input:
|
|
294
|
+
label.append(f" {_NEEDS_INPUT_MARK}")
|
|
295
|
+
star = _RECOMMENDED_STAR if starred else " "
|
|
296
|
+
grid = Table.grid()
|
|
297
|
+
grid.add_column(width=_MARK_WIDTH, no_wrap=True)
|
|
298
|
+
grid.add_column(ratio=1)
|
|
299
|
+
grid.add_row(Text(f" {star} ", style=style), label)
|
|
300
|
+
if choice.kind is not ChoiceKind.OWN_ANSWER:
|
|
301
|
+
grid.add_row(Text(" " * _MARK_WIDTH), _consequence(choice, disabled=disabled))
|
|
302
|
+
return grid
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
"""A keyboard-driven list of one-line rows shared by the task Events and Relationships views."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import TYPE_CHECKING, ClassVar
|
|
7
|
+
|
|
8
|
+
from rich.cells import cell_len
|
|
9
|
+
from rich.text import Text
|
|
10
|
+
from textual import on
|
|
11
|
+
from textual.binding import Binding
|
|
12
|
+
from textual.containers import Vertical
|
|
13
|
+
from textual.message import Message
|
|
14
|
+
from textual.widgets import Static
|
|
15
|
+
|
|
16
|
+
from shikiru.dashboard.details_list_view import ROW_LEAD
|
|
17
|
+
from shikiru.ui.section import BOLD_PRIMARY, grey
|
|
18
|
+
from shikiru.ui.tokens import PRIMARY_TEXT_COLOR
|
|
19
|
+
|
|
20
|
+
if TYPE_CHECKING:
|
|
21
|
+
from collections.abc import Sequence
|
|
22
|
+
|
|
23
|
+
from rich.console import Console, ConsoleOptions, RenderableType, RenderResult
|
|
24
|
+
from textual import events
|
|
25
|
+
from textual.binding import BindingType
|
|
26
|
+
|
|
27
|
+
_LEAD_GAP = " "
|
|
28
|
+
_TAIL_GAP = " "
|
|
29
|
+
_STEP_UP = -1
|
|
30
|
+
_STEP_DOWN = 1
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass(frozen=True)
|
|
34
|
+
class RowCells:
|
|
35
|
+
"""One row: a grey lead, the primary text and an optional styled tail that is never cut."""
|
|
36
|
+
|
|
37
|
+
lead: str
|
|
38
|
+
text: str | Text
|
|
39
|
+
tail: Text | None = None
|
|
40
|
+
columns: tuple[str | Text, str | Text] | None = None
|
|
41
|
+
column_width: int = 0
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass(frozen=True)
|
|
45
|
+
class _RowLine:
|
|
46
|
+
cells: RowCells
|
|
47
|
+
selected: bool
|
|
48
|
+
|
|
49
|
+
def __rich_console__(self, console: Console, options: ConsoleOptions) -> RenderResult:
|
|
50
|
+
if self.cells.columns is not None:
|
|
51
|
+
yield from self._column_lines(console, options)
|
|
52
|
+
return
|
|
53
|
+
line = Text(ROW_LEAD)
|
|
54
|
+
line.append_text(grey(f"{self.cells.lead}{_LEAD_GAP}" if self.cells.lead else ""))
|
|
55
|
+
tail = self.cells.tail or Text()
|
|
56
|
+
gap = _TAIL_GAP if tail else ""
|
|
57
|
+
body = Text(style=BOLD_PRIMARY if self.selected else PRIMARY_TEXT_COLOR)
|
|
58
|
+
body.append_text(
|
|
59
|
+
Text(self.cells.text) if isinstance(self.cells.text, str) else self.cells.text
|
|
60
|
+
)
|
|
61
|
+
room = max(options.max_width - line.cell_len - cell_len(gap) - tail.cell_len, 1)
|
|
62
|
+
body.truncate(room, overflow="ellipsis")
|
|
63
|
+
line.append_text(body)
|
|
64
|
+
line.append(gap)
|
|
65
|
+
line.append_text(tail)
|
|
66
|
+
line.truncate(max(options.max_width, 1), overflow="ellipsis", pad=True)
|
|
67
|
+
yield line
|
|
68
|
+
|
|
69
|
+
def _column_lines(self, console: Console, options: ConsoleOptions) -> RenderResult:
|
|
70
|
+
columns = self.cells.columns
|
|
71
|
+
if columns is None:
|
|
72
|
+
return
|
|
73
|
+
label, value = columns
|
|
74
|
+
label_text = Text(label) if isinstance(label, str) else label.copy()
|
|
75
|
+
value_text = Text(value) if isinstance(value, str) else value.copy()
|
|
76
|
+
label_text.pad_right(max(self.cells.column_width - label_text.cell_len, 0))
|
|
77
|
+
prefix_width = len(ROW_LEAD) + self.cells.column_width + 1
|
|
78
|
+
value_width = max(options.max_width - prefix_width, 1)
|
|
79
|
+
lines = value_text.wrap(console, value_width, overflow="fold") or [Text()]
|
|
80
|
+
for index, value_line in enumerate(lines):
|
|
81
|
+
line = Text(ROW_LEAD)
|
|
82
|
+
if index == 0:
|
|
83
|
+
line.append_text(label_text)
|
|
84
|
+
else:
|
|
85
|
+
line.append(" " * self.cells.column_width)
|
|
86
|
+
line.append(" ")
|
|
87
|
+
line.append_text(value_line)
|
|
88
|
+
yield line
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class _Row(Static):
|
|
92
|
+
class Chosen(Message):
|
|
93
|
+
def __init__(self, index: int) -> None:
|
|
94
|
+
super().__init__()
|
|
95
|
+
self.index = index
|
|
96
|
+
|
|
97
|
+
def __init__(self, index: int, content: RenderableType, *, selected: bool) -> None:
|
|
98
|
+
super().__init__(content, classes="row-list-row", markup=False)
|
|
99
|
+
self._index = index
|
|
100
|
+
self.set_class(selected, "-selected")
|
|
101
|
+
|
|
102
|
+
def on_click(self, event: events.Click) -> None:
|
|
103
|
+
event.stop()
|
|
104
|
+
self.post_message(self.Chosen(self._index))
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class RowList(Vertical, can_focus=True):
|
|
108
|
+
"""Rows with a selected background; arrows move, PgUp/PgDn page, End asks for the latest."""
|
|
109
|
+
|
|
110
|
+
BINDINGS: ClassVar[list[BindingType]] = [
|
|
111
|
+
Binding("up", f"move({_STEP_UP})", "Previous", show=False),
|
|
112
|
+
Binding("down", f"move({_STEP_DOWN})", "Next", show=False),
|
|
113
|
+
Binding("pageup", "page(0)", "Newer page", show=False),
|
|
114
|
+
Binding("pagedown", "page(1)", "Older page", show=False),
|
|
115
|
+
Binding("end", "latest", "Latest", show=False),
|
|
116
|
+
Binding("enter", "open", "Open", show=False),
|
|
117
|
+
]
|
|
118
|
+
DEFAULT_CSS = """
|
|
119
|
+
RowList { height: auto; }
|
|
120
|
+
RowList:focus { background-tint: transparent; }
|
|
121
|
+
.row-list-row { height: 1; width: 100%; }
|
|
122
|
+
.row-list-row.-selected { background: $selected-surface; }
|
|
123
|
+
"""
|
|
124
|
+
|
|
125
|
+
class _Posted(Message):
|
|
126
|
+
def __init__(self, row_list: RowList) -> None:
|
|
127
|
+
super().__init__()
|
|
128
|
+
self.row_list = row_list
|
|
129
|
+
|
|
130
|
+
@property
|
|
131
|
+
def control(self) -> RowList:
|
|
132
|
+
return self.row_list
|
|
133
|
+
|
|
134
|
+
class Moved(_Posted):
|
|
135
|
+
"""The selection moved to `index`."""
|
|
136
|
+
|
|
137
|
+
def __init__(self, row_list: RowList, index: int) -> None:
|
|
138
|
+
super().__init__(row_list)
|
|
139
|
+
self.index = index
|
|
140
|
+
|
|
141
|
+
class Opened(_Posted):
|
|
142
|
+
"""The row at `index` was opened."""
|
|
143
|
+
|
|
144
|
+
def __init__(self, row_list: RowList, index: int) -> None:
|
|
145
|
+
super().__init__(row_list)
|
|
146
|
+
self.index = index
|
|
147
|
+
|
|
148
|
+
class Paged(_Posted):
|
|
149
|
+
"""The next (`older`) or previous page was asked for, by an arrow past an end or by key."""
|
|
150
|
+
|
|
151
|
+
def __init__(self, row_list: RowList, *, older: bool, by_arrow: bool = False) -> None:
|
|
152
|
+
super().__init__(row_list)
|
|
153
|
+
self.older = older
|
|
154
|
+
self.by_arrow = by_arrow
|
|
155
|
+
|
|
156
|
+
class Latest(_Posted):
|
|
157
|
+
"""The latest rows were asked for."""
|
|
158
|
+
|
|
159
|
+
class Scrolled(_Posted):
|
|
160
|
+
"""The operator scrolled the rows away from the first one."""
|
|
161
|
+
|
|
162
|
+
def __init__(self, *, widget_id: str, edge_paging: bool = False) -> None:
|
|
163
|
+
"""Create an empty list; rows arrive through `set_rows`.
|
|
164
|
+
|
|
165
|
+
With `edge_paging`, moving past the last or first row asks for the older or newer page.
|
|
166
|
+
"""
|
|
167
|
+
super().__init__(id=widget_id)
|
|
168
|
+
self._edge_paging = edge_paging
|
|
169
|
+
self._cells: tuple[RowCells, ...] = ()
|
|
170
|
+
self._selected = 0
|
|
171
|
+
self._empty = ""
|
|
172
|
+
self._widgets: list[_Row] = []
|
|
173
|
+
|
|
174
|
+
@property
|
|
175
|
+
def selected(self) -> int:
|
|
176
|
+
"""The index of the selected row."""
|
|
177
|
+
return self._selected
|
|
178
|
+
|
|
179
|
+
def set_rows(self, rows: Sequence[RowCells], *, selected: int, empty: str) -> None:
|
|
180
|
+
"""Replace the rows, select `selected` (clamped) and show `empty` when none."""
|
|
181
|
+
previous = self._selected
|
|
182
|
+
self._cells = tuple(rows)
|
|
183
|
+
self._empty = empty
|
|
184
|
+
self._selected = min(max(selected, 0), max(len(self._cells) - 1, 0))
|
|
185
|
+
self._paint()
|
|
186
|
+
if self._selected != previous:
|
|
187
|
+
self._reveal()
|
|
188
|
+
|
|
189
|
+
def action_move(self, step: int) -> None:
|
|
190
|
+
"""Move the selection; at an end, page instead when edge paging is on."""
|
|
191
|
+
target = self._selected + step
|
|
192
|
+
if 0 <= target < len(self._cells):
|
|
193
|
+
self._choose(target)
|
|
194
|
+
elif self._edge_paging:
|
|
195
|
+
self.post_message(self.Paged(self, older=step > 0, by_arrow=True))
|
|
196
|
+
|
|
197
|
+
def action_page(self, older: int) -> None:
|
|
198
|
+
"""Ask for the older (PgDn) or newer (PgUp) page."""
|
|
199
|
+
self.post_message(self.Paged(self, older=bool(older)))
|
|
200
|
+
|
|
201
|
+
def action_latest(self) -> None:
|
|
202
|
+
"""Ask to jump to the latest rows and follow them."""
|
|
203
|
+
self.scroll_home(animate=False)
|
|
204
|
+
self.post_message(self.Latest(self))
|
|
205
|
+
|
|
206
|
+
def action_open(self) -> None:
|
|
207
|
+
"""Open the selected row."""
|
|
208
|
+
if self._cells:
|
|
209
|
+
self.post_message(self.Opened(self, self._selected))
|
|
210
|
+
|
|
211
|
+
def watch_scroll_y(self, old_value: float, new_value: float) -> None:
|
|
212
|
+
"""Tell the owner when the rows are scrolled away from the first one."""
|
|
213
|
+
super().watch_scroll_y(old_value, new_value)
|
|
214
|
+
if new_value > 0:
|
|
215
|
+
self.post_message(self.Scrolled(self))
|
|
216
|
+
|
|
217
|
+
@on(_Row.Chosen)
|
|
218
|
+
def _row_chosen(self, message: _Row.Chosen) -> None:
|
|
219
|
+
message.stop()
|
|
220
|
+
self._choose(message.index)
|
|
221
|
+
self.action_open()
|
|
222
|
+
|
|
223
|
+
def _choose(self, index: int) -> None:
|
|
224
|
+
if index == self._selected:
|
|
225
|
+
return
|
|
226
|
+
self._selected = index
|
|
227
|
+
self._paint()
|
|
228
|
+
self._reveal()
|
|
229
|
+
self.post_message(self.Moved(self, index))
|
|
230
|
+
|
|
231
|
+
def _lines(self) -> list[RenderableType]:
|
|
232
|
+
if not self._cells:
|
|
233
|
+
return [grey(self._empty)]
|
|
234
|
+
return [_RowLine(cells, index == self._selected) for index, cells in enumerate(self._cells)]
|
|
235
|
+
|
|
236
|
+
def _paint(self) -> None:
|
|
237
|
+
lines = self._lines()
|
|
238
|
+
highlighted = bool(self._cells)
|
|
239
|
+
for index, (row, line) in enumerate(zip(self._widgets, lines, strict=False)):
|
|
240
|
+
row.update(line)
|
|
241
|
+
row.set_class(highlighted and index == self._selected, "-selected")
|
|
242
|
+
for row in self._widgets[len(lines) :]:
|
|
243
|
+
row.remove()
|
|
244
|
+
del self._widgets[len(lines) :]
|
|
245
|
+
fresh = [
|
|
246
|
+
_Row(index, line, selected=highlighted and index == self._selected)
|
|
247
|
+
for index, line in enumerate(lines)
|
|
248
|
+
if index >= len(self._widgets)
|
|
249
|
+
]
|
|
250
|
+
self._widgets.extend(fresh)
|
|
251
|
+
if fresh:
|
|
252
|
+
self.mount(*fresh)
|
|
253
|
+
|
|
254
|
+
def _reveal(self) -> None:
|
|
255
|
+
if self._selected < len(self._widgets):
|
|
256
|
+
self._widgets[self._selected].scroll_visible(animate=False)
|