Skip to content

typr tui: interactive editor for typr.yaml (jatatui) + closed-beta gate#192

Merged
oyvindberg merged 2 commits into
mainfrom
tui-revival
May 18, 2026
Merged

typr tui: interactive editor for typr.yaml (jatatui) + closed-beta gate#192
oyvindberg merged 2 commits into
mainfrom
tui-revival

Conversation

@oyvindberg
Copy link
Copy Markdown
Collaborator

@oyvindberg oyvindberg commented May 17, 2026

What's in this PR

Screen.Recording.2026-05-18.at.01.33.51.mp4

A quick tour of typr tui — the new interactive editor for typr.yaml, built on the jatatui-react library.


typr: interactive TUI (jatatui) + closed-beta gate

The visible one. typr tui opens an interactive editor for typr.yaml — sources, outputs, field types, domain types — without ever hand-writing the YAML.

Built on jatatui 0.30.0 — a Java port of tui-rs with a React-style component layer, sister project to typr. Six artifacts: jatatui-{core, widgets, crossterm, react, components} + crossterm JNI. Several higher-level components started life inside typr.cli.app.components during this work and were lifted upstream; what's still local is CaretCell (typr-specific tree caret) and JatatuiInterop (a small Scala 3 SAM-conversion façade).

Screens (typr/cli/app/screens/):

Screen What it does
Splash logo + tagline + closed-beta status chip; 2s auto-dismiss after acceptance
BetaNotice full-screen terms modal — Initial gates the app, ReadOnly reached via t
MainMenu 6-card grid: Sources / Schemas / Outputs / Domain Types / Field Types / Generate
SourceList / Editor / Form / Wizard add / edit / delete sources, live form, real JDBC test-connection button, "used by N outputs:" with clickable links
OutputList / Editor / Wizard same shape for codegen outputs
TypeList / Editor / Wizard FieldType: pattern editor with live "preview matches" against cached MetaDb. DomainType: fields + aligned-source rows + live alignment matrix (✓ match / ⚠ missing / · extras)
SchemaPicker + Schema / Spec / Avro / Proto Browser per-source-kind entity browsers, fuzzy /, e extracts a field as a FieldType, c pushes the entity into DomainFromEntity
DomainFromEntity two-pane discovery — every entity grouped by source on the left, live preview of the would-be DomainType + alignment suggestions across other sources on the right
Generate per-source load badges + per-output ProgressTracker rows + log panel; daemon ticker keeps duration counters smooth

Runtime (typr/cli/app/):

  • AppApi carries config + sourceCache + loadStatus + updateConfig (writes back via ConfigWriter) + quit
  • LoadedSource is a sealed sum (Db / Spec / Avro / Proto) with one load(json, buildDir) codec dispatching off the typed ParsedSource — no 4× isXxx() reparsing
  • Shell forks one daemon thread per source on mount; screens render purely from sourceCache + loadStatus — no per-screen re-fetch
  • App.run opens the terminal in raw mode and runs a hand-rolled resize / key / mouse → renderer.dispatch loop

Closed-beta gate (typr/cli/beta/):

  • BetaTerms — display text + termsVersion (bumpable to force re-prompt) + expiresOn = 2026-07-01 + warningWindowDays = 14
  • BetaGate — XDG-aware acceptance file at $XDG_CONFIG_HOME/typr/beta-accepted.txt (with ~/.config/typr/... / %APPDATA%/typr/... fallbacks); queries isCurrent / daysUntilExpiry / isExpired / isInWarningWindow

Refactored Main.scala so there is exactly one path from Opts.subcommand to a runnable body — the private cmd(name, help, level) helper. Every verb picks a GateLevel:

Level Commands Behaviour
CliStrict generate / watch / check refuses if not accepted; refuses if expired
TuiOrInit tui / init expiry check only; acceptance handled inside the in-TUI modal
Informational terms bypasses both gates — users hitting the wall must still be able to read what they agreed to

Adding a new verb means picking a level. There is no overload that skips the gate.

Side effect: --accept is now uniform across every subcommand, so typr terms --accept reads + accepts in one step — a lovely first-run flow.


Also in this PR: build: bleep M10 + built-in publishing, scalafmt + -no-indent, coursier channel — mechanical-but-load-bearing housekeeping that the TUI work sits on top of.

