Skip to content

Instantly share code, notes, and snippets.

@thegedge
Last active November 3, 2025 16:20
Show Gist options
  • Select an option

  • Save thegedge/491768543f5b7bffa2f51ee6b59b3db2 to your computer and use it in GitHub Desktop.

Select an option

Save thegedge/491768543f5b7bffa2f51ee6b59b3db2 to your computer and use it in GitHub Desktop.
Generator-based TUI exploration
const Root = () => Row(
Logo,
Column(
Row(Spinner, "Loading..."),
"Press q to quit",
),
);
export const Row = (...children: Element[]) => {
return styled({ display: "flex", flexDirection: "row", gap: 1 }, children);
};
export const Column = (...children: Element[]) => {
return styled({ display: "flex", flexDirection: "column", gap: 1 }, children);
};
// `styled` is the way we can inform the renderer of layout things, allowing components to remain more "pure"
//
// If you did want to pass in "props" to a component, just add more arguments!
// You're in control, because this is just regular JavaScript.
const Spinner = async function* (frames = DEFAULT_FRAMES) {
for (let frameIndex = 0; ; frameIndex = ++frameIndex % frames.length) {
yield frames[frameIndex];
await new Promise((resolve) => setTimeout(resolve, 100));
}
};
// No "re-render", so all the vars exist for the lifetime of the component
// (i.e., no need for `useState` / `useMemo`)
export const Spinner = (props: { frames?: string[] }) => {
const frames = props.frames ?? DEFAULT_FRAMES;
const [frameIndex, setFrameIndex] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setFrameIndex((frameIndex + 1) % frames.length);
}, 100);
return () => clearInterval(interval);
});
return <>frames[frameIndex]</>;
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment