scrowl/src/ui/Digith/Signal/SignalDigith.tsx
2026-04-06 20:34:20 +02:00

153 lines
4.1 KiB
TypeScript

import { createEffect, createSignal, onCleanup, Show } from "solid-js";
import { Digith } from "src/ui/Digith";
import { useProgram } from "src/ui/ProgramProvider";
import { CodeEditor } from "src/ui/Component/CodeEditor";
import { sourceText } from "source-region";
import { Program } from "src/lang/program";
import { V, Validation, letValidate } from "src/ui/validation";
import { validateSignalExprRaw } from "src/ui/validation/helpers";
import { updateDigith } from "src/ui/Scrowl/scrowlStore";
import { DigithError } from "src/ui/Digith/DigithError";
import { Value } from "src/lang/eval/value";
import { Val } from "src/ui/Component/Value";
import { externalSubscribe } from "src/lang/eval/signalValue";
type Input = {
raw_body: string,
}
const validator: Validation<Input, Program.UpdateSignal, DigithError> = letValidate(
(input) => ({
body: V.elseErr(validateSignalExprRaw(input.raw_body), err => ({
payload: { tag: "Parse", field: "body", err, src: sourceText(input.raw_body).fullRegion() },
ids: ["body"],
tags: ["footer"],
config: { title: "Signal Body" },
})),
}),
(fields, input) => {
return V.ok({
body: fields.body,
raw_body: input.raw_body
});
}
);
export function SignalDigith(props: { signal: Digith.Signal }) {
const program = useProgram();
const [value, setValue] = createSignal<Value | null>(null);
const [body, setBody] = createSignal(props.signal.raw_body);
const [errors, setErrors] = createSignal<DigithError[]>([]);
const isDirty = () =>
body() !== props.signal.raw_body;
createEffect(() => {
// TODO: Improve runtime error view
try {
const signal = Program.get_or_create_signal(program, props.signal.name);
// TODO: Not sure about this one. Setting a signal in `setValue` is discouraged.
setTimeout(() => setValue(signal.currentValue), 0);
const cancel = externalSubscribe(signal, (newValue) => {
setValue(newValue);
});
onCleanup(cancel);
} catch (e) {
// TODO: setErrors... but how? Shouldn't this be independent of `errors`?
console.log("Failed to link Signal: ", e);
}
});
function handleRedefine() {
setErrors([]);
const validRes = validator({ raw_body: body() });
if (validRes.tag === "errors") {
setErrors(validRes.errors);
return;
}
const updateData = validRes.value;
const progRes = Program.updateSignal(program, props.signal.name, {
body: updateData.body,
raw_body: updateData.raw_body
});
if (progRes.tag === "error") {
setErrors([{
payload: { tag: "Program", err: progRes.error },
ids: ["program"],
tags: ["footer"],
config: { title: "Update Failed" },
}]);
return;
}
// reloading the digith
updateDigith(props.signal.id, {
...props.signal,
raw_body: updateData.raw_body
});
}
// TODO
return (
<article>
<header>
<strong>Signal</strong>
{/* Dirty Indicator / Status */}
<div>
<Show when={isDirty()} fallback={<span style={{color: "var(--pico-muted-color)"}}>Synced</span>}>
<span style={{color: "var(--pico-primary)"}}> Unsaved Changes</span>
</Show>
</div>
</header>
<div>
Name
<input
type="text"
value={props.signal.name}
disabled
style={{ opacity: 0.7, cursor: "not-allowed" }}
/>
</div>
<label>Body</label>
<CodeEditor
value={body()}
onUpdate={setBody}
onRun={handleRedefine}
/>
<footer style={{ display: "flex", "align-items": "center", gap: "1rem" }}>
<button
onClick={handleRedefine}
disabled={!isDirty()}
>
Redefine
</button>
</footer>
<div style={{ "margin-top": "1rem" }}>
<DigithError.ByTag errors={errors()} tag="footer" />
</div>
<Show when={ value() } fallback={<div>Initializing...</div>}>
{(value) => (
<div>
<label>Current Value</label>
<Val value={ value() } />
</div>
)}
</Show>
</article>
);
}