halo main (unreleased) · latest release docsGitHub

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.

Demo

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>
  );
}