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.
Files changed (319) hide show
  1. shikiru/__init__.py +1 -0
  2. shikiru/__main__.py +8 -0
  3. shikiru/agents/__init__.py +3 -0
  4. shikiru/agents/contracts.py +590 -0
  5. shikiru/agents/registry.py +87 -0
  6. shikiru/agents/sdk/__init__.py +17 -0
  7. shikiru/agents/sdk/reset_time.py +137 -0
  8. shikiru/agents/sdk/turn.py +335 -0
  9. shikiru/agents/settings.py +151 -0
  10. shikiru/agents/testing/__init__.py +26 -0
  11. shikiru/agents/testing/contract_suite.py +653 -0
  12. shikiru/agents/testing/resume.py +15 -0
  13. shikiru/bootstrap.py +1277 -0
  14. shikiru/cli/__init__.py +1 -0
  15. shikiru/cli/app.py +770 -0
  16. shikiru/cli/context.py +63 -0
  17. shikiru/cli/render.py +1475 -0
  18. shikiru/cli/report.py +156 -0
  19. shikiru/cli/session_prompt.py +63 -0
  20. shikiru/cli/view.py +131 -0
  21. shikiru/clock.py +41 -0
  22. shikiru/config/__init__.py +8 -0
  23. shikiru/config/defaults.toml +131 -0
  24. shikiru/config/loader.py +178 -0
  25. shikiru/config/settings.py +392 -0
  26. shikiru/dashboard/__init__.py +3 -0
  27. shikiru/dashboard/actions_list.py +799 -0
  28. shikiru/dashboard/activity.py +150 -0
  29. shikiru/dashboard/app.py +1765 -0
  30. shikiru/dashboard/blocker_evidence.py +122 -0
  31. shikiru/dashboard/blocker_rows.py +84 -0
  32. shikiru/dashboard/briefing_view.py +871 -0
  33. shikiru/dashboard/contract.py +1086 -0
  34. shikiru/dashboard/covered.py +32 -0
  35. shikiru/dashboard/detail.py +1209 -0
  36. shikiru/dashboard/detail_fold.py +244 -0
  37. shikiru/dashboard/detail_hint.py +68 -0
  38. shikiru/dashboard/details_list_view.py +150 -0
  39. shikiru/dashboard/diagnostics.py +80 -0
  40. shikiru/dashboard/diagnostics_sections.py +186 -0
  41. shikiru/dashboard/evidence_table.py +123 -0
  42. shikiru/dashboard/extra_details.py +214 -0
  43. shikiru/dashboard/fault_guidance.py +216 -0
  44. shikiru/dashboard/fold_bodies.py +276 -0
  45. shikiru/dashboard/help_keys.py +166 -0
  46. shikiru/dashboard/help_screen.py +204 -0
  47. shikiru/dashboard/interaction.py +17 -0
  48. shikiru/dashboard/ladder_editor.py +544 -0
  49. shikiru/dashboard/live_settings.py +62 -0
  50. shikiru/dashboard/motion.py +368 -0
  51. shikiru/dashboard/orchestration_fields.py +201 -0
  52. shikiru/dashboard/palette.py +1606 -0
  53. shikiru/dashboard/palette_app.py +89 -0
  54. shikiru/dashboard/palette_drafts.py +33 -0
  55. shikiru/dashboard/resolution.py +573 -0
  56. shikiru/dashboard/resolution_view.py +302 -0
  57. shikiru/dashboard/row_list.py +256 -0
  58. shikiru/dashboard/section_rows.py +173 -0
  59. shikiru/dashboard/services.py +665 -0
  60. shikiru/dashboard/session_history.py +402 -0
  61. shikiru/dashboard/shikiru_init_ascii_art.txt +40 -0
  62. shikiru/dashboard/task_picker.py +358 -0
  63. shikiru/dashboard/tick.py +59 -0
  64. shikiru/dashboard/timeline_filter.py +129 -0
  65. shikiru/dashboard/timeline_section.py +385 -0
  66. shikiru/dashboard/usage_body.py +269 -0
  67. shikiru/dashboard/views.py +2314 -0
  68. shikiru/dashboard/wizard.py +1389 -0
  69. shikiru/domain/__init__.py +1 -0
  70. shikiru/domain/acceptance.py +130 -0
  71. shikiru/domain/activity.py +28 -0
  72. shikiru/domain/attempts.py +333 -0
  73. shikiru/domain/briefing.py +257 -0
  74. shikiru/domain/commands.py +219 -0
  75. shikiru/domain/decisions.py +231 -0
  76. shikiru/domain/docker.py +262 -0
  77. shikiru/domain/engine_uptime.py +87 -0
  78. shikiru/domain/escalation.py +194 -0
  79. shikiru/domain/escalation_evidence.py +186 -0
  80. shikiru/domain/escalation_panel.py +144 -0
  81. shikiru/domain/events.py +71 -0
  82. shikiru/domain/evidence_export.py +89 -0
  83. shikiru/domain/faults.py +86 -0
  84. shikiru/domain/feedback.py +180 -0
  85. shikiru/domain/feedback_rounds.py +172 -0
  86. shikiru/domain/fencing.py +31 -0
  87. shikiru/domain/ladder.py +102 -0
  88. shikiru/domain/ladder_text.py +44 -0
  89. shikiru/domain/levels.py +44 -0
  90. shikiru/domain/motion.py +199 -0
  91. shikiru/domain/packet_evidence.py +377 -0
  92. shikiru/domain/pushes.py +47 -0
  93. shikiru/domain/quota_blocks.py +44 -0
  94. shikiru/domain/quota_waits.py +219 -0
  95. shikiru/domain/recovery.py +194 -0
  96. shikiru/domain/reports.py +498 -0
  97. shikiru/domain/results.py +217 -0
  98. shikiru/domain/runs.py +85 -0
  99. shikiru/domain/scheduling.py +231 -0
  100. shikiru/domain/status_tasks.py +40 -0
  101. shikiru/domain/steps.py +325 -0
  102. shikiru/domain/tasks.py +174 -0
  103. shikiru/domain/timeline.py +208 -0
  104. shikiru/domain/timeline_phrases.py +271 -0
  105. shikiru/domain/versions.py +18 -0
  106. shikiru/domain/workflow.py +372 -0
  107. shikiru/engine/__init__.py +0 -0
  108. shikiru/engine/acceptance.py +90 -0
  109. shikiru/engine/agents.py +61 -0
  110. shikiru/engine/cosmetic_decorator.py +99 -0
  111. shikiru/engine/daemon.py +409 -0
  112. shikiru/engine/dependency.py +37 -0
  113. shikiru/engine/escalation_filing.py +110 -0
  114. shikiru/engine/escalations.py +401 -0
  115. shikiru/engine/events.py +285 -0
  116. shikiru/engine/evidence.py +81 -0
  117. shikiru/engine/fault_reporter.py +144 -0
  118. shikiru/engine/feedback.py +577 -0
  119. shikiru/engine/feedback_audit.py +80 -0
  120. shikiru/engine/feedback_intake.py +95 -0
  121. shikiru/engine/feedback_snapshot.py +148 -0
  122. shikiru/engine/flow.py +87 -0
  123. shikiru/engine/handlers/__init__.py +33 -0
  124. shikiru/engine/handlers/base.py +148 -0
  125. shikiru/engine/handlers/engine.py +24 -0
  126. shikiru/engine/handlers/escalations.py +174 -0
  127. shikiru/engine/handlers/operator.py +81 -0
  128. shikiru/engine/handlers/tasks.py +237 -0
  129. shikiru/engine/history.py +128 -0
  130. shikiru/engine/integration.py +1234 -0
  131. shikiru/engine/integration_turns.py +337 -0
  132. shikiru/engine/layout_guard.py +362 -0
  133. shikiru/engine/lifecycle.py +60 -0
  134. shikiru/engine/monitor.py +317 -0
  135. shikiru/engine/notification_delivery.py +124 -0
  136. shikiru/engine/notification_feed.py +57 -0
  137. shikiru/engine/notifier.py +91 -0
  138. shikiru/engine/pull_request_closing.py +117 -0
  139. shikiru/engine/push_service.py +177 -0
  140. shikiru/engine/quota_probe_threads.py +79 -0
  141. shikiru/engine/quota_reconciler.py +210 -0
  142. shikiru/engine/quota_schedule.py +24 -0
  143. shikiru/engine/quota_waits.py +102 -0
  144. shikiru/engine/reconcile.py +91 -0
  145. shikiru/engine/recovery.py +284 -0
  146. shikiru/engine/retention.py +102 -0
  147. shikiru/engine/root_preparation.py +86 -0
  148. shikiru/engine/scheduler.py +320 -0
  149. shikiru/engine/session_guard.py +46 -0
  150. shikiru/engine/sessions.py +95 -0
  151. shikiru/engine/signalled.py +75 -0
  152. shikiru/engine/step_runner.py +140 -0
  153. shikiru/engine/supervisor.py +101 -0
  154. shikiru/engine/takeover.py +223 -0
  155. shikiru/engine/task_facts.py +46 -0
  156. shikiru/engine/tick_guard.py +95 -0
  157. shikiru/engine/titles.py +297 -0
  158. shikiru/engine/transitions.py +79 -0
  159. shikiru/engine/turn_context.py +206 -0
  160. shikiru/engine/turn_execution.py +30 -0
  161. shikiru/engine/turn_loop.py +230 -0
  162. shikiru/engine/turn_processes.py +79 -0
  163. shikiru/engine/turns.py +457 -0
  164. shikiru/errors.py +144 -0
  165. shikiru/helper/__init__.py +3 -0
  166. shikiru/helper/helper.py +182 -0
  167. shikiru/helper/models.py +11 -0
  168. shikiru/mcp/__init__.py +1 -0
  169. shikiru/mcp/server.py +196 -0
  170. shikiru/operator/__init__.py +33 -0
  171. shikiru/operator/attachment.py +24 -0
  172. shikiru/operator/commands.py +319 -0
  173. shikiru/operator/detectors.py +374 -0
  174. shikiru/operator/doctor.py +351 -0
  175. shikiru/operator/events.py +57 -0
  176. shikiru/operator/evidence_export.py +498 -0
  177. shikiru/operator/evidence_writer.py +78 -0
  178. shikiru/operator/lifecycle.py +294 -0
  179. shikiru/operator/plugin_checks.py +194 -0
  180. shikiru/operator/probe.py +287 -0
  181. shikiru/operator/queries.py +1601 -0
  182. shikiru/operator/services.py +36 -0
  183. shikiru/operator/sessions.py +378 -0
  184. shikiru/operator/strip_views.py +162 -0
  185. shikiru/operator/timeline.py +416 -0
  186. shikiru/operator/toml_text.py +62 -0
  187. shikiru/palette.py +18 -0
  188. shikiru/paths.py +91 -0
  189. shikiru/plugins/__init__.py +0 -0
  190. shikiru/plugins/agents/__init__.py +1 -0
  191. shikiru/plugins/agents/claude/__init__.py +5 -0
  192. shikiru/plugins/agents/claude/agent.py +251 -0
  193. shikiru/plugins/agents/claude/context.py +116 -0
  194. shikiru/plugins/agents/claude/headless.py +92 -0
  195. shikiru/plugins/agents/claude/hook.py +57 -0
  196. shikiru/plugins/agents/claude/model_catalog.py +105 -0
  197. shikiru/plugins/agents/claude/options.py +62 -0
  198. shikiru/plugins/agents/claude/turn.py +1018 -0
  199. shikiru/plugins/agents/codex/__init__.py +5 -0
  200. shikiru/plugins/agents/codex/agent.py +486 -0
  201. shikiru/plugins/agents/codex/headless.py +166 -0
  202. shikiru/plugins/agents/codex/home.py +14 -0
  203. shikiru/plugins/agents/codex/model_catalog.py +128 -0
  204. shikiru/plugins/agents/codex/options.py +94 -0
  205. shikiru/plugins/agents/codex/quota.py +195 -0
  206. shikiru/plugins/agents/codex/sandbox.py +31 -0
  207. shikiru/plugins/agents/codex/sandbox_probe.py +97 -0
  208. shikiru/plugins/agents/codex/turn.py +767 -0
  209. shikiru/plugins/agents/codex/usage.py +106 -0
  210. shikiru/plugins/agents/shared_settings.py +62 -0
  211. shikiru/plugins/widgets/__init__.py +3 -0
  212. shikiru/plugins/widgets/dialogue.py +144 -0
  213. shikiru/plugins/widgets/disk.py +123 -0
  214. shikiru/plugins/widgets/docker.py +225 -0
  215. shikiru/plugins/widgets/escalations.py +402 -0
  216. shikiru/plugins/widgets/faults.py +204 -0
  217. shikiru/plugins/widgets/header.py +321 -0
  218. shikiru/plugins/widgets/queue.py +1140 -0
  219. shikiru/plugins/widgets/queue_hint.py +93 -0
  220. shikiru/plugins/widgets/resources.py +105 -0
  221. shikiru/plugins/widgets/strips.py +82 -0
  222. shikiru/plugins/widgets/usage.py +176 -0
  223. shikiru/private.py +94 -0
  224. shikiru/process.py +535 -0
  225. shikiru/prompts/__init__.py +3 -0
  226. shikiru/prompts/renderer.py +85 -0
  227. shikiru/prompts/templates/_base.md.j2 +17 -0
  228. shikiru/prompts/templates/_macros.md.j2 +61 -0
  229. shikiru/prompts/templates/conflicts.md.j2 +22 -0
  230. shikiru/prompts/templates/consult.md.j2 +5 -0
  231. shikiru/prompts/templates/continue.md.j2 +4 -0
  232. shikiru/prompts/templates/feedback_analysis.md.j2 +29 -0
  233. shikiru/prompts/templates/feedback_rework.md.j2 +33 -0
  234. shikiru/prompts/templates/feedback_verify.md.j2 +33 -0
  235. shikiru/prompts/templates/handoff.md.j2 +23 -0
  236. shikiru/prompts/templates/implement.md.j2 +13 -0
  237. shikiru/prompts/templates/plan.md.j2 +30 -0
  238. shikiru/prompts/templates/rework.md.j2 +13 -0
  239. shikiru/prompts/templates/triage.md.j2 +20 -0
  240. shikiru/prompts/templates/verify.md.j2 +29 -0
  241. shikiru/store/__init__.py +1 -0
  242. shikiru/store/commands.py +242 -0
  243. shikiru/store/database.py +297 -0
  244. shikiru/store/escalations.py +240 -0
  245. shikiru/store/events.py +213 -0
  246. shikiru/store/feedback.py +266 -0
  247. shikiru/store/migrations/001_initial.sql +63 -0
  248. shikiru/store/migrations/002_headless_turns.sql +31 -0
  249. shikiru/store/migrations/003_drop_command_handled_at.sql +1 -0
  250. shikiru/store/migrations/004_repo_base_ref.sql +3 -0
  251. shikiru/store/migrations/005_complexity_explicit.sql +3 -0
  252. shikiru/store/migrations/006_events_kind_index.sql +1 -0
  253. shikiru/store/migrations/007_notification_cursor.sql +1 -0
  254. shikiru/store/migrations/008_reported_usage.sql +1 -0
  255. shikiru/store/migrations/009_billing_declarations.sql +2 -0
  256. shikiru/store/migrations/010_canonical_quota_windows.sql +3 -0
  257. shikiru/store/migrations/012_drop_task_quota_evidence.sql +2 -0
  258. shikiru/store/migrations/013_push_targets.sql +7 -0
  259. shikiru/store/migrations/014_agent_block_time.sql +6 -0
  260. shikiru/store/migrations/015_repair_orphan_agent_blocks.sql +3 -0
  261. shikiru/store/migrations/016_feedback_rounds.sql +29 -0
  262. shikiru/store/migrations/017_feedback_round_comments.sql +5 -0
  263. shikiru/store/migrations/__init__.py +1 -0
  264. shikiru/store/notification_cursor.py +29 -0
  265. shikiru/store/pushes.py +117 -0
  266. shikiru/store/quotas.py +153 -0
  267. shikiru/store/reader.py +310 -0
  268. shikiru/store/retention.py +92 -0
  269. shikiru/store/runs.py +227 -0
  270. shikiru/store/tasks.py +949 -0
  271. shikiru/telemetry/__init__.py +1 -0
  272. shikiru/telemetry/disk.py +167 -0
  273. shikiru/telemetry/docker.py +164 -0
  274. shikiru/telemetry/resources.py +296 -0
  275. shikiru/tmux/__init__.py +3 -0
  276. shikiru/tmux/client.py +637 -0
  277. shikiru/tmux/decorator.py +225 -0
  278. shikiru/tmux/layout.py +200 -0
  279. shikiru/tmux/session.py +381 -0
  280. shikiru/ui/__init__.py +9 -0
  281. shikiru/ui/age.py +45 -0
  282. shikiru/ui/bubbles.py +41 -0
  283. shikiru/ui/compact.py +1209 -0
  284. shikiru/ui/console.py +49 -0
  285. shikiru/ui/fold.py +50 -0
  286. shikiru/ui/gated_static.py +117 -0
  287. shikiru/ui/gauge.py +40 -0
  288. shikiru/ui/glyphs.py +109 -0
  289. shikiru/ui/keys.py +57 -0
  290. shikiru/ui/live_interval.py +42 -0
  291. shikiru/ui/popup.py +311 -0
  292. shikiru/ui/quota_status.py +26 -0
  293. shikiru/ui/recent_window.py +78 -0
  294. shikiru/ui/screen.py +57 -0
  295. shikiru/ui/section.py +193 -0
  296. shikiru/ui/session.py +65 -0
  297. shikiru/ui/size.py +17 -0
  298. shikiru/ui/splash.py +42 -0
  299. shikiru/ui/status.py +47 -0
  300. shikiru/ui/status_strip.py +149 -0
  301. shikiru/ui/stream.py +152 -0
  302. shikiru/ui/strip.py +332 -0
  303. shikiru/ui/theme.py +83 -0
  304. shikiru/ui/tokens.py +51 -0
  305. shikiru/view_log.py +126 -0
  306. shikiru/workspace/__init__.py +3 -0
  307. shikiru/workspace/base_refs.py +160 -0
  308. shikiru/workspace/caches.py +136 -0
  309. shikiru/workspace/facts.py +118 -0
  310. shikiru/workspace/git.py +601 -0
  311. shikiru/workspace/github.py +744 -0
  312. shikiru/workspace/messages.py +107 -0
  313. shikiru/workspace/reports.py +145 -0
  314. shikiru/workspace/roots.py +180 -0
  315. shikiru_cli-0.2.0.dist-info/METADATA +428 -0
  316. shikiru_cli-0.2.0.dist-info/RECORD +319 -0
  317. shikiru_cli-0.2.0.dist-info/WHEEL +4 -0
  318. shikiru_cli-0.2.0.dist-info/entry_points.txt +18 -0
  319. shikiru_cli-0.2.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,204 @@