bleep M3 → M10, custom publishing → built-in bleep publish:

  • deleted: Publish.scala, PublishLocal.scala, projectsToPublish.scala
  • new template-publishable template — groupId dev.typr, sonatypeProfileName com.olvind, MIT, single developer
  • all 11 publishable projects extend it via extends: lists: typr, typr-codegen, typr-dsl, typr-dsl-{scala,kotlin,anorm,doobie,zio-jdbc}, typr-runtime-{anorm,doobie,zio-jdbc}
  • bleep-plugin-ci-release swapped for bleep-core in typr-scripts (CompileBenchmark / GeneratedShowcase / GitOps still need bleep.* and ryddig.*)
  • verified: bleep publish local-ivy --dry-run produces 204 files with the correct POMs across the lot

-no-indent on template-scala-3:

  • compiler now enforces brace syntax for all Scala 3 code — no significant-indent drift between authors
  • a single scalac -no-indent -rewrite pass rewrote ~115 files automatically; one manual comma fix in OutputEditor.scala afterwards
  • scalafmt picks up the change via scala3-dialect overrides in .scalafmt.conf for typr / typr-codegen / typr-scripts / typr-scripts-sourcegen

coursier-channel.json at the repo root mirrors bleep's install pattern:

cs install --channel https://raw.githubusercontent.com/typr-dev/typr/main/coursier-channel.json typr

Resolves dev.typr:typr_3:latest.release directly from Maven Central — no separate release pipeline of our own.

Misc:

  • TypoLogger prefix typo:typr:
  • typr.yaml heavily reordered by ConfigWriter's sortKeys + dropNullKeys pass — semantically identical
  • .claude/scheduled_tasks.lock untracked + gitignored
  • small regenerated test-row tweaks under testers/.../product_summary and testers/.../inventory_check

Both commits compile independently — verified at 1dcb545e8 and at the tip.

Test plan

  • bleep --no-color compile is green at the tip
  • bleep publish local-ivy --dry-run --version 0.0.0-test produces 11 publishable projects' POMs with dev.typr groupId / MIT license
  • bleep run typr -- generate --accept runs end-to-end against a real schema
  • typr tui opens, navigates, save round-trips through ConfigWriter
  • typr terms prints the terms text + warning line if within 14 days of 2026-07-01
  • First-run typr generate without acceptance refuses with notAcceptedMessage; typr generate --accept accepts and runs
  • After 2026-07-01 (roll the system clock to test), every gated subcommand exits 78 with expiredMessage; typr terms still prints text (Informational level)
  • cs install --channel https://raw.githubusercontent.com/typr-dev/typr/main/coursier-channel.json typr resolves once the channel file lands on main

🤖 Generated with Claude Code

@oyvindberg oyvindberg changed the title typr: bleep M10 + built-in publishing + interactive TUI (jatatui) typr tui: interactive editor for typr.yaml (jatatui) + closed-beta gate May 17, 2026
oyvindberg and others added 2 commits May 18, 2026 09:47
…er channel

Everything on this branch that isn't the interactive TUI / closed-beta
work. Splits cleanly from the TUI delta in the next commit.

────────────────────────────────────────────────────────────────────────
bleep M3 → M10 + built-in publishing
────────────────────────────────────────────────────────────────────────

  - bleep.yaml: `\$version: 1.0.0-M3` → `1.0.0-M10`
  - Migrated from custom publish scripts to bleep's built-in `publish`
    subcommand:
      * removed `scripts.Publish` (used CiReleasePlugin + manual
        coursier Info / packageLibraries plumbing)
      * removed `scripts.PublishLocal` (manual `commands.publishLocal`)
      * removed `scripts.projectsToPublish` (filter list)
      * removed `build.bleep::bleep-plugin-ci-release` from
        typr-scripts deps (added `bleep-core` directly because
        CompileBenchmark / GeneratedShowcase / GitOps were resolving
        `bleep.*` and `ryddig.*` transitively through it)
      * removed `my-publish-local:` + `publish:` script entries
      * added `template-publishable` (groupId dev.typr,
        sonatypeProfileName com.olvind, MIT, single developer) and
        wired all 11 publishable projects (typr, typr-codegen,
        typr-dsl, typr-dsl-{scala,kotlin,anorm,doobie,zio-jdbc},
        typr-runtime-{anorm,doobie,zio-jdbc}) to extend it as a list
    Verified via `bleep publish local-ivy --dry-run`: 204 files, all
    POMs carry the correct groupId / description / url / license /
    developer.

Distribution stays via Maven Central. `coursier-channel.json` at the
repo root is a Coursier channel descriptor (same pattern bleep uses)
pinning `dev.typr:typr_3:latest.release`, mainClass `typr.cli.Main`,
`-XX:+UseG1GC`. Users install with:

  cs install --channel https://raw.githubusercontent.com/typr-dev/typr/main/coursier-channel.json typr

────────────────────────────────────────────────────────────────────────
scalafmt + `-no-indent`
────────────────────────────────────────────────────────────────────────

  - `.scalafmt.conf`: added scala3 dialect overrides for the scala-3
    source roots that weren't already covered — typr/src/scala,
    typr-codegen/src/scala, typr-scripts/src/scala,
    typr-scripts-sourcegen/src/scala. Without these, scalafmt's
    default `Scala213Source3` dialect chokes on `enum` / `given` /
    optional-braces syntax that the Scala 3 code uses.
  - `bleep.yaml`: added `-no-indent` to `template-scala-3` scala
    options so the compiler enforces brace syntax for all Scala 3
    code (no significant-indentation drift between authors).
  - Ran `scalac -no-indent -rewrite` once over the affected projects
    to auto-convert existing indent-style → brace-style; ~115 files
    changed. One manual fix afterwards: a match expression in
    `typr/cli/app/screens/OutputEditor.scala` lost a comma between a
    `} match` clause and the next named argument.

(The TUI source files included in the rewrite belong with the next
commit, not this one — they're added by the TUI commit on top of this
state.)

────────────────────────────────────────────────────────────────────────
Misc
────────────────────────────────────────────────────────────────────────

  - `TypoLogger.Console` now prefixes lines with `typr:` instead of
    the leftover `typo:`.
  - Site: getting-started rewritten around closed-beta access (drops
    placeholder install / quickstart, points at oyvind@typr.dev for
    seats); landing-page polish (announcement bar, mobile layout).
  - `typr.yaml` is heavily reordered by ConfigWriter's
    sortKeys + dropNullKeys pass — functionally identical.
  - `.gitignore` ignores `.claude/scheduled_tasks.lock`; that file is
    removed from the tracked tree.
  - Small regenerated test row tweaks under
    `testers/.../product_summary` and `testers/.../inventory_check`
    from a regen pass against the current schema.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The CLI shell already existed (generate / watch / check, decline +
cats-effect). This commit layers two new surfaces on top.

────────────────────────────────────────────────────────────────────────
1. Interactive TUI — `typr tui` (new app/ + screens/)
────────────────────────────────────────────────────────────────────────

A jatatui-based editor for `typr.yaml` so you can create / edit /
delete sources, outputs, and types without hand-writing the YAML.

Built on **jatatui** (`com.olvind.jatatui` 0.30.0) — a Java port of
tui-rs with a React-style component layer. Six artifacts wired into
the typr project: jatatui-{core,widgets,crossterm,react,components} +
crossterm (JNI). Several higher-level components started life inside
`typr.cli.app.components` and were lifted upstream during the work;
what's still local is `CaretCell` (typr-specific clickable tree caret)
and `JatatuiInterop` (Scala 3 doesn't auto-SAM-convert bound
`() => Unit` to `Runnable`, and `Boolean => Element` doesn't fit
`j.u.f.Function[j.l.Boolean, Element]` because of primitive boxing, so
we wrap with `Run.given` Conversion + a `Link.focusable` façade).

Entry point: `typr tui` → `App.run` opens the terminal in raw mode and
runs a hand-rolled loop (resize / key / mouse → renderer.dispatch).
`Shell` mounts a Router over an `AppApi` context that screens read via
`RenderContext.useContext`.

`AppApi` (typr/cli/app/AppApi.scala):
  - config: TyprConfig                       (parsed typr.yaml)
  - configPath: Path
  - updateConfig(f) — mutates + writes via ConfigWriter
  - sourceCache: Map[String, LoadedSource]   (one per source)
  - loadStatus:  Map[String, LoadStatus]     (NotLoaded / Loading /
                                              Loaded / Failed)
  - quit() — flips the running flag

`LoadedSource` (typr/cli/app/LoadedSource.scala) is a sealed sum
(`Db(MetaDb)` | `Spec(ParsedSpec)` | `Avro(List[AvroSchemaFile])` |
`Proto(List[ProtoFile])`) with one `load(json, buildDir)` codec that
dispatches off the typed `ParsedSource` — no 4× isXxx() checks
re-parsing the JSON.

Shell forks one daemon thread per source on mount; caches and statuses
fill in per-source as each load completes. Screens render purely from
`sourceCache` + `loadStatus` — no per-screen re-fetch.

