Source code for build_tools.syllable_walk_tui.modules.generator.panel

"""
Combiner panel UI component.

This module provides the CombinerPanel widget for name_combiner controls.
The panel mirrors the exact CLI options from build_tools/name_combiner.

CLI Options → UI Controls:
    --run-dir          → Determined by which patch panel this is in
    --syllables        → Syllables spinner (2-4, default: 2)
    --count            → Count spinner (default: 10000)
    --seed             → Seed input (None = random)
    --frequency-weight → Frequency weight slider (0.0-1.0)
"""

from __future__ import annotations

from textual.app import ComposeResult
from textual.widgets import Button, Label, Static

from build_tools.syllable_walk_tui.controls import FloatSlider, IntSpinner, RadioOption, SeedInput


[docs] class CombinerPanel(Static): """ Panel with name_combiner controls for a specific patch. Mirrors the CLI options exactly: - Syllables: 2, 3, or 4 (--syllables) - Count: Number of candidates (--count, default: 10000) - Seed: RNG seed (--seed, None = random) - Frequency Weight: Sampling bias (--frequency-weight, default: 1.0) Args: patch_name: Name of the patch ("A" or "B") """ DEFAULT_CSS = """ CombinerPanel { width: 100%; height: auto; padding: 1; } CombinerPanel .panel-header { text-style: bold; color: $accent; margin-bottom: 1; } CombinerPanel .control-row { height: auto; margin-bottom: 1; } CombinerPanel .mode-label { margin-top: 1; margin-bottom: 0; } CombinerPanel .mode-options { layout: horizontal; height: auto; margin-bottom: 1; } CombinerPanel .combiner-mode-options { layout: vertical; height: auto; margin-bottom: 1; } /* Override spinner-value width for count spinner (needs room for 100000) */ #combiner-count-a .spinner-value, #combiner-count-b .spinner-value { width: 10; } CombinerPanel .generate-button { margin-top: 1; margin-bottom: 1; } CombinerPanel .output-section { margin-top: 1; border-top: solid $primary-darken-2; padding-top: 1; } CombinerPanel .output-header { text-style: bold; color: $primary; margin-bottom: 1; } CombinerPanel .meta-line { color: $text; } CombinerPanel .meta-label { color: $text-muted; } CombinerPanel .meta-value { color: $success; } CombinerPanel .meta-path { color: $accent; text-style: italic; } CombinerPanel .placeholder { color: $text-muted; text-style: italic; } """ def __init__(self, patch_name: str = "A", **kwargs) -> None: """ Initialize combiner panel. Args: patch_name: Name of the patch ("A" or "B") """ super().__init__(**kwargs) self.patch_name = patch_name
[docs] def compose(self) -> ComposeResult: """Create combiner panel layout matching CLI options.""" yield Label(f"PATCH {self.patch_name} NAME COMBINER", classes="panel-header") # --syllables: 2, 3, or 4 (exact mode) yield IntSpinner( "Syllables (Exact)", value=2, min_val=2, max_val=4, step=1, id=f"combiner-syllables-{self.patch_name.lower()}", ) # Syllable mode (exact vs all) yield Label("Syllable Mode:", classes="mode-label") with Static(classes="combiner-mode-options"): yield RadioOption( "exact", "Use selected count", is_selected=True, id=f"combiner-mode-exact-{self.patch_name.lower()}", ) yield RadioOption( "all", "Generate 2-4", is_selected=False, id=f"combiner-mode-all-{self.patch_name.lower()}", ) # --count: Number of candidates (default: 10000) yield IntSpinner( "Count", value=10000, min_val=100, max_val=100000, step=1000, id=f"combiner-count-{self.patch_name.lower()}", ) # --seed: RNG seed (None = random) yield SeedInput(value=None, id=f"combiner-seed-{self.patch_name.lower()}") # --frequency-weight: 0.0-1.0 (default: 1.0) yield FloatSlider( "Freq Weight", value=1.0, min_val=0.0, max_val=1.0, step=0.1, precision=1, id=f"combiner-freq-weight-{self.patch_name.lower()}", ) # Generate button yield Button( "Generate Candidates", id=f"generate-candidates-{self.patch_name.lower()}", variant="primary", classes="generate-button", ) # Output section (metadata display) with Static( classes="output-section", id=f"combiner-output-section-{self.patch_name.lower()}" ): yield Label("", id=f"combiner-output-{self.patch_name.lower()}", classes="placeholder")
[docs] def update_output(self, meta: dict | None = None) -> None: """ Update the output display with combiner metadata. Args: meta: Metadata dict from combiner (matches nltk_combiner_meta.json structure) Keys: tool, version, generated_at, arguments, output """ try: output_label = self.query_one(f"#combiner-output-{self.patch_name.lower()}", Label) if meta is None: output_label.update("(Press Generate to create candidates)") output_label.set_classes("placeholder") return # Build output text from metadata lines = [] # Arguments section args = meta.get("arguments", {}) syllables_arg = args.get("syllables", "?") if syllables_arg == "all": lines.append("Syllables: all (2-4)") else: lines.append(f"Syllables: {syllables_arg}") lines.append(f"Count: {args.get('count', '?'):,}") lines.append(f"Seed: {args.get('seed', '?')}") lines.append(f"Freq Weight: {args.get('frequency_weight', '?')}") lines.append("") # Output section out = meta.get("output", {}) generated = out.get("candidates_generated", 0) unique = out.get("unique_names", 0) unique_pct = out.get("unique_percentage", 0) lines.append(f"Generated: {generated:,} candidates") lines.append(f"Unique: {unique:,} ({unique_pct:.1f}%)") # Output path (relative) candidates_file = out.get("candidates_file", "") if candidates_file: # Show just the relative part: candidates/xxx.json if "/candidates/" in candidates_file: rel_path = "candidates/" + candidates_file.split("/candidates/")[-1] else: rel_path = candidates_file lines.append(f"→ {rel_path}") output_label.update("\n".join(lines)) output_label.set_classes("meta-line") except Exception: # nosec B110 - Widget may not be mounted yet pass
[docs] def clear_output(self) -> None: """Clear the output display.""" self.update_output(None)