diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index d696277..7c48adf 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -133,3 +133,18 @@ jobs: test -f home-smoke/.config/sandcode/sandcode.toml test -f home-smoke/.config/sandcode/.env + + - name: Run installed setup TUI smoke + run: | + cd e2e-install + mkdir -p logs + status=0 + timeout 5s script -qefc './node_modules/.bin/sandcode setup' logs/setup-tui.log || status=$? + if [ "$status" -ne 0 ] && [ "$status" -ne 124 ]; then + cat logs/setup-tui.log + exit "$status" + fi + + grep -q 'sandcode' logs/setup-tui.log + ! grep -q 'Orphan text error' logs/setup-tui.log + ! grep -q 'dispose is not a function' logs/setup-tui.log diff --git a/README.md b/README.md index ad5244a..de84706 100644 --- a/README.md +++ b/README.md @@ -86,6 +86,11 @@ sandcode setup --yes --vault-path ~/vaults/research --obsidian-integration headl `sandcode setup` uses an OpenTUI wizard by default when a TTY is available. +Keyboard controls: + +- `Esc` goes back one step +- `Ctrl+C` exits setup immediately + It writes: - `~/.config/sandcode/sandcode.toml` diff --git a/src/setup-ui.tsx b/src/setup-ui.tsx index 6c086fb..2c03808 100644 --- a/src/setup-ui.tsx +++ b/src/setup-ui.tsx @@ -1,6 +1,6 @@ import { createCliRenderer } from "@opentui/core"; import { render, useKeyboard } from "@opentui/solid"; -import { createMemo, createSignal, For, Show } from "solid-js"; +import { createMemo, createSignal, For } from "solid-js"; import { applySetupState, type SetupContext, @@ -343,7 +343,7 @@ function stateSnapshot(state: SetupState): string[] { return lines; } -function SetupWizard(props: { +export function SetupWizard(props: { context: SetupContext; initialState: SetupState; complete: (result: SetupResult) => void; @@ -539,102 +539,8 @@ function SetupWizard(props: { backgroundColor="#141a20" gap={1} > - - - - {(stepAccessor) => ( - - {stepAccessor().eyebrow} - - {stepAccessor().title} - - {stepAccessor().description} - {stepAccessor().hint} - { - if (!option) { - return; - } - setState((current) => { - const next = { ...current }; - stepAccessor().commit(next, option.value); - return next; - }); - }} - onSelect={(_index: number, option: WizardChoice | null) => { - if (!option) { - return; - } - commitAndAdvance(() => { - setState((current) => { - const next = { ...current }; - stepAccessor().commit(next, option.value); - return next; - }); - }); - }} - /> - - )} - - - - {(stepAccessor) => ( - - {stepAccessor().eyebrow} - - {stepAccessor().title} - - {stepAccessor().description} - {stepAccessor().hint} - - {state().syncTimeoutError} - - { - setState((current) => { - const next = { ...current }; - stepAccessor().commit(next, value); - return next; - }); - }} - onSubmit={(value: string) => { - const parsed = Number.parseInt(value.trim(), 10); - const shouldAdvance = - stepAccessor().key !== "sync-timeout" || - (Number.isInteger(parsed) && parsed > 0); - - setState((current) => { - const next = { ...current }; - stepAccessor().commit(next, value); - return next; - }); - - if (shouldAdvance) { - setStepIndex((current) => - getNextWizardStepIndex(current, steps().length), - ); - } - }} - /> - - )} - - - } - > + {phase() === "wizard" ? ( + activeStep().kind === "summary" ? ( Ready @@ -673,10 +579,101 @@ function SetupWizard(props: { }} /> - - - - + ) : ( + (() => { + const choiceStep = activeChoiceStep(); + if (choiceStep) { + return ( + + {choiceStep.eyebrow} + + {choiceStep.title} + + {choiceStep.description} + {choiceStep.hint} + { + if (!option) { + return; + } + setState((current) => { + const next = { ...current }; + choiceStep.commit(next, option.value); + return next; + }); + }} + onSelect={(_index: number, option: WizardChoice | null) => { + if (!option) { + return; + } + commitAndAdvance(() => { + setState((current) => { + const next = { ...current }; + choiceStep.commit(next, option.value); + return next; + }); + }); + }} + /> + + ); + } + + const inputStep = activeInputStep(); + if (inputStep) { + return ( + + {inputStep.eyebrow} + + {inputStep.title} + + {inputStep.description} + {inputStep.hint} + {inputStep.key === "sync-timeout" && state().syncTimeoutError ? ( + {state().syncTimeoutError} + ) : null} + { + setState((current) => { + const next = { ...current }; + inputStep.commit(next, value); + return next; + }); + }} + onSubmit={(value: string) => { + const parsed = Number.parseInt(value.trim(), 10); + const shouldAdvance = + inputStep.key !== "sync-timeout" || + (Number.isInteger(parsed) && parsed > 0); + + setState((current) => { + const next = { ...current }; + inputStep.commit(next, value); + return next; + }); + + if (shouldAdvance) { + setStepIndex((current) => + getNextWizardStepIndex(current, steps().length), + ); + } + }} + /> + + ); + } + + return null; + })() + ) + ) : phase() === "saving" ? ( Writing @@ -684,35 +681,40 @@ function SetupWizard(props: { Running validations and writing files. Stay on this screen. - - - + ) : phase() === "done" ? ( Complete Sandcode is configured. Press Enter or Esc to leave setup. - - {(saved) => ( + {(() => { + const savedResult = result(); + if (!savedResult) { + return null; + } + + return ( - Config: {saved().configPath} - - {(envPath) => Env: {envPath()}} - + Config: {savedResult.configPath} + {savedResult.envPath ? ( + Env: {savedResult.envPath} + ) : null} - )} - + ); + })()} - - - + ) : phase() === "error" ? ( Validation failed {errorMessage()} Press Enter or Esc to go back and edit the setup values. - + ) : null} + + + Esc goes back. Ctrl+C exits setup immediately. +