1
+ """The Help popup: one tab per context, opened on the context it was asked from."""
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.binding import Binding
11
+ from textual.containers import Horizontal, VerticalScroll
12
+ from textual.screen import ModalScreen
13
+ from textual.widgets import Button, Label, Rule, Static
14
+
15
+ from shikiru.dashboard.help_keys import HelpAvailability, HelpGroup, HelpTab, help_groups
16
+ from shikiru.ui.keys import key_hint
17
+ from shikiru.ui.popup import (
18
+ POPUP_BODY_CLASS,
19
+ POPUP_CLOSE_CLASS,
20
+ POPUP_CSS,
21
+ POPUP_FRAME_CLASS,
22
+ POPUP_HEADER_CLASS,
23
+ POPUP_RULE_CLASS,
24
+ POPUP_TITLE_CLASS,
25
+ PopupFrame,
26
+ popup_close_label,
27
+ prepare_popup,
28
+ )
29
+ from shikiru.ui.section import BOLD_STEEL, Section, grey, labelled_rows
30
+ from shikiru.ui.tokens import STEEL_BLUE
31
+
32
+ if TYPE_CHECKING:
33
+ from rich.console import Console, ConsoleOptions, RenderResult
34
+ from textual import events
35
+ from textual.app import ComposeResult
36
+ from textual.binding import BindingType
37
+
38
+ HELP_CLOSE_BUTTON = "workspace-help-close"
39
+ HELP_CONTEXT = "workspace-help-context"
40
+ HELP_TABS = "workspace-help-tabs"
41
+ HELP_HINT = "workspace-help-hint"
42
+ HELP_TITLE = "Help"
43
+ HELP_SCROLL = "workspace-help-scroll"
44
+ HELP_COPY = "workspace-help-copy"
45
+ HELP_FRAME = "workspace-help-frame"
46
+ COMPACT_MAX_COLUMNS = 48
47
+ COMPACT_MAX_ROWS = 20
48
+ TINY_MAX_ROWS = 10
49
+ _SWITCH_ARROWS = "\N{LEFTWARDS ARROW} \N{RIGHTWARDS ARROW}"
50
+ _SWITCH_LABEL = " switch section"
51
+ TAB_CLASS = "help-tab"
52
+ TAB_ACTIVE_CLASS = "-active"
53
+ SWITCH_ACTION = "switch"
54
+ SELECT_ACTION = "select_tab"
55
+ _TABS = tuple(HelpTab)
56
+
57
+
58
+ @dataclass(frozen=True)
59
+ class HelpContext:
60
+ """What Help was opened from: its tab and the Home parts that exist."""
61
+
62
+ opening: HelpTab
63
+ availability: HelpAvailability
64
+
65
+
66
+ class _TabLabel(Static):
67
+ def __init__(self, index: int, tab: HelpTab) -> None:
68
+ super().__init__(tab.value, classes=TAB_CLASS, markup=False)
69
+ self._index = index
70
+
71
+ async def on_click(self) -> None:
72
+ await self.run_action(f"screen.{SELECT_ACTION}({self._index})")
73
+
74
+
75
+ class _KeyTable:
76
+ def __init__(self, groups: tuple[HelpGroup, ...]) -> None:
77
+ self._groups = groups
78
+
79
+ def __rich_console__(self, console: Console, options: ConsoleOptions) -> RenderResult:
80
+ del console
81
+ for index, group in enumerate(self._groups):
82
+ if index:
83
+ yield Text("")
84
+ label_width = max(cell_len(row.keys) for row in group.rows)
85
+ rows = [
86
+ (Text(row.keys, style=BOLD_STEEL), (key_hint(row.action),)) for row in group.rows
87
+ ]
88
+ body = labelled_rows(rows, options.max_width, label_width)
89
+ yield Section(group.title, body, ruled=False, title_style=STEEL_BLUE)
90
+
91
+
92
+ class _SectionHeightTable:
93
+ def __init__(self, active: _KeyTable, sections: tuple[_KeyTable, ...]) -> None:
94
+ self._active = active
95
+ self._sections = sections
96
+
97
+ def __rich_console__(self, console: Console, options: ConsoleOptions) -> RenderResult:
98
+ heights = [
99
+ len(console.render_lines(section, options, pad=False)) for section in self._sections
100
+ ]
101
+ shown = len(console.render_lines(self._active, options, pad=False))
102
+ yield self._active
103
+ for _ in range(max(heights) - shown):
104
+ yield Text("")
105
+
106
+
107
+ class WorkspaceHelpScreen(ModalScreen[None]):
108
+ """Help with a tab per context; Left and Right switch tabs and the body scrolls."""
109
+
110
+ BINDINGS: ClassVar[list[BindingType]] = [
111
+ Binding("escape", "close", "Close"),
112
+ Binding("left", f"{SWITCH_ACTION}(-1)", "Previous tab", show=False, priority=True),
113
+ Binding("right", f"{SWITCH_ACTION}(1)", "Next tab", show=False, priority=True),
114
+ ]
115
+ DEFAULT_CSS = (
116
+ POPUP_CSS
117
+ + f"""
118
+ #{HELP_TABS} {{ height: 1; overflow: hidden; }}
119
+ #{HELP_HINT} {{ height: 1; margin-bottom: 1; }}
120
+ .{TAB_CLASS} {{ width: auto; margin: 0 2 0 0; color: $muted-text; }}
121
+ .{TAB_CLASS}.{TAB_ACTIVE_CLASS} {{
122
+ color: $steel-blue; text-style: bold; background: $selected-surface;
123
+ }}
124
+ #{HELP_SCROLL} {{ max-height: 1fr; min-height: 0; }}
125
+ #{HELP_COPY} {{ height: auto; color: $primary-text; }}
126
+ #{HELP_FRAME}.compact {{ margin: 0 1; padding: 0 1; }}
127
+ """
128
+ )
129
+
130
+ def __init__(self, context: HelpContext) -> None:
131
+ super().__init__()
132
+ prepare_popup(self)
133
+ self._help = context
134
+ self._tab = context.opening
135
+
136
+ def compose(self) -> ComposeResult:
137
+ """Render the header, the tab row and the key table of the opening tab."""
138
+ with PopupFrame(widget_id=HELP_FRAME, classes=POPUP_FRAME_CLASS):
139
+ with Horizontal(classes=POPUP_HEADER_CLASS):
140
+ yield Label(HELP_TITLE, id=HELP_CONTEXT, classes=POPUP_TITLE_CLASS)
141
+ yield Button(popup_close_label(), id=HELP_CLOSE_BUTTON, classes=POPUP_CLOSE_CLASS)
142
+ yield Rule(classes=POPUP_RULE_CLASS)
143
+ with Horizontal(id=HELP_TABS):
144
+ for index, tab in enumerate(_TABS):
145
+ yield _TabLabel(index, tab)
146
+ yield Static(self._hint(keys_only=False), id=HELP_HINT, markup=False)
147
+ with VerticalScroll(id=HELP_SCROLL, classes=POPUP_BODY_CLASS):
148
+ yield Static(self._table(), id=HELP_COPY, markup=False)
149
+
150
+ def _hint(self, *, keys_only: bool) -> Text:
151
+ hint = Text(_SWITCH_ARROWS, style=BOLD_STEEL)
152
+ if not keys_only:
153
+ hint.append_text(grey(_SWITCH_LABEL))
154
+ return hint
155
+
156
+ def on_mount(self) -> None:
157
+ self._apply_compact_layout(self.size.width, self.size.height)
158
+ self._paint_tabs()
159
+ self.set_focus(self.query_one(f"#{HELP_SCROLL}", VerticalScroll))
160
+ self.call_after_refresh(self.query_one(f"#{HELP_FRAME}", PopupFrame).content_ready)
161
+
162
+ def on_resize(self, event: events.Resize) -> None:
163
+ self._apply_compact_layout(event.size.width, event.size.height)
164
+ frame = self.query_one(f"#{HELP_FRAME}", PopupFrame)
165
+ frame.reset_preferred_size()
166
+ self.call_after_refresh(frame.content_ready)
167
+
168
+ def _apply_compact_layout(self, width: int, height: int) -> None:
169
+ frame = self.query_one(f"#{HELP_FRAME}", PopupFrame)
170
+ frame.set_class(width <= COMPACT_MAX_COLUMNS or height <= COMPACT_MAX_ROWS, "compact")
171
+ self.query_one(f"#{HELP_HINT}", Static).update(
172
+ self._hint(keys_only=height <= TINY_MAX_ROWS)
173
+ )
174
+
175
+ def _paint_tabs(self) -> None:
176
+ for tab, label in zip(_TABS, self.query(f".{TAB_CLASS}"), strict=True):
177
+ label.set_class(tab is self._tab, TAB_ACTIVE_CLASS)
178
+
179
+ def _table(self) -> _SectionHeightTable:
180
+ sections = tuple(_KeyTable(help_groups(tab, self._help.availability)) for tab in _TABS)
181
+ return _SectionHeightTable(sections[_TABS.index(self._tab)], sections)
182
+
183
+ def action_select_tab(self, index: int) -> None:
184
+ """Show the tab at `index` from the top."""
185
+ tab = _TABS[index]
186
+ if tab is self._tab:
187
+ return
188
+ self._tab = tab
189
+ self._paint_tabs()
190
+ self.query_one(f"#{HELP_COPY}", Static).update(self._table())
191
+ self.query_one(f"#{HELP_SCROLL}", VerticalScroll).scroll_home(animate=False)
192
+
193
+ def action_switch(self, step: int) -> None:
194
+ """Show the neighbouring tab, stopping at both ends."""
195
+ self.action_select_tab(min(max(_TABS.index(self._tab) + step, 0), len(_TABS) - 1))
196
+
197
+ async def on_button_pressed(self, event: Button.Pressed) -> None:
198
+ """Close help when its button is activated."""
199
+ if event.button.id == HELP_CLOSE_BUTTON:
200
+ self.dismiss()
201
+
202
+ async def action_close(self) -> None:
203
+ """Dismiss contextual help."""
204
+ self.dismiss()
@@ -0,0 +1,17 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ from textual.widgets import Input, Select, TextArea
6
+
7
+ if TYPE_CHECKING:
8
+ from textual.widget import Widget
9
+
10
+
11
+ def native_editor_has_focus(focused: Widget | None) -> bool:
12
+ current = focused
13
+ while current is not None:
14
+ if isinstance(current, (Input, Select, TextArea)):
15
+ return True
16
+ current = current.parent
17
+ return False