Screens (typr/cli/app/screens/):

  Splash               logo + tagline; 2s auto-dismiss (only after
                       acceptance); chip with closed-beta status +
                       expiry; `t` re-opens BetaNotice (read-only)
  BetaNotice           full-screen terms modal; Initial gates the rest
                       of the app, ReadOnly is reached via `t`
  MainMenu             6-card grid: Sources / Schemas / Outputs /
                       Domain Types / Field Types / Generate
  SourceList /
  SourceEditor /
  SourceForm /
  SourceWizard         add/edit/delete sources; live form; "test
                       connection" button (real JDBC probe); "used by
                       N outputs:" with clickable links
  OutputList /
  OutputEditor /
  OutputWizard         add/edit/delete outputs
  TypeList /
  TypeEditor /
  TypeWizard           field types or domain types depending on kind;
                       FieldType editor: deep editor for api/db/model
                       match patterns + validation rules + "preview
                       matches" against cached MetaDb's;
                       DomainType editor: primary + description + fields
                       rows (add/delete/rename via FieldSpecObject) +
                       aligned-source rows + generate options, with
                       a live alignment matrix below (✓ match / ⚠
                       missing / · extras footer)
  SchemaPicker /
  SchemaBrowser /
  SpecBrowser /
  AvroBrowser /
  ProtoBrowser         per-source-kind browsers; fuzzy `/` search; `e`
                       extracts a field as a FieldType; `c` pushes
                       DomainFromEntity
  DomainFromEntity     two-pane discovery: left = every entity from
                       loaded sources grouped by source; right = live
                       preview of the would-be DomainType + alignment
                       suggestions across other sources
  FieldTypeForm /
  DomainTypeForm       create-new flows
  Generate             status + per-source load badges (left) +
                       per-output ProgressTracker rows (right) +
                       log panel; daemon ticker keeps duration counters
                       smooth

Utilities:
  ConnectionTest       opens a Hikari JDBC connection + reads metadata
  TypePreview          runs FieldType DbMatch patterns against a
                       cached MetaDb, returns matched columns
  EntityCatalog        uniform "record-shaped thing" across all four
                       source kinds; field-name normalisation
                       (snake↔camel) for cross-source alignment scoring

New subcommands wired in Main.scala:
  typr tui    — opens the editor
  typr init   — drops a starter typr.yaml + opens the TUI
  typr terms  — prints closed-beta terms

────────────────────────────────────────────────────────────────────────
2. Closed-beta acceptance gate + July 1, 2026 timebomb
────────────────────────────────────────────────────────────────────────

Typr launches as a partially paid product. Before any tier / pricing
details land in the binary, every first-run user sees a four-bullet
terms modal: (1) not open source; (2) commercial DBs won't stay free;
(3) a limit on how many boundaries take part in domain-type alignment
per generate run is coming; (4) this build expires 2026-07-01 — keep
yours fresh from typr.dev.

`typr.cli.beta`:
  - BetaTerms — display text, one-liner, termsVersion (bumpable to
    force re-prompt on material edits), expiresOn (2026-07-01),
    warningWindowDays (14)
  - BetaGate  — XDG-aware acceptance file at
    `$XDG_CONFIG_HOME/typr/beta-accepted.txt` (with
    `~/.config/typr/...` / `%APPDATA%/typr/...` fallbacks); plain-text
    accepted-at / binary-version / terms-version keys; queries
    isCurrent / daysUntilExpiry / isExpired / isInWarningWindow plus
    three pre-formatted user-facing messages

CLI gating (Main.scala):
  - Every subcommand takes `--accept`; passing it writes the
    acceptance file inline before running the command.
  - GateLevel.CliStrict (generate / watch / check) refuses when not
    accepted, with a hint pointing at `--accept` or `typr terms`.
  - GateLevel.TuiOrInit (tui / init) skips the CLI refusal — the user
    accepts inside the in-TUI modal instead.
  - GateLevel.Informational (terms) bypasses both gates so users can
    always read what they're agreeing to.
  - Expiry runs first regardless: post-2026-07-01 every command prints
    a stderr block pointing at https://typr.dev/get and exits 78.

Warning window: from 14 days before expiry, the CLI emits a logger.warn
countdown line (visible in CI logs). The TUI splash chip turns yellow
and counts down to the day.

────────────────────────────────────────────────────────────────────────
Known followups
────────────────────────────────────────────────────────────────────────

  - ConfigWriter loses untyped fields on round-trip (e.g. saga /
    state-machine YAML blocks). TUI edits that write typr.yaml will
    silently drop content not modelled by the generated boundary case
    classes. Either preserve unknown keys, or restrict TUI saves to
    the typed surface.
  - Tier-based codegen limits (per the pricing tiers) are out of
    scope here — replaced with the simpler "closed beta · these
    things will eventually be paid" gate.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@oyvindberg oyvindberg merged commit f8e69cd into main May 18, 2026
@oyvindberg oyvindberg deleted the tui-revival branch May 18, 2026 07:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant