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
shikiru/ui/popup.py
ADDED
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
from textual.containers import Vertical
|
|
6
|
+
from textual.css.scalar import Unit
|
|
7
|
+
from textual.geometry import Size
|
|
8
|
+
|
|
9
|
+
from shikiru.ui.keys import key_hint
|
|
10
|
+
from shikiru.ui.screen import widget_is_attached
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from rich.text import Text
|
|
14
|
+
from textual import events
|
|
15
|
+
from textual.widget import Widget
|
|
16
|
+
|
|
17
|
+
POPUP_FRAME_CLASS = "popup-frame"
|
|
18
|
+
POPUP_HEADER_CLASS = "popup-header"
|
|
19
|
+
POPUP_TITLE_CLASS = "popup-title"
|
|
20
|
+
POPUP_CLOSE_CLASS = "popup-close-control"
|
|
21
|
+
POPUP_RULE_CLASS = "popup-rule"
|
|
22
|
+
POPUP_BODY_CLASS = "popup-body"
|
|
23
|
+
POPUP_SECONDARY_CLASS = "popup-secondary"
|
|
24
|
+
POPUP_EDIT_CLASS = "popup-edit"
|
|
25
|
+
POPUP_ROW_CLASS = "palette-row"
|
|
26
|
+
POPUP_CLOSE_LABEL = "Esc \N{MULTIPLICATION SIGN}"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def popup_close_label() -> Text:
|
|
30
|
+
"""Return the Esc close label with its key in the key style."""
|
|
31
|
+
return key_hint(POPUP_CLOSE_LABEL)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class PopupFrame(Vertical):
|
|
35
|
+
"""Keep a content-sized popup frame stable when its screen is resized."""
|
|
36
|
+
|
|
37
|
+
def __init__(
|
|
38
|
+
self,
|
|
39
|
+
*children: Widget,
|
|
40
|
+
name: str | None = None,
|
|
41
|
+
widget_id: str | None = None,
|
|
42
|
+
classes: str | None = None,
|
|
43
|
+
disabled: bool = False,
|
|
44
|
+
markup: bool = True,
|
|
45
|
+
) -> None:
|
|
46
|
+
super().__init__(
|
|
47
|
+
*children,
|
|
48
|
+
name=name,
|
|
49
|
+
id=widget_id,
|
|
50
|
+
classes=classes,
|
|
51
|
+
disabled=disabled,
|
|
52
|
+
markup=markup,
|
|
53
|
+
)
|
|
54
|
+
self._content_ready = False
|
|
55
|
+
self._preferred_size: Size | None = None
|
|
56
|
+
self._applied_size: Size | None = None
|
|
57
|
+
|
|
58
|
+
def content_ready(self) -> None:
|
|
59
|
+
"""Mark meaningful initial content as ready for preferred-size capture."""
|
|
60
|
+
if not self.is_mounted or not widget_is_attached(self):
|
|
61
|
+
return
|
|
62
|
+
self._content_ready = True
|
|
63
|
+
self.call_after_refresh(self.update_preferred_size)
|
|
64
|
+
|
|
65
|
+
def reset_preferred_size(self) -> None:
|
|
66
|
+
"""Forget the prior measurement when a popup changes content purpose."""
|
|
67
|
+
self._preferred_size = None
|
|
68
|
+
self._applied_size = None
|
|
69
|
+
self._content_ready = False
|
|
70
|
+
self.remove_class("stable")
|
|
71
|
+
self.styles.width = "auto"
|
|
72
|
+
self.styles.height = "auto"
|
|
73
|
+
|
|
74
|
+
def on_resize(self, event: events.Resize) -> None:
|
|
75
|
+
del event
|
|
76
|
+
self.call_after_refresh(self.update_preferred_size)
|
|
77
|
+
|
|
78
|
+
def update_preferred_size(self) -> None:
|
|
79
|
+
"""Capture an unclamped outer size once, then clamp it to the current screen."""
|
|
80
|
+
if not self.is_mounted or not widget_is_attached(self):
|
|
81
|
+
return
|
|
82
|
+
viewport = self.screen.size
|
|
83
|
+
if self._preferred_size is None:
|
|
84
|
+
self._remember_preferred_size()
|
|
85
|
+
if self._preferred_size is not None:
|
|
86
|
+
self._apply_preferred_size(viewport)
|
|
87
|
+
|
|
88
|
+
def _remember_preferred_size(self) -> None:
|
|
89
|
+
if not self._content_ready:
|
|
90
|
+
return
|
|
91
|
+
height = self.styles.height
|
|
92
|
+
children = tuple(self.children)
|
|
93
|
+
viewport = self.screen.size
|
|
94
|
+
natural_width = (
|
|
95
|
+
max(
|
|
96
|
+
(child.virtual_size.width + child.styles.margin.width for child in children),
|
|
97
|
+
default=0,
|
|
98
|
+
)
|
|
99
|
+
+ self.styles.gutter.width
|
|
100
|
+
)
|
|
101
|
+
max_width = self.styles.max_width
|
|
102
|
+
max_width_cells = max_width.cells if max_width is not None else None
|
|
103
|
+
preferred_width = max(
|
|
104
|
+
self.outer_size.width,
|
|
105
|
+
max_width_cells if max_width_cells is not None else natural_width,
|
|
106
|
+
)
|
|
107
|
+
relative_height = height is not None and height.unit in {
|
|
108
|
+
Unit.PERCENT,
|
|
109
|
+
Unit.HEIGHT,
|
|
110
|
+
Unit.VIEW_HEIGHT,
|
|
111
|
+
}
|
|
112
|
+
height_clamped = self.outer_size.height >= viewport.height - self.styles.margin.height
|
|
113
|
+
if relative_height and not height_clamped:
|
|
114
|
+
natural_height = self.outer_size.height
|
|
115
|
+
elif relative_height:
|
|
116
|
+
natural_height = sum(_preferred_child_height(child) for child in children)
|
|
117
|
+
natural_height += self.styles.gutter.height
|
|
118
|
+
else:
|
|
119
|
+
content_width = max(0, preferred_width - self.styles.gutter.width)
|
|
120
|
+
natural_height = sum(
|
|
121
|
+
_preferred_child_height_at_width(child, viewport, content_width)
|
|
122
|
+
for child in children
|
|
123
|
+
)
|
|
124
|
+
natural_height += self.styles.gutter.height
|
|
125
|
+
preferred_height = (
|
|
126
|
+
natural_height
|
|
127
|
+
if relative_height and height_clamped
|
|
128
|
+
else max(self.outer_size.height, natural_height)
|
|
129
|
+
)
|
|
130
|
+
self._preferred_size = Size(
|
|
131
|
+
preferred_width,
|
|
132
|
+
preferred_height,
|
|
133
|
+
)
|
|
134
|
+
self.add_class("stable")
|
|
135
|
+
|
|
136
|
+
def _apply_preferred_size(self, viewport: Size) -> None:
|
|
137
|
+
if self._preferred_size is None:
|
|
138
|
+
return
|
|
139
|
+
margin = self.styles.margin
|
|
140
|
+
minimum_height = sum(_minimum_child_height(child) for child in self.children)
|
|
141
|
+
minimum_height += self.styles.gutter.height
|
|
142
|
+
size = Size(
|
|
143
|
+
max(1, min(self._preferred_size.width, viewport.width - margin.width)),
|
|
144
|
+
max(
|
|
145
|
+
1,
|
|
146
|
+
min(
|
|
147
|
+
max(self._preferred_size.height, minimum_height),
|
|
148
|
+
viewport.height - margin.height,
|
|
149
|
+
),
|
|
150
|
+
),
|
|
151
|
+
)
|
|
152
|
+
if size != self._applied_size:
|
|
153
|
+
self.styles.width = size.width
|
|
154
|
+
self.styles.height = size.height
|
|
155
|
+
self._applied_size = size
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _preferred_child_height(child: Widget) -> int:
|
|
159
|
+
if not child.display:
|
|
160
|
+
height = child.styles.height
|
|
161
|
+
fixed_height = height.cells if height is not None else None
|
|
162
|
+
return (fixed_height or 0) + child.styles.margin.height
|
|
163
|
+
height = child.styles.height
|
|
164
|
+
fixed_height = height.cells if height is not None else None
|
|
165
|
+
return (
|
|
166
|
+
max(child.outer_size.height, child.virtual_size.height, fixed_height or 0)
|
|
167
|
+
+ child.styles.margin.height
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _preferred_child_height_at_width(child: Widget, viewport: Size, width: int) -> int:
|
|
172
|
+
if not child.display:
|
|
173
|
+
return _preferred_child_height(child)
|
|
174
|
+
content_width = max(0, width - child.styles.gutter.width)
|
|
175
|
+
content_height = child.get_content_height(Size(content_width, 0), viewport, content_width)
|
|
176
|
+
height = child.styles.height
|
|
177
|
+
fixed_height = height.cells if height is not None else None
|
|
178
|
+
return (
|
|
179
|
+
max(
|
|
180
|
+
child.outer_size.height,
|
|
181
|
+
fixed_height or 0,
|
|
182
|
+
content_height + child.styles.gutter.height,
|
|
183
|
+
)
|
|
184
|
+
+ child.styles.margin.height
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _minimum_child_height(child: Widget) -> int:
|
|
189
|
+
if not child.display:
|
|
190
|
+
return 0
|
|
191
|
+
height = child.styles.height
|
|
192
|
+
fixed_height = height.cells if height is not None else None
|
|
193
|
+
minimum_height = child.styles.min_height
|
|
194
|
+
minimum_height_cells = minimum_height.cells if minimum_height is not None else None
|
|
195
|
+
flexible_height = height is not None and height.unit is Unit.FRACTION
|
|
196
|
+
outer_height = 0 if flexible_height else child.outer_size.height
|
|
197
|
+
return (
|
|
198
|
+
max(outer_height, fixed_height or 0, minimum_height_cells or 0) + child.styles.margin.height
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
POPUP_BUTTON_CSS = """
|
|
203
|
+
.popup-frame Button.-style-default {
|
|
204
|
+
color: $primary-text; background: $surface; border: solid $neutral-border;
|
|
205
|
+
border-top: solid $neutral-border; border-bottom: solid $neutral-border;
|
|
206
|
+
text-style: none;
|
|
207
|
+
}
|
|
208
|
+
.popup-frame Button.-style-default.-primary {
|
|
209
|
+
border: solid $neutral-border;
|
|
210
|
+
border-top: solid $neutral-border; border-bottom: solid $neutral-border;
|
|
211
|
+
}
|
|
212
|
+
.popup-frame Button.-style-default:focus,
|
|
213
|
+
.popup-frame Button.-style-default.-primary:focus {
|
|
214
|
+
color: $primary-text; background: $selected-surface;
|
|
215
|
+
border: solid $focus; border-top: solid $focus; border-bottom: solid $focus;
|
|
216
|
+
text-style: reverse;
|
|
217
|
+
}
|
|
218
|
+
.popup-frame Button.-style-default:disabled,
|
|
219
|
+
.popup-frame Button.-style-default.-primary:disabled {
|
|
220
|
+
color: $secondary-text; background: $surface;
|
|
221
|
+
border: solid $neutral-border;
|
|
222
|
+
border-top: solid $neutral-border; border-bottom: solid $neutral-border;
|
|
223
|
+
text-opacity: 76%;
|
|
224
|
+
}
|
|
225
|
+
"""
|
|
226
|
+
|
|
227
|
+
POPUP_CONTROL_CSS = """
|
|
228
|
+
.popup-frame Button:focus {
|
|
229
|
+
color: $primary-text; background: $selected-surface; text-style: reverse;
|
|
230
|
+
}
|
|
231
|
+
.popup-frame Button:disabled { color: $secondary-text; }
|
|
232
|
+
.popup-frame SelectCurrent {
|
|
233
|
+
border: solid $neutral-border; padding: 0 1; background: $detail-surface;
|
|
234
|
+
}
|
|
235
|
+
.popup-frame Select:focus > SelectCurrent,
|
|
236
|
+
.popup-frame Select.-expanded > SelectCurrent {
|
|
237
|
+
border: solid $focus; background: $selected-surface;
|
|
238
|
+
}
|
|
239
|
+
.popup-frame SelectOverlay {
|
|
240
|
+
border: solid $neutral-border; scrollbar-size-vertical: $scrollbar-width;
|
|
241
|
+
}
|
|
242
|
+
.popup-frame SelectOverlay:focus { border: solid $focus; }
|
|
243
|
+
.popup-frame TextArea { scrollbar-size-vertical: $scrollbar-width; }
|
|
244
|
+
.popup-frame TextArea.popup-edit {
|
|
245
|
+
height: auto; min-height: 2; max-height: 6; padding: 0;
|
|
246
|
+
border: none; border-bottom: solid $neutral-border; background: $surface;
|
|
247
|
+
}
|
|
248
|
+
.popup-frame TextArea.popup-edit > .text-area--cursor-line { background: transparent; }
|
|
249
|
+
.popup-frame TextArea.popup-edit:focus {
|
|
250
|
+
border: none; border-bottom: solid $focus; background: $selected-surface;
|
|
251
|
+
}
|
|
252
|
+
.popup-frame Select:disabled > SelectCurrent { color: $secondary-text; }
|
|
253
|
+
.popup-frame Button.popup-close-control {
|
|
254
|
+
width: auto; min-width: 5; height: 1; margin: 0; padding: 0 1;
|
|
255
|
+
border: none; border-top: none; border-bottom: none;
|
|
256
|
+
background: $surface; color: $primary-text; text-style: none;
|
|
257
|
+
}
|
|
258
|
+
.popup-frame Button.popup-close-control.-style-default:focus {
|
|
259
|
+
border: none; border-top: none; border-right: none; border-bottom: none; border-left: none;
|
|
260
|
+
background: $selected-surface; color: $primary-text; text-style: reverse;
|
|
261
|
+
}
|
|
262
|
+
"""
|
|
263
|
+
|
|
264
|
+
POPUP_COMPACT_CONTROL_CSS = """
|
|
265
|
+
.palette-row { layout: horizontal; height: auto; margin-bottom: 0; }
|
|
266
|
+
.palette-row Label { width: 9; height: 1; color: $secondary-text; text-style: none; }
|
|
267
|
+
.palette-row Input, .palette-row Select { width: 1fr; }
|
|
268
|
+
.palette-row Input {
|
|
269
|
+
height: 1; border: none; padding: 0 1; background: $surface;
|
|
270
|
+
}
|
|
271
|
+
.palette-row Input:focus {
|
|
272
|
+
border: none; background: $selected-surface; text-style: none;
|
|
273
|
+
}
|
|
274
|
+
.popup-frame .palette-row SelectCurrent {
|
|
275
|
+
height: 1; border: none; padding: 0 1; background: $detail-surface;
|
|
276
|
+
}
|
|
277
|
+
.popup-frame .palette-row Select:focus > SelectCurrent,
|
|
278
|
+
.popup-frame .palette-row Select.-expanded > SelectCurrent {
|
|
279
|
+
border: none; background: $selected-surface;
|
|
280
|
+
}
|
|
281
|
+
"""
|
|
282
|
+
|
|
283
|
+
POPUP_CSS = (
|
|
284
|
+
"""
|
|
285
|
+
.popup-frame {
|
|
286
|
+
width: 100%; max-width: 64; height: auto; max-height: 100%; margin: 1 2;
|
|
287
|
+
padding: 1 2; border: solid $steel-blue; background: $surface; color: $primary-text;
|
|
288
|
+
}
|
|
289
|
+
.popup-header { width: 100%; height: 1; }
|
|
290
|
+
.popup-title { width: 1fr; height: 1; color: $primary-text; text-style: bold; }
|
|
291
|
+
.popup-frame Rule.popup-rule { width: 100%; height: 1; margin: 0; color: $neutral-border; }
|
|
292
|
+
.popup-body {
|
|
293
|
+
height: auto; max-height: 1fr; min-height: 0; padding: 0;
|
|
294
|
+
}
|
|
295
|
+
.popup-frame.stable > .popup-body { height: 1fr; }
|
|
296
|
+
.popup-body, .popup-frame #task-detail-body {
|
|
297
|
+
overflow-x: hidden; overflow-y: auto;
|
|
298
|
+
scrollbar-size-horizontal: 0; scrollbar-size-vertical: $scrollbar-width;
|
|
299
|
+
}
|
|
300
|
+
.popup-secondary { color: $secondary-text; }
|
|
301
|
+
"""
|
|
302
|
+
+ POPUP_BUTTON_CSS
|
|
303
|
+
+ POPUP_CONTROL_CSS
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def prepare_popup(screen: Widget) -> None:
|
|
308
|
+
screen.styles.align_horizontal = "center"
|
|
309
|
+
screen.styles.align_vertical = "middle"
|
|
310
|
+
screen.styles.background = "transparent"
|
|
311
|
+
screen.styles.padding = 0
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""The status line of an agent pane whose turn or dispatch waits on a quota block."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from shikiru.domain.quota_waits import QUOTA_BANNER_SEPARATOR, quota_reset_clock
|
|
8
|
+
from shikiru.ui.glyphs import GLYPH_QUOTA
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from datetime import datetime
|
|
12
|
+
|
|
13
|
+
from shikiru.domain.quota_waits import QuotaAction
|
|
14
|
+
|
|
15
|
+
QUOTA_BLOCKED_STATUS = "Quota blocked"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def quota_status_text(action: QuotaAction | None, at: datetime | None, now: datetime) -> str:
|
|
19
|
+
"""Return the pane status line; the engine's next `action` and its time show only when known.
|
|
20
|
+
|
|
21
|
+
`at` and `now` are rendered in the zone they carry, so callers pass the viewer's local time.
|
|
22
|
+
"""
|
|
23
|
+
text = f"{GLYPH_QUOTA} {QUOTA_BLOCKED_STATUS}"
|
|
24
|
+
if action is None or at is None:
|
|
25
|
+
return text
|
|
26
|
+
return f"{text}{QUOTA_BANNER_SEPARATOR}{action.value} {quota_reset_clock(at, now)}"
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""Paged window over the most recent terminal tasks."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass(frozen=True, slots=True)
|
|
9
|
+
class RecentWindow:
|
|
10
|
+
"""A fixed-size window at `offset` over `total` most recent tasks, newest first."""
|
|
11
|
+
|
|
12
|
+
offset: int
|
|
13
|
+
size: int
|
|
14
|
+
total: int
|
|
15
|
+
|
|
16
|
+
@property
|
|
17
|
+
def shown(self) -> int:
|
|
18
|
+
"""Return how many tasks the window holds; the oldest page may be short."""
|
|
19
|
+
return max(min(self.size, self.total - self.offset), 0)
|
|
20
|
+
|
|
21
|
+
@property
|
|
22
|
+
def paged(self) -> bool:
|
|
23
|
+
"""Return whether more tasks exist than one window holds."""
|
|
24
|
+
return self.total > self.size
|
|
25
|
+
|
|
26
|
+
@property
|
|
27
|
+
def has_later(self) -> bool:
|
|
28
|
+
"""Return whether a newer page exists."""
|
|
29
|
+
return self.offset > 0
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
def has_earlier(self) -> bool:
|
|
33
|
+
"""Return whether an older page exists."""
|
|
34
|
+
return self.offset + self.size < self.total
|
|
35
|
+
|
|
36
|
+
def at_page_of(self, position: int) -> RecentWindow:
|
|
37
|
+
"""Return the window whose page holds the task at absolute `position`."""
|
|
38
|
+
return RecentWindow(position // self.size * self.size, self.size, self.total)
|
|
39
|
+
|
|
40
|
+
def clamped(self) -> RecentWindow:
|
|
41
|
+
"""Return the window moved to the last page when `offset` lies past the end."""
|
|
42
|
+
return self.at_page_of(max(min(self.offset, self.total - 1), 0))
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass(frozen=True, slots=True)
|
|
46
|
+
class StepTarget:
|
|
47
|
+
"""Where Up/Down lands: a visible row, or a Recent task on another page."""
|
|
48
|
+
|
|
49
|
+
visible_index: int | None = None
|
|
50
|
+
recent_position: int | None = None
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def step_selection(
|
|
54
|
+
active_count: int,
|
|
55
|
+
window: RecentWindow,
|
|
56
|
+
current: int | None,
|
|
57
|
+
step: int,
|
|
58
|
+
) -> StepTarget:
|
|
59
|
+
"""Step one row through active rows then the visible Recent rows, paging at window edges.
|
|
60
|
+
|
|
61
|
+
`current` indexes that visible order, or is `None` when the selection is not visible.
|
|
62
|
+
Entering Recent from the active rows never pages; leaving the window pages by one task.
|
|
63
|
+
"""
|
|
64
|
+
count = active_count + window.shown
|
|
65
|
+
if current is None:
|
|
66
|
+
return StepTarget(visible_index=0 if step > 0 else count - 1)
|
|
67
|
+
if window.shown:
|
|
68
|
+
if current == count - 1 and step > 0:
|
|
69
|
+
if window.has_earlier:
|
|
70
|
+
return StepTarget(recent_position=window.offset + window.shown)
|
|
71
|
+
if active_count == 0:
|
|
72
|
+
return StepTarget(recent_position=0)
|
|
73
|
+
if current == active_count and step < 0:
|
|
74
|
+
if window.has_later:
|
|
75
|
+
return StepTarget(recent_position=window.offset - 1)
|
|
76
|
+
if active_count == 0:
|
|
77
|
+
return StepTarget(recent_position=window.total - 1)
|
|
78
|
+
return StepTarget(visible_index=(current + step) % count)
|
shikiru/ui/screen.py
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, Protocol, runtime_checkable
|
|
4
|
+
|
|
5
|
+
from textual.dom import NoScreen
|
|
6
|
+
from textual.screen import Screen
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from textual.widget import Widget
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@runtime_checkable
|
|
13
|
+
class ConditionalTabStop(Protocol):
|
|
14
|
+
"""A widget that Tab and Shift+Tab visit only while `is_tab_stop()` holds."""
|
|
15
|
+
|
|
16
|
+
def is_tab_stop(self) -> bool: ...
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class TabStopScreen(Screen[object]):
|
|
20
|
+
"""Keep widgets that are not stops now out of the Tab order, except the focused widget."""
|
|
21
|
+
|
|
22
|
+
@property
|
|
23
|
+
def focus_chain(self) -> list[Widget]:
|
|
24
|
+
focused = self.focused
|
|
25
|
+
return [
|
|
26
|
+
widget
|
|
27
|
+
for widget in super().focus_chain
|
|
28
|
+
if widget is focused
|
|
29
|
+
or not isinstance(widget, ConditionalTabStop)
|
|
30
|
+
or widget.is_tab_stop()
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def is_first_child(widget: Widget) -> bool:
|
|
35
|
+
return widget.parent is not None and widget.parent.children[0] is widget
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def widget_is_attached(widget: Widget) -> bool:
|
|
39
|
+
return _screen_for(widget) is not None
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def widget_is_on_screen(widget: Widget, screen: object) -> bool:
|
|
43
|
+
return _screen_for(widget) is screen
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def widget_is_current(widget: Widget) -> bool:
|
|
47
|
+
screen = _screen_for(widget)
|
|
48
|
+
return screen is not None and screen.is_current
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _screen_for(widget: Widget) -> Screen[object] | None:
|
|
52
|
+
if not widget.is_attached:
|
|
53
|
+
return None
|
|
54
|
+
try:
|
|
55
|
+
return widget.screen
|
|
56
|
+
except NoScreen:
|
|
57
|
+
return None
|
shikiru/ui/section.py
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
"""Rule-and-heading sections with labelled, separator-wrapped rows shared by the panels."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, replace
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
from rich.cells import cell_len
|
|
9
|
+
from rich.console import Group, RenderableType
|
|
10
|
+
from rich.rule import Rule
|
|
11
|
+
from rich.table import Table
|
|
12
|
+
from rich.text import Text
|
|
13
|
+
|
|
14
|
+
from shikiru.ui.age import AGE_NOW
|
|
15
|
+
from shikiru.ui.tokens import (
|
|
16
|
+
AMBER,
|
|
17
|
+
NEUTRAL_BORDER_COLOR,
|
|
18
|
+
PRIMARY_TEXT_COLOR,
|
|
19
|
+
SECONDARY_TEXT_COLOR,
|
|
20
|
+
STEEL_BLUE,
|
|
21
|
+
STYLE_BOLD,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
if TYPE_CHECKING:
|
|
25
|
+
from rich.console import Console, ConsoleOptions, RenderResult
|
|
26
|
+
|
|
27
|
+
GROUP_SEPARATOR = " · "
|
|
28
|
+
LABEL_GAP = 1
|
|
29
|
+
MIN_VALUE_WIDTH = 24
|
|
30
|
+
VALUE_OVERFLOW = "fold"
|
|
31
|
+
HEADING_MIN_GAP = 1
|
|
32
|
+
STACKED_BELOW_WIDTH = 32
|
|
33
|
+
OBSERVED_WORD = "observed"
|
|
34
|
+
AGO_WORD = "ago"
|
|
35
|
+
UNKNOWN_AGE = "\N{EN DASH}"
|
|
36
|
+
STALE_WORD = "stale"
|
|
37
|
+
FAILED_WORD = "failed"
|
|
38
|
+
UNAVAILABLE_WORD = "unavailable"
|
|
39
|
+
BOLD_STEEL = f"{STYLE_BOLD} {STEEL_BLUE}"
|
|
40
|
+
BOLD_PRIMARY = f"{STYLE_BOLD} {PRIMARY_TEXT_COLOR}"
|
|
41
|
+
|
|
42
|
+
Row = tuple[Text, tuple[Text, ...]]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def grey(text: str) -> Text:
|
|
46
|
+
"""Return `text` in the secondary grey."""
|
|
47
|
+
return Text(text, style=SECONDARY_TEXT_COLOR)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def row_name(text: str) -> Text:
|
|
51
|
+
"""Return `text` as a bold white row name."""
|
|
52
|
+
return Text(text, style=BOLD_PRIMARY)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def number(value: str) -> Text:
|
|
56
|
+
"""Return `value` as a bold cyan figure."""
|
|
57
|
+
return Text(value, style=BOLD_STEEL)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def key_label(key: str, label: str) -> Text:
|
|
61
|
+
"""Return `key` in the bold cyan key style followed by its grey `label`."""
|
|
62
|
+
return Text.assemble((key, BOLD_STEEL), grey(f" {label}"))
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def observed_text(observed_ago: str) -> str:
|
|
66
|
+
"""Return `observed <age> ago`, or `observed now`."""
|
|
67
|
+
suffix = "" if observed_ago == AGE_NOW else f" {AGO_WORD}"
|
|
68
|
+
return f"{OBSERVED_WORD} {observed_ago}{suffix}"
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def observation(observed_ago: str, *, stale: bool) -> Text | None:
|
|
72
|
+
"""Return the amber `stale · observed <age> ago` heading note, or nothing unless `stale`."""
|
|
73
|
+
if not stale:
|
|
74
|
+
return None
|
|
75
|
+
return Text(f"{STALE_WORD}{GROUP_SEPARATOR}{observed_text(observed_ago)}", style=AMBER)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def state_line(word: str, colour: str, error: str | None) -> Text:
|
|
79
|
+
"""Return a sampling state `word` in `colour`, with any recorded `error` after it."""
|
|
80
|
+
detail = f"{GROUP_SEPARATOR}{error}" if error else ""
|
|
81
|
+
return Text(f"{word}{detail}", style=colour)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def wrap_groups(
|
|
85
|
+
groups: tuple[Text, ...], width: int | None, separator: str = GROUP_SEPARATOR
|
|
86
|
+
) -> Text:
|
|
87
|
+
"""Join `groups` with grey `separator`s, breaking lines only between groups."""
|
|
88
|
+
result = Text()
|
|
89
|
+
line_width = 0
|
|
90
|
+
for group in groups:
|
|
91
|
+
gap = separator if line_width else ""
|
|
92
|
+
group_width = cell_len(group.plain)
|
|
93
|
+
exceeds_width = line_width + cell_len(gap) + group_width > width if width else False
|
|
94
|
+
if width is not None and line_width and exceeds_width:
|
|
95
|
+
result.append("\n")
|
|
96
|
+
line_width = 0
|
|
97
|
+
gap = ""
|
|
98
|
+
result.append(gap, style=SECONDARY_TEXT_COLOR)
|
|
99
|
+
result.append(group)
|
|
100
|
+
line_width += cell_len(gap) + group_width
|
|
101
|
+
return result
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class WrappedGroups:
|
|
105
|
+
"""Renderable that wraps its groups with `separator` at the width it is rendered in."""
|
|
106
|
+
|
|
107
|
+
def __init__(self, groups: tuple[Text, ...], separator: str = GROUP_SEPARATOR) -> None:
|
|
108
|
+
self._groups = groups
|
|
109
|
+
self._separator = separator
|
|
110
|
+
|
|
111
|
+
def __rich_console__(self, console: Console, options: ConsoleOptions) -> RenderResult:
|
|
112
|
+
del console
|
|
113
|
+
yield wrap_groups(self._groups, options.max_width, self._separator)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def labelled_rows(
|
|
117
|
+
rows: list[Row], width: int, label_width: int, stack_below: int = STACKED_BELOW_WIDTH
|
|
118
|
+
) -> RenderableType:
|
|
119
|
+
"""Lay rows out as a label column and wrapped values, stacked below `stack_below` cells."""
|
|
120
|
+
value_width = width - label_width - LABEL_GAP
|
|
121
|
+
value_indent = labelled_value_indent(width, label_width, stack_below)
|
|
122
|
+
if value_indent == 0 and width < stack_below and label_width <= width - LABEL_GAP:
|
|
123
|
+
return Group(*_stacked_lines(rows, width))
|
|
124
|
+
if value_indent == 0:
|
|
125
|
+
grid = Table.grid()
|
|
126
|
+
grid.add_column(width=width, overflow=VALUE_OVERFLOW)
|
|
127
|
+
for label, groups in rows:
|
|
128
|
+
if label.plain:
|
|
129
|
+
grid.add_row(label)
|
|
130
|
+
grid.add_row(wrap_groups(groups, width))
|
|
131
|
+
return grid
|
|
132
|
+
grid = Table.grid()
|
|
133
|
+
grid.add_column(width=label_width + LABEL_GAP, no_wrap=True)
|
|
134
|
+
grid.add_column(width=value_width, overflow=VALUE_OVERFLOW)
|
|
135
|
+
for label, groups in rows:
|
|
136
|
+
grid.add_row(label, wrap_groups(groups, value_width))
|
|
137
|
+
return grid
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def labelled_value_indent(
|
|
141
|
+
width: int, label_width: int, stack_below: int = STACKED_BELOW_WIDTH
|
|
142
|
+
) -> int:
|
|
143
|
+
"""Return the value cell's indent when `labelled_rows` has a value column."""
|
|
144
|
+
value_width = width - label_width - LABEL_GAP
|
|
145
|
+
if value_width < MIN_VALUE_WIDTH:
|
|
146
|
+
return 0
|
|
147
|
+
if width < stack_below and label_width <= width - LABEL_GAP:
|
|
148
|
+
return 0
|
|
149
|
+
return label_width + LABEL_GAP
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _stacked_lines(rows: list[Row], width: int) -> list[Text]:
|
|
153
|
+
lines: list[Text] = []
|
|
154
|
+
for label, groups in rows:
|
|
155
|
+
if label.plain:
|
|
156
|
+
lines.append(label)
|
|
157
|
+
lines.append(wrap_groups(groups, width))
|
|
158
|
+
return lines
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
@dataclass(frozen=True)
|
|
162
|
+
class Section:
|
|
163
|
+
"""Render `body` below an optional rule and a heading, with an optional `trailing`."""
|
|
164
|
+
|
|
165
|
+
title: str
|
|
166
|
+
body: RenderableType
|
|
167
|
+
trailing: Text | None = None
|
|
168
|
+
ruled: bool = True
|
|
169
|
+
title_style: str = SECONDARY_TEXT_COLOR
|
|
170
|
+
|
|
171
|
+
def with_ruled(self, *, ruled: bool) -> Section:
|
|
172
|
+
"""Return this section drawn with or without its leading rule."""
|
|
173
|
+
return replace(self, ruled=ruled)
|
|
174
|
+
|
|
175
|
+
def __rich_console__(self, console: Console, options: ConsoleOptions) -> RenderResult:
|
|
176
|
+
"""Yield the rule when ruled, the heading and the body."""
|
|
177
|
+
del console
|
|
178
|
+
if self.ruled:
|
|
179
|
+
yield Rule(style=NEUTRAL_BORDER_COLOR)
|
|
180
|
+
yield self._heading(options.max_width)
|
|
181
|
+
yield self.body
|
|
182
|
+
|
|
183
|
+
def _heading(self, width: int) -> RenderableType:
|
|
184
|
+
title = Text()
|
|
185
|
+
title.append(self.title, style=self.title_style)
|
|
186
|
+
if self.trailing is None:
|
|
187
|
+
return title
|
|
188
|
+
gap = width - cell_len(title.plain) - cell_len(self.trailing.plain)
|
|
189
|
+
if gap < HEADING_MIN_GAP:
|
|
190
|
+
return Group(title, self.trailing)
|
|
191
|
+
title.append(" " * gap)
|
|
192
|
+
title.append(self.trailing)
|
|
193
|
+
return title
|