Demo
Five widgets in one halo.json: a top bar, a desktop clock (defineWidget props: formats and corner), an app dock (apps from props), an OSD that hides itself, and a load-average sparkline (poll, computed, reactive lists); colors shared through theme.ts.

Run it
swift run halo start --config examples/demo # from a clone
halo start --config "$(brew --prefix)/share/halo/examples/demo" # Homebrew
Source
halo.json12 linesGitHub
{
"widgets": {
"top-bar": { "widget": "top-bar" },
"clock": { "widget": "clock", "props": { "date": "EEE d MMMM" } },
"dock": {
"widget": "dock",
"props": { "apps": ["com.apple.finder", "com.apple.Safari", "com.apple.Terminal"] }
},
"osd": { "widget": "osd" },
"load": { "widget": "load" }
}
}
theme.ts4 linesGitHub
export const surface = "#1e1e2ee0";
export const foreground = "#cdd6f4";
export const muted = "#9399b2";
export const accent = "#89b4fa";
widgets/clock.tsx47 linesGitHub
import { Box, Surface, Text, defineWidget, type Edge } from "halo";
import { clock } from "halo/services";
import { foreground, muted, surface } from "@/demo/theme";
const corners: Record<string, Edge[]> = {
"top-left": ["top", "left"],
"top-right": ["top", "right"],
"bottom-left": ["bottom", "left"],
"bottom-right": ["bottom", "right"],
};
export default defineWidget({
props: {
time: { type: "string", default: "HH:mm", label: "Time format" },
date: { type: "string", default: "EEEE, d MMMM", label: "Date format" },
corner: {
type: "select",
options: ["top-left", "top-right", "bottom-left", "bottom-right"],
default: "bottom-right",
label: "Corner",
},
},
render: ({ screen, props }) => (
<Surface
screen={screen}
anchor={corners[props.corner]}
margins={24}
size={{ width: 240, height: 96 }}
layer="desktop"
>
<Box
style={{
justify: "center",
padding: [0, 18],
gap: 2,
background: surface,
radius: 16,
}}
>
<Text style={{ fontSize: 40, fontWeight: "semibold", color: foreground }}>
{clock.format(props.time)}
</Text>
<Text style={{ fontSize: 13, color: muted }}>{clock.format(props.date)}</Text>
</Box>
</Surface>
),
});
widgets/dock.tsx28 linesGitHub
import { Box, Image, Surface, type WidgetProps } from "halo";
import { surface } from "@/demo/theme";
export default function Dock({ screen, props }: WidgetProps<{ apps?: string[] }>) {
return (
<Surface
screen={screen}
anchor={["left"]}
margins={[0, 0, 0, 8]}
size={{ width: 56, height: 164 }}
layer="aboveMenu"
>
<Box
style={{
justify: "center",
align: "center",
gap: 8,
background: surface,
radius: 14,
}}
>
{(props.apps ?? []).map((app) => (
<Image app={app} size={40} />
))}
</Box>
</Surface>
);
}
widgets/load.tsx71 linesGitHub
import { Box, Surface, Text, exec, poll, type WidgetProps } from "halo";
import { computed, signal, untrack } from "halo/signals";
const SAMPLES = 30;
const GRAPH_HEIGHT = 36;
// 1-minute load average as a number, a color that follows it, and a rolling sparkline.
function LoadGraph() {
const raw = poll("sysctl -n vm.loadavg", 2000, "{ 0 0 0 }");
const load = computed(
() => Number(raw().replace(/[{}]/g, "").trim().split(/\s+/)[0]) || 0,
);
const cores = signal(1);
exec("sysctl", ["-n", "hw.ncpu"]).then((result) =>
cores.set(Number(result.stdout) || 1),
);
const history = signal<number[]>([]);
setInterval(
() => history.update((samples) => [...samples, untrack(load)].slice(-SAMPLES)),
2000,
);
const peak = computed(() => Math.max(1, ...history()));
const pressure = computed(() => load() / cores());
const color = computed(() =>
pressure() > 0.8 ? "#f38ba8" : pressure() > 0.5 ? "#f9e2af" : "#a6e3a1",
);
return (
<Box style={{ padding: [12, 14], gap: 10, background: "#1e1e2ee0", radius: 16 }}>
<Box style={{ flexDirection: "row", justify: "space-between", align: "center" }}>
<Text style={{ color: "#9399b2", fontSize: 11, fontWeight: "semibold" }}>
{() => `LOAD · ${cores()} CORES`}
</Text>
<Text style={{ color, fontSize: 20, fontWeight: "semibold" }}>
{() => load().toFixed(2)}
</Text>
</Box>
<Box style={{ flexDirection: "row", align: "end", gap: 2, height: GRAPH_HEIGHT }}>
{() =>
history().map((sample) => (
<Box
style={{
width: 5,
height: Math.max(2, (sample / peak()) * GRAPH_HEIGHT),
background: color(),
radius: 2,
}}
/>
))
}
</Box>
</Box>
);
}
export default function Load({ screen }: WidgetProps) {
return (
<Surface
screen={screen}
anchor={["top", "right"]}
margins={[48, 8, 0, 0]}
size={{ width: 220, height: 96 }}
layer="aboveMenu"
>
<LoadGraph />
</Surface>
);
}
widgets/osd.tsx29 linesGitHub
import { Box, Image, Surface, Text, type WidgetProps } from "halo";
import { signal } from "halo/signals";
import { foreground, muted, surface } from "@/demo/theme";
export default function Osd({ screen }: WidgetProps) {
const visible = signal(true);
setTimeout(() => visible.set(false), 3000);
return (
<Surface
screen={screen}
size={{ width: 180, height: 180 }}
layer="overlay"
visible={visible}
>
<Box
style={{
justify: "center",
align: "center",
gap: 14,
background: surface,
radius: 24,
}}
>
<Image symbol="speaker.wave.2.fill" size={56} color={foreground} />
<Text style={{ fontSize: 15, fontWeight: "medium", color: muted }}>halo</Text>
</Box>
</Surface>
);
}
widgets/top-bar.tsx65 linesGitHub
import { Box, Image, Surface, Text, type WidgetProps } from "halo";
import { battery, clock } from "halo/services";
import { accent, foreground, muted, surface } from "@/demo/theme";
function Workspace(props: { name: string; active?: boolean }) {
return (
<Box
style={{
padding: [2, 7],
radius: 5,
background: props.active ? accent : "transparent",
}}
>
<Text
style={{
fontSize: 12,
fontWeight: "semibold",
color: props.active ? "#1e1e2e" : muted,
}}
>
{props.name}
</Text>
</Box>
);
}
export default function TopBar({ screen }: WidgetProps) {
return (
<Surface
screen={screen}
anchor={["top", "left", "right"]}
margins={[4, 8, 0, 8]}
size={{ height: 32 }}
layer="belowMenu"
avoid={["menuBar", "notch"]}
>
<Box
style={{
flexDirection: "row",
justify: "space-between",
align: "center",
padding: [0, 10],
background: surface,
radius: 8,
}}
>
<Box style={{ flexDirection: "row", align: "center", gap: 6 }}>
<Image symbol="square.grid.2x2" size={14} color={accent} />
<Workspace name="1" active />
<Workspace name="2" />
<Workspace name="3" />
</Box>
<Box style={{ flexDirection: "row", align: "center", gap: 8 }}>
<Image symbol="battery.75percent" size={16} color={foreground} />
<Text style={{ fontSize: 13, fontWeight: "medium", color: foreground }}>
{() => `${battery.percent() ?? "–"}%`}
</Text>
<Text style={{ fontSize: 13, fontWeight: "medium", color: foreground }}>
{clock.format("EEE d MMM HH:mm")}
</Text>
</Box>
</Box>
</Surface>
);
}