induscode 0.1.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 (167) hide show
  1. induscode/__init__.py +56 -0
  2. induscode/addons/__init__.py +176 -0
  3. induscode/addons/contract.py +923 -0
  4. induscode/addons/dispatch/__init__.py +43 -0
  5. induscode/addons/dispatch/event_dispatcher.py +348 -0
  6. induscode/addons/dispatch/tool_interceptor.py +349 -0
  7. induscode/addons/host.py +469 -0
  8. induscode/addons/loader.py +314 -0
  9. induscode/addons/manifest.py +232 -0
  10. induscode/addons/surface.py +199 -0
  11. induscode/boot/__init__.py +108 -0
  12. induscode/boot/auth_vault.py +323 -0
  13. induscode/boot/boot.py +210 -0
  14. induscode/boot/contract.py +223 -0
  15. induscode/boot/invocation.py +117 -0
  16. induscode/boot/runners/__init__.py +42 -0
  17. induscode/boot/runners/link_runner.py +82 -0
  18. induscode/boot/runners/oneshot_runner.py +85 -0
  19. induscode/boot/runners/registry.py +46 -0
  20. induscode/boot/runners/repl_runner.py +340 -0
  21. induscode/boot/runners/session.py +549 -0
  22. induscode/boot/stages.py +198 -0
  23. induscode/boot/upgrade/__init__.py +36 -0
  24. induscode/boot/upgrade/apply.py +125 -0
  25. induscode/boot/upgrade/upgrades.py +136 -0
  26. induscode/briefing/__init__.py +115 -0
  27. induscode/briefing/compose.py +414 -0
  28. induscode/briefing/contract.py +528 -0
  29. induscode/briefing/macros.py +721 -0
  30. induscode/briefing/skills.py +417 -0
  31. induscode/capability_deck/__init__.py +233 -0
  32. induscode/capability_deck/bridge_ledger/__init__.py +66 -0
  33. induscode/capability_deck/bridge_ledger/key.py +181 -0
  34. induscode/capability_deck/bridge_ledger/ledger.py +276 -0
  35. induscode/capability_deck/bridge_ledger/network.py +336 -0
  36. induscode/capability_deck/builtin_bridge.py +358 -0
  37. induscode/capability_deck/cards/__init__.py +116 -0
  38. induscode/capability_deck/cards/bg_process.py +482 -0
  39. induscode/capability_deck/cards/memory.py +226 -0
  40. induscode/capability_deck/cards/saas.py +280 -0
  41. induscode/capability_deck/cards/task.py +256 -0
  42. induscode/capability_deck/cards/todo.py +312 -0
  43. induscode/capability_deck/contract.py +450 -0
  44. induscode/capability_deck/manifest.py +126 -0
  45. induscode/capability_deck/provision.py +217 -0
  46. induscode/channels/__init__.py +146 -0
  47. induscode/channels/contract.py +585 -0
  48. induscode/channels/framer.py +132 -0
  49. induscode/channels/link/__init__.py +50 -0
  50. induscode/channels/link/dialog.py +246 -0
  51. induscode/channels/link/driver.py +308 -0
  52. induscode/channels/link/server.py +217 -0
  53. induscode/channels/oneshot.py +178 -0
  54. induscode/channels/ops.py +140 -0
  55. induscode/channels/session_ops.py +172 -0
  56. induscode/conductor/__init__.py +240 -0
  57. induscode/conductor/catalog.py +309 -0
  58. induscode/conductor/conductor.py +1084 -0
  59. induscode/conductor/contract.py +1035 -0
  60. induscode/conductor/matcher.py +291 -0
  61. induscode/conductor/serialize.py +575 -0
  62. induscode/conductor/signal_hub.py +382 -0
  63. induscode/conductor/skill_parse.py +294 -0
  64. induscode/conductor/transcript_store.py +449 -0
  65. induscode/console/__init__.py +236 -0
  66. induscode/console/app.py +1677 -0
  67. induscode/console/components/__init__.py +62 -0
  68. induscode/console/components/banner.py +499 -0
  69. induscode/console/components/banner_sweep.py +188 -0
  70. induscode/console/components/emblem.py +181 -0
  71. induscode/console/components/status_bar.py +102 -0
  72. induscode/console/contract.py +836 -0
  73. induscode/console/input/__init__.py +107 -0
  74. induscode/console/input/chord.py +197 -0
  75. induscode/console/input/dir_reader.py +113 -0
  76. induscode/console/input/intents.py +258 -0
  77. induscode/console/input/providers.py +469 -0
  78. induscode/console/mount.py +137 -0
  79. induscode/console/overlays/__init__.py +94 -0
  80. induscode/console/overlays/auth.py +503 -0
  81. induscode/console/overlays/pickers.py +526 -0
  82. induscode/console/overlays/router.py +129 -0
  83. induscode/console/overlays/sessions.py +232 -0
  84. induscode/console/reducer.py +145 -0
  85. induscode/console/resume_picker.py +156 -0
  86. induscode/console/slash_commands/__init__.py +78 -0
  87. induscode/console/slash_commands/builtins.py +254 -0
  88. induscode/console/slash_commands/dynamic.py +217 -0
  89. induscode/console/slash_commands/integrations.py +949 -0
  90. induscode/console/slash_commands/transcript.py +404 -0
  91. induscode/console/slash_commands/workbench.py +430 -0
  92. induscode/console/startup.py +434 -0
  93. induscode/console/theme/__init__.py +44 -0
  94. induscode/console/theme/adapter.py +168 -0
  95. induscode/console/theme/palette.py +128 -0
  96. induscode/console/theme/resolve.py +123 -0
  97. induscode/console/theme/tokens.py +185 -0
  98. induscode/console_slash/__init__.py +111 -0
  99. induscode/console_slash/contract.py +185 -0
  100. induscode/console_slash/registry.py +140 -0
  101. induscode/console_slash/resolve.py +194 -0
  102. induscode/console_slash/shared.py +172 -0
  103. induscode/entry.py +108 -0
  104. induscode/insight/__init__.py +153 -0
  105. induscode/insight/collector.py +73 -0
  106. induscode/insight/replay.py +305 -0
  107. induscode/insight/wrapper.py +1115 -0
  108. induscode/kit/__init__.py +82 -0
  109. induscode/kit/clipboard_image.py +215 -0
  110. induscode/kit/external_editor.py +120 -0
  111. induscode/kit/image.py +188 -0
  112. induscode/kit/shell.py +89 -0
  113. induscode/kit/tool_fetch.py +288 -0
  114. induscode/launch/__init__.py +224 -0
  115. induscode/launch/catalog.py +310 -0
  116. induscode/launch/contract.py +569 -0
  117. induscode/launch/credentials.py +852 -0
  118. induscode/launch/invocation/__init__.py +39 -0
  119. induscode/launch/invocation/attachments.py +281 -0
  120. induscode/launch/invocation/flags.py +210 -0
  121. induscode/launch/invocation/read.py +369 -0
  122. induscode/launch/invocation/usage.py +110 -0
  123. induscode/launch/oauth.py +808 -0
  124. induscode/launch/packages.py +299 -0
  125. induscode/launch/pickers.py +291 -0
  126. induscode/py.typed +0 -0
  127. induscode/runtime_bridge/__init__.py +166 -0
  128. induscode/runtime_bridge/bridges/__init__.py +66 -0
  129. induscode/runtime_bridge/bridges/_drive.py +268 -0
  130. induscode/runtime_bridge/bridges/builtins.py +177 -0
  131. induscode/runtime_bridge/bridges/claude_cli.py +198 -0
  132. induscode/runtime_bridge/bridges/codex_cli.py +203 -0
  133. induscode/runtime_bridge/bridges/indusagi_cli.py +217 -0
  134. induscode/runtime_bridge/broker.py +397 -0
  135. induscode/runtime_bridge/contract.py +734 -0
  136. induscode/runtime_bridge/sink.py +351 -0
  137. induscode/sessions/__init__.py +25 -0
  138. induscode/sessions/contract.py +119 -0
  139. induscode/sessions/library.py +350 -0
  140. induscode/settings/__init__.py +47 -0
  141. induscode/settings/contract.py +313 -0
  142. induscode/settings/manager.py +268 -0
  143. induscode/transcript_export/__init__.py +109 -0
  144. induscode/transcript_export/contract.py +522 -0
  145. induscode/transcript_export/publish.py +455 -0
  146. induscode/transcript_export/sgr.py +566 -0
  147. induscode/transcript_export/template.py +319 -0
  148. induscode/transcript_export/theme_bridge.py +325 -0
  149. induscode/window_budget/__init__.py +76 -0
  150. induscode/window_budget/budget/__init__.py +26 -0
  151. induscode/window_budget/budget/estimate.py +273 -0
  152. induscode/window_budget/budget/gate.py +60 -0
  153. induscode/window_budget/budget/slice.py +145 -0
  154. induscode/window_budget/condenser.py +170 -0
  155. induscode/window_budget/contract.py +329 -0
  156. induscode/window_budget/summarize/__init__.py +33 -0
  157. induscode/window_budget/summarize/condense.py +212 -0
  158. induscode/window_budget/summarize/prompt.py +241 -0
  159. induscode/workspace/__init__.py +30 -0
  160. induscode/workspace/brand.py +96 -0
  161. induscode/workspace/locator.py +269 -0
  162. induscode-0.1.0.dist-info/METADATA +97 -0
  163. induscode-0.1.0.dist-info/RECORD +167 -0
  164. induscode-0.1.0.dist-info/WHEEL +4 -0
  165. induscode-0.1.0.dist-info/entry_points.txt +3 -0
  166. induscode-0.1.0.dist-info/licenses/CREDITS.md +22 -0
  167. induscode-0.1.0.dist-info/licenses/NOTICE +7 -0
@@ -0,0 +1,128 @@
1
+ """Theme palettes — the console's own re-derived accent ramps.
2
+
3
+ Port of TS ``src/console/theme/palette.ts``; the hex values are carried over
4
+ VERBATIM. This module owns the four raw
5
+ :class:`~induscode.console.contract.ThemePalette` values the console ships
6
+ with, one per :data:`~induscode.console.contract.ThemeScheme`. A palette is
7
+ the *source* nine-stop ramp a scheme is built from: three accent hues (a cool
8
+ primary, a warm secondary, a muted support tertiary), a three-stop neutral
9
+ text gradient (ink/body/muted), and the three status hues
10
+ (affirm/caution/alarm). Everything the console renders is computed from these
11
+ stops by the token derivation step — no module downstream of here writes a
12
+ literal hex.
13
+
14
+ The hex values are an original ramp authored for this console. The cool
15
+ primary is a desaturated spring-green/teal, the warm secondary a tangerine
16
+ amber, and the support tertiary a soft periwinkle; the neutral gradient is a
17
+ cool slate. The ``daylight`` variant re-derives the same roles at higher
18
+ saturation / lower luminance so they read on a bright terminal background.
19
+ None of these stops reuse an upstream accent — they were picked fresh against
20
+ a contrast target for each role.
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ from dataclasses import replace
26
+ from types import MappingProxyType
27
+ from typing import Final, Mapping
28
+
29
+ from ..contract import ThemePalette, ThemeScheme
30
+
31
+ __all__ = [
32
+ "DAYLIGHT_CB_PALETTE",
33
+ "DAYLIGHT_PALETTE",
34
+ "MIDNIGHT_CB_PALETTE",
35
+ "MIDNIGHT_PALETTE",
36
+ "PALETTES",
37
+ ]
38
+
39
+
40
+ # ---------------------------------------------------------------------------
41
+ # The two raw ramps
42
+ # ---------------------------------------------------------------------------
43
+
44
+ #: The dark-terminal ramp.
45
+ #:
46
+ #: Cool spring-teal primary against a cool-slate neutral gradient, with a warm
47
+ #: amber secondary for contrast accents and a soft periwinkle support tone.
48
+ MIDNIGHT_PALETTE: Final[ThemePalette] = ThemePalette(
49
+ primary="#3ad6b4",
50
+ secondary="#f0a24b",
51
+ tertiary="#7c9fe8",
52
+ ink="#f4f6fb",
53
+ body="#c3c8d4",
54
+ muted="#7e8696",
55
+ affirm="#46c98a",
56
+ caution="#e8b341",
57
+ alarm="#ef6a6a",
58
+ )
59
+
60
+ #: The light-terminal ramp.
61
+ #:
62
+ #: The same role assignments re-derived deeper and more saturated so each hue
63
+ #: keeps its contrast against a bright background; the neutral gradient
64
+ #: inverts to a dark-on-light slate.
65
+ DAYLIGHT_PALETTE: Final[ThemePalette] = ThemePalette(
66
+ primary="#0f8f78",
67
+ secondary="#c26a10",
68
+ tertiary="#3a5ec0",
69
+ ink="#1c2230",
70
+ body="#3a4253",
71
+ muted="#6b7382",
72
+ affirm="#1d9d63",
73
+ caution="#a9740a",
74
+ alarm="#c4322f",
75
+ )
76
+
77
+
78
+ # ---------------------------------------------------------------------------
79
+ # The daltonized (color-blind-friendly) ramps
80
+ # ---------------------------------------------------------------------------
81
+
82
+ #: The dark-terminal color-blind-safe ramp.
83
+ #:
84
+ #: A clone of :data:`MIDNIGHT_PALETTE` that re-derives the three *status*
85
+ #: hues so a red-green color-blind user (deuteran/protan, ~8% of men) can
86
+ #: tell success from failure without relying on the red-green axis. Success
87
+ #: moves off green onto a vivid blue (``affirm → #4aa3ff``); failure stays
88
+ #: red but is deepened to a darker tone (``alarm → #c83232``) so it sits well
89
+ #: below the bright blue in lightness, and the amber warning is nudged
90
+ #: brighter — so the three status tones separate by *lightness*, not hue
91
+ #: alone. The three accent hues and the neutral gradient are carried over
92
+ #: unchanged from the base midnight ramp — only the status stops move, so
93
+ #: the overall look stays the midnight scheme.
94
+ MIDNIGHT_CB_PALETTE: Final[ThemePalette] = replace(
95
+ MIDNIGHT_PALETTE,
96
+ affirm="#4aa3ff",
97
+ caution="#f2c75c",
98
+ alarm="#c83232",
99
+ )
100
+
101
+ #: The light-terminal color-blind-safe ramp.
102
+ #:
103
+ #: The daylight counterpart of :data:`MIDNIGHT_CB_PALETTE`: clones
104
+ #: :data:`DAYLIGHT_PALETTE` and remaps success to a deeper blue tuned for a
105
+ #: bright background (``affirm → #1f6fd6``), deepens the red alarm
106
+ #: (``alarm → #b02622``) for lightness separation from that blue, and darkens
107
+ #: the amber warning so the three status tones stay separable by lightness on
108
+ #: a light terminal too.
109
+ DAYLIGHT_CB_PALETTE: Final[ThemePalette] = replace(
110
+ DAYLIGHT_PALETTE,
111
+ affirm="#1f6fd6",
112
+ caution="#8a5e00",
113
+ alarm="#b02622",
114
+ )
115
+
116
+ #: The raw ramp for each scheme, keyed by
117
+ #: :data:`~induscode.console.contract.ThemeScheme`.
118
+ #:
119
+ #: The token derivation step reads from this table; the resolver returns the
120
+ #: scheme whose palette it expanded.
121
+ PALETTES: Final[Mapping[ThemeScheme, ThemePalette]] = MappingProxyType(
122
+ {
123
+ "midnight": MIDNIGHT_PALETTE,
124
+ "daylight": DAYLIGHT_PALETTE,
125
+ "midnight-cb": MIDNIGHT_CB_PALETTE,
126
+ "daylight-cb": DAYLIGHT_CB_PALETTE,
127
+ }
128
+ )
@@ -0,0 +1,123 @@
1
+ """Theme resolution — assemble the fully-resolved :class:`ConsoleTheme` for
2
+ each scheme and expose the picker entry points.
3
+
4
+ Port of TS ``src/console/theme/resolve.ts``. This is the top of the theme
5
+ engine. It runs the pipeline once per scheme at module load:
6
+
7
+ .. code-block:: text
8
+
9
+ palette ──derive_tokens──▶ tokens ──theme_bundle──▶ framework bundle
10
+ └─────────────────────────┴────────────────────────────┘
11
+ ConsoleTheme
12
+
13
+ The four assembled themes are frozen into :data:`THEMES`;
14
+ :func:`resolve_theme` is the single sanctioned way the surface obtains the
15
+ theme for a scheme name. Because the heavy work (adapter + Textual Theme +
16
+ Pygments style construction) happens here at the config-load boundary, the
17
+ render path only ever reads an already-built :class:`ConsoleTheme` — it never
18
+ re-derives colours.
19
+
20
+ Port delta (locked; analysis 02 §5): each theme carries a full framework
21
+ ``ThemeBundle``; the M5-wave-2 ``ConsoleApp`` registers every bundle's
22
+ Textual Theme at startup so ``scheme:set`` becomes a native
23
+ ``app.theme = scheme`` live retheme. The Textual dark flag is derived from
24
+ the ramp the same way the token math picks its background anchor: a light
25
+ ``ink`` implies a dark terminal.
26
+ """
27
+
28
+ from __future__ import annotations
29
+
30
+ from types import MappingProxyType
31
+ from typing import Final, Mapping
32
+
33
+ from ..contract import (
34
+ DEFAULT_SCHEME,
35
+ ConsoleTheme,
36
+ ThemePalette,
37
+ ThemeScheme,
38
+ is_theme_scheme,
39
+ )
40
+ from .adapter import theme_bundle
41
+ from .palette import PALETTES
42
+ from .tokens import derive_tokens, luminance
43
+
44
+ __all__ = [
45
+ "THEMES",
46
+ "THEME_SCHEMES",
47
+ "resolve_theme",
48
+ ]
49
+
50
+
51
+ # ---------------------------------------------------------------------------
52
+ # Assembly
53
+ # ---------------------------------------------------------------------------
54
+
55
+
56
+ def _assemble_theme(scheme: ThemeScheme, palette: ThemePalette) -> ConsoleTheme:
57
+ """Run the full pipeline for one scheme: derive tokens from the ramp,
58
+ build the framework bundle from those tokens, and box everything into a
59
+ :class:`ConsoleTheme`.
60
+
61
+ :param scheme: the scheme identity this resolves
62
+ :param palette: the raw ramp the scheme is built from
63
+ """
64
+ tokens = derive_tokens(palette)
65
+ # A light high-contrast ink means a dark terminal — the same anchor logic
66
+ # the diff-background derivation uses.
67
+ bundle = theme_bundle(scheme, tokens, dark=luminance(palette.ink) >= 0.5)
68
+ return ConsoleTheme(
69
+ scheme=scheme,
70
+ palette=palette,
71
+ tokens=tokens,
72
+ adapter=bundle.adapter,
73
+ bundle=bundle,
74
+ )
75
+
76
+
77
+ # ---------------------------------------------------------------------------
78
+ # The resolved theme table
79
+ # ---------------------------------------------------------------------------
80
+
81
+ #: The built-in themes, fully resolved and keyed by
82
+ #: :data:`~induscode.console.contract.ThemeScheme`.
83
+ #:
84
+ #: Assembled once at load time from :data:`~.palette.PALETTES`. This is the
85
+ #: table the theme picker lists and :func:`resolve_theme` reads; it is the
86
+ #: console's complete shipped theme set.
87
+ THEMES: Final[Mapping[ThemeScheme, ConsoleTheme]] = MappingProxyType(
88
+ {
89
+ "midnight": _assemble_theme("midnight", PALETTES["midnight"]),
90
+ "daylight": _assemble_theme("daylight", PALETTES["daylight"]),
91
+ "midnight-cb": _assemble_theme("midnight-cb", PALETTES["midnight-cb"]),
92
+ "daylight-cb": _assemble_theme("daylight-cb", PALETTES["daylight-cb"]),
93
+ }
94
+ )
95
+
96
+
97
+ # ---------------------------------------------------------------------------
98
+ # The resolver
99
+ # ---------------------------------------------------------------------------
100
+
101
+
102
+ def resolve_theme(scheme: str | None = None) -> ConsoleTheme:
103
+ """Resolve a scheme name to its fully-built :class:`ConsoleTheme`.
104
+
105
+ The single entry point the surface and the loader use to turn a scheme
106
+ name (from settings, a slash argument, or the
107
+ :data:`~induscode.console.contract.DEFAULT_SCHEME`) into a theme. An
108
+ unrecognised or absent name falls back to the default rather than
109
+ raising, so a corrupt preference never blanks the console.
110
+
111
+ :param scheme: a candidate scheme name, possibly invalid or ``None``
112
+ :returns: the resolved theme for that scheme, or the default theme
113
+ """
114
+ if isinstance(scheme, str) and is_theme_scheme(scheme):
115
+ return THEMES[scheme]
116
+ return THEMES[DEFAULT_SCHEME]
117
+
118
+
119
+ #: The schemes the console offers, in picker order.
120
+ #:
121
+ #: Derived from :data:`THEMES` so adding a scheme to the table adds it to the
122
+ #: picker with no second list to update.
123
+ THEME_SCHEMES: Final[tuple[ThemeScheme, ...]] = tuple(THEMES.keys())
@@ -0,0 +1,185 @@
1
+ """Token derivation — expand a raw :class:`ThemePalette` into the semantic
2
+ :class:`ThemeTokens` the console actually renders against.
3
+
4
+ Port of TS ``src/console/theme/tokens.ts``; the hex math (parse / format /
5
+ blend / luminance / anchor) is carried over exactly, including JS
6
+ ``Math.round`` semantics for channel rounding.
7
+
8
+ The console body never names a hex or a palette stop; it names a *role*
9
+ (``signal``, ``frame``, ``body_text``, ``alarm``, …). This module is the one
10
+ place the thirteen roles are bound to ramp stops. The mapping is pure and
11
+ total: every field of :class:`ThemeTokens` is assigned exactly once from a
12
+ :class:`ThemePalette` field, so a ramp that is missing a stop fails at
13
+ construction rather than producing an undefined colour at render time.
14
+
15
+ Role → stop assignments (identical structure for both schemes; the schemes
16
+ differ only in the underlying ramp):
17
+
18
+ - ``signal`` ← ``primary`` (the dominant accent: focus, selection)
19
+ - ``frame`` ← ``tertiary`` (default structural border lines)
20
+ - ``quiet_frame`` ← ``muted`` (de-emphasised separators)
21
+ - ``prompt_surface`` ← ``secondary`` (the composer's active tint)
22
+ - ``card_accent`` ← ``secondary`` (row gutters and headers)
23
+ - ``body_text`` ← ``body`` (default answer foreground)
24
+ - ``muted_text`` ← ``muted`` (timestamps, hints, metadata)
25
+ - ``ink_text`` ← ``ink`` (high-contrast text on an accent)
26
+ - ``notice`` ← ``tertiary`` (informational tone)
27
+ - ``affirm`` ← ``affirm`` (success tone)
28
+ - ``caution`` ← ``caution`` (warning tone)
29
+ - ``alarm`` ← ``alarm`` (error/fault tone)
30
+ - ``pending`` ← ``primary`` (busy/in-flight tone)
31
+
32
+ Rich-render roles (markdown / diff / syntax highlighting) are derived from
33
+ the same nine stops so the styled-transcript / colored-diff / fenced-code
34
+ surfaces recolour with the scheme and need no extra palette stop:
35
+
36
+ - ``code_inline`` ← ``primary`` (inline code accent)
37
+ - ``heading`` ← ``primary`` (heading accent)
38
+ - ``blockquote_bar`` ← ``muted`` (dim quote bar)
39
+ - ``diff_added_bg`` ← ``affirm`` blended to anchor (added-line tint)
40
+ - ``diff_removed_bg`` ← ``alarm`` blended to anchor (removed-line tint)
41
+ - ``diff_added_text`` ← ``affirm`` (added fg / ``+``)
42
+ - ``diff_removed_text`` ← ``alarm`` (removed fg / ``-``)
43
+ - ``syn_keyword`` ← ``primary`` (keywords)
44
+ - ``syn_string`` ← ``affirm`` (strings)
45
+ - ``syn_number`` ← ``caution`` (numbers)
46
+ - ``syn_comment`` ← ``muted`` (comments)
47
+ - ``syn_type`` ← ``tertiary`` (types / classes)
48
+
49
+ Deriving the two diff backgrounds (rather than adding raw stops) keeps the
50
+ :class:`ThemePalette` a flat nine-stop ramp while guaranteeing every scheme —
51
+ including the color-blind variants — gets a legible ``+``/``-`` tint that
52
+ tracks its own success/alarm hue.
53
+ """
54
+
55
+ from __future__ import annotations
56
+
57
+ import math
58
+
59
+ from ..contract import ThemePalette, ThemeTokens
60
+
61
+ __all__ = [
62
+ "derive_tokens",
63
+ "luminance",
64
+ ]
65
+
66
+
67
+ # ---------------------------------------------------------------------------
68
+ # Hex helpers — derive the diff-background tints
69
+ # ---------------------------------------------------------------------------
70
+
71
+
72
+ def _parse_hex(hex_color: str) -> tuple[int, int, int]:
73
+ """Parse a ``#rrggbb`` (or ``#rgb``) string into an ``(r, g, b)`` triple
74
+ (0–255)."""
75
+ h = hex_color.strip()
76
+ if h.startswith("#"):
77
+ h = h[1:]
78
+ if len(h) == 3:
79
+ h = "".join(c + c for c in h)
80
+ try:
81
+ n = int(h[:6].ljust(6, "0"), 16)
82
+ except ValueError:
83
+ # TS Number.parseInt yields NaN here; the bit-shifts then read 0.
84
+ n = 0
85
+ return ((n >> 16) & 0xFF, (n >> 8) & 0xFF, n & 0xFF)
86
+
87
+
88
+ def _js_round(value: float) -> int:
89
+ """JS ``Math.round`` — half-up toward +infinity (Python ``round`` banks)."""
90
+ return math.floor(value + 0.5)
91
+
92
+
93
+ def _to_hex(rgb: tuple[float, float, float]) -> str:
94
+ """Format an ``(r, g, b)`` triple back into a ``#rrggbb`` string."""
95
+ r, g, b = rgb
96
+ clamped = (max(0, min(255, _js_round(v))) for v in (r, g, b))
97
+ return "#" + "".join(f"{v:02x}" for v in clamped)
98
+
99
+
100
+ def _mix_hex(a: str, b: str, t: float) -> str:
101
+ """Linearly blend two hex colours; ``t`` is the weight of ``b`` (0 → ``a``,
102
+ 1 → ``b``). Used to pull a saturated status hue toward a neutral anchor so
103
+ the resulting diff-line tint is dark/quiet enough for content to read on
104
+ top of it."""
105
+ ar, ag, ab = _parse_hex(a)
106
+ br, bg, bb = _parse_hex(b)
107
+ return _to_hex(
108
+ (
109
+ ar + (br - ar) * t,
110
+ ag + (bg - ag) * t,
111
+ ab + (bb - ab) * t,
112
+ )
113
+ )
114
+
115
+
116
+ def _diff_background(hue: str, anchor: str) -> str:
117
+ """Derive a diff-line background tint from a status hue by blending it
118
+ heavily (78%) toward the scheme's implied terminal background. We keep
119
+ ~22% of the hue so the tint is recognisably green/red (or blue/red on the
120
+ CB schemes) without overpowering the foreground text painted on top."""
121
+ return _mix_hex(hue, anchor, 0.78)
122
+
123
+
124
+ def luminance(hex_color: str) -> float:
125
+ """Relative luminance (0 dark → 1 light) of a hex colour, for anchor
126
+ choice (and the resolver's Textual dark-flag derivation)."""
127
+ r, g, b = _parse_hex(hex_color)
128
+ return (0.2126 * r + 0.7152 * g + 0.0722 * b) / 255
129
+
130
+
131
+ def _background_anchor(palette: ThemePalette) -> str:
132
+ """The neutral a diff tint should be pulled toward — the scheme's implied
133
+ terminal background. A scheme whose high-contrast ``ink`` is light (a
134
+ dark terminal) implies a near-black background, so we blend the status
135
+ hue toward black; a scheme with dark ``ink`` (a light terminal) blends
136
+ toward white. This keeps the added/removed tints quiet enough for the
137
+ diff content painted on top to stay legible in either scheme family."""
138
+ return "#000000" if luminance(palette.ink) >= 0.5 else "#ffffff"
139
+
140
+
141
+ # ---------------------------------------------------------------------------
142
+ # Derivation
143
+ # ---------------------------------------------------------------------------
144
+
145
+
146
+ def derive_tokens(palette: ThemePalette) -> ThemeTokens:
147
+ """Map a raw accent ramp onto the closed semantic token set.
148
+
149
+ Pure and deterministic: the same palette always yields the same tokens,
150
+ and the returned object is a fresh frozen value the resolver can cache.
151
+ The role assignments are shared by both schemes — only the ramp handed in
152
+ differs.
153
+
154
+ :param palette: the raw nine-stop ramp for a scheme
155
+ :returns: the full semantic token map (status + rich-render roles)
156
+ """
157
+ anchor = _background_anchor(palette)
158
+ return ThemeTokens(
159
+ signal=palette.primary,
160
+ frame=palette.tertiary,
161
+ quiet_frame=palette.muted,
162
+ prompt_surface=palette.secondary,
163
+ card_accent=palette.secondary,
164
+ body_text=palette.body,
165
+ muted_text=palette.muted,
166
+ ink_text=palette.ink,
167
+ notice=palette.tertiary,
168
+ affirm=palette.affirm,
169
+ caution=palette.caution,
170
+ alarm=palette.alarm,
171
+ pending=palette.primary,
172
+ # Rich-render roles — derived from the same nine stops.
173
+ code_inline=palette.primary,
174
+ heading=palette.primary,
175
+ blockquote_bar=palette.muted,
176
+ diff_added_bg=_diff_background(palette.affirm, anchor),
177
+ diff_removed_bg=_diff_background(palette.alarm, anchor),
178
+ diff_added_text=palette.affirm,
179
+ diff_removed_text=palette.alarm,
180
+ syn_keyword=palette.primary,
181
+ syn_string=palette.affirm,
182
+ syn_number=palette.caution,
183
+ syn_comment=palette.muted,
184
+ syn_type=palette.tertiary,
185
+ )
@@ -0,0 +1,111 @@
1
+ """Slash command framework — contracts, registry assembly, line resolution,
2
+ and the shared command toolkit.
3
+
4
+ Port of the *framework* half of TS ``src/console/slash`` (``contract.ts``'s
5
+ slash types, ``registry.ts``, ``resolve.ts``, ``commands/shared.ts``), in
6
+ three pure pieces:
7
+
8
+ - **The contracts** — :class:`SlashCommand` rows, the
9
+ :class:`Handled` / :class:`Prompt` / :class:`Unknown` outcome union, the
10
+ :class:`SlashContext` dataclass of callables a handler acts through, and
11
+ the frozen :class:`SlashRegistry` shape.
12
+ - **The registry** — :func:`build_registry` folds an ordered command list
13
+ into the registry (commands + a name/alias index, duplicate tokens
14
+ rejected loudly), and the derived lookups (:func:`match_prefix`,
15
+ :func:`find_command`, :func:`commands_in_family`, :func:`list_families`)
16
+ power resolution, completion, and grouped listing.
17
+ - **Resolution** — :func:`resolve_slash` parses a typed composer line and
18
+ resolves it against a registry to a discriminated
19
+ :class:`NotSlash` / :class:`Match` / :class:`Miss` outcome.
20
+
21
+ Milestone note (M1)
22
+ -------------------
23
+ This package lands under ``induscode.console_slash`` for M1: it is a pure
24
+ leaf with no console dependency. The full console package
25
+ (``induscode.console``) arrives with M5 and will re-export this surface as
26
+ its ``slash`` layer; M5 also brings the command catalog itself —
27
+ ``builtins.py`` with the explicit ``build_catalog(cwd, home)`` assembly (no
28
+ import-time I/O) and the pre-folded default registry (the TS
29
+ ``DEFAULT_SLASH_REGISTRY``) — plus the command groups
30
+ (``transcript`` / ``workbench`` / ``integrations`` / ``dynamic``). The
31
+ effectful dispatcher (which awaits a matched command's ``run``) likewise
32
+ lives outside this barrel; everything exported here is pure.
33
+ """
34
+
35
+ from .contract import (
36
+ Handled,
37
+ OpenModal,
38
+ Prompt,
39
+ SlashCommand,
40
+ SlashContext,
41
+ SlashOutcome,
42
+ SlashRegistry,
43
+ SlashRun,
44
+ Unknown,
45
+ )
46
+ from .registry import (
47
+ build_registry,
48
+ commands_in_family,
49
+ find_command,
50
+ list_families,
51
+ match_prefix,
52
+ tokens_of,
53
+ )
54
+ from .resolve import (
55
+ SLASH_PREFIX,
56
+ Match,
57
+ Miss,
58
+ NotSlash,
59
+ SlashLine,
60
+ SlashResolution,
61
+ looks_like_slash,
62
+ parse_slash,
63
+ resolve_slash,
64
+ )
65
+ from .shared import (
66
+ FAMILY,
67
+ HANDLED,
68
+ FamilyLabels,
69
+ SubCommand,
70
+ VerbSplit,
71
+ family_runner,
72
+ info,
73
+ split_verb,
74
+ warn,
75
+ )
76
+
77
+ __all__ = [
78
+ "FAMILY",
79
+ "FamilyLabels",
80
+ "HANDLED",
81
+ "Handled",
82
+ "Match",
83
+ "Miss",
84
+ "NotSlash",
85
+ "OpenModal",
86
+ "Prompt",
87
+ "SLASH_PREFIX",
88
+ "SlashCommand",
89
+ "SlashContext",
90
+ "SlashLine",
91
+ "SlashOutcome",
92
+ "SlashRegistry",
93
+ "SlashResolution",
94
+ "SlashRun",
95
+ "SubCommand",
96
+ "Unknown",
97
+ "VerbSplit",
98
+ "build_registry",
99
+ "commands_in_family",
100
+ "family_runner",
101
+ "find_command",
102
+ "info",
103
+ "list_families",
104
+ "looks_like_slash",
105
+ "match_prefix",
106
+ "parse_slash",
107
+ "resolve_slash",
108
+ "split_verb",
109
+ "tokens_of",
110
+ "warn",
111
+ ]