Skip to content

Instantly share code, notes, and snippets.

@tjdevries
Created October 27, 2025 16:12
Show Gist options
  • Select an option

  • Save tjdevries/66ecc9c377e5b9277bdebac42b4f113b to your computer and use it in GitHub Desktop.

Select an option

Save tjdevries/66ecc9c377e5b9277bdebac42b4f113b to your computer and use it in GitHub Desktop.
import { TextAttributes } from "@opentui/core";
import { render, useKeyboard } from "@opentui/react";
import { Atom, Result, useAtom, useAtomValue } from "@effect-atom/atom-react";
import { Schema, Effect } from "effect";
class Coffee extends Schema.Class<Coffee>("Coffee")({
id: Schema.String,
name: Schema.String,
price: Schema.Number,
description: Schema.String,
}) {}
class CoffeeGroup extends Schema.Class<CoffeeGroup>("CoffeeGroup")({
name: Schema.String,
coffees: Schema.Array(Coffee),
}) {}
function DisplayCoffeeGroup(props: { group: CoffeeGroup; selected: string }) {
return (
<box width="100%" border={false} borderColor="green">
<text>{props.group.name}</text>
{props.group.coffees.map((coffee, index) => (
<box
key={index}
width="100%"
border={true}
borderColor={props.selected == coffee.id ? "red" : "green"}
>
<text>{coffee.name}</text>
</box>
))}
</box>
);
}
class CoffeeGroups extends Effect.Service<CoffeeGroups>()("app/CoffeeGroups", {
effect: Effect.gen(function* () {
const getAll = Effect.succeed([
CoffeeGroup.make({
name: "Featured",
coffees: [
Coffee.make({
id: "cron",
name: "Cron",
price: 10,
description: "A cron job",
}),
],
}),
CoffeeGroup.make({
name: "Originals",
coffees: [
Coffee.make({
id: "404",
name: "404",
price: 10,
description: "A 404 page",
}),
Coffee.make({
id: "object",
name: "[Object object]",
price: 10,
description: "An object",
}),
Coffee.make({
id: "segfault",
name: "Segfault",
price: 10,
description: "A segfault",
}),
],
}),
]);
return { getAll } as const;
}),
}) {}
const runtimeAtom: Atom.AtomRuntime<CoffeeGroups, never> = Atom.runtime(
CoffeeGroups.Default,
);
// You can then use the AtomRuntime to make Atom's that use the services from the Layer
const coffeeGroupsAtom = runtimeAtom.atom(
Effect.gen(function* () {
const coffeeGroups = yield* CoffeeGroups;
return yield* coffeeGroups.getAll;
}),
);
const coffeeGroupIdsAtom = Atom.make(
Effect.fn(function* (get: Atom.Context) {
const coffeeGroups = yield* get.result(coffeeGroupsAtom);
return coffeeGroups.flatMap((group) =>
group.coffees.map((coffee) => coffee.id),
);
}),
);
const currentCoffeeIdxAtom = Atom.make(0);
function App() {
const coffeeGroups = useAtomValue(coffeeGroupsAtom);
const [currentCoffeeIdx, setCurrentCoffeeIdx] = useAtom(currentCoffeeIdxAtom);
useKeyboard((key) => {
const groups = Result.getOrElse(coffeeGroups, () => {
return [];
});
console.log(groups.length);
setCurrentCoffeeIdx((prev) => {
if (key.name == "up") {
return (prev + 1) % groups.length;
} else if (key.name == "down") {
return (((prev - 1) % groups.length) + groups.length) % groups.length;
}
return prev;
});
});
return (
<box alignItems="flex-start" justifyContent="flex-start" flexGrow={1}>
{Result.match(coffeeGroups, {
onInitial() {
return <text>Loading...</text>;
},
onSuccess(groups) {
const coffeeIds = groups.value.flatMap((group) =>
group.coffees.map((coffee) => coffee.id),
);
return groups.value.map((group, index) => (
<box key={index} width="25%" border={true} borderColor="green">
<text>
`${group.name}: ${currentCoffeeIdx} / ${coffeeIds.length}{" "}
coffees`
</text>
<DisplayCoffeeGroup
group={group}
selected={coffeeIds[currentCoffeeIdx % coffeeIds.length]}
/>
</box>
));
},
onFailure(error) {
return <text>{`${error}`}</text>;
},
})}
</box>
);
}
render(<App />);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment