halo DocsGitHub

Demo

Five widgets on one display: a top bar, a desktop clock, an app launcher strip, an OSD that hides itself, and a load-average sparkline (poll, computed, reactive lists) split into its own file.

Demo

Run it

swift run halo start --config examples/demo                         # from a clone
halo start --config "$(brew --prefix)/share/halo/examples/demo"   # Homebrew

Source

main.tsx

import { Box, Image, PerScreen, Surface, Text, type ScreenInfo } from "halo";
import { battery, clock } from "halo/services";
import { signal } from "halo/signals";
import { LoadGraph } from "@/demo/load";

const surface = "#1e1e2ee0";
const foreground = "#cdd6f4";
const muted = "#9399b2";
const accent = "#89b4fa";

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

function TopBar(props: { screen: ScreenInfo }) {
  return (
    <Surface
      screen={props.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>
  );
}

function DesktopClock(props: { screen: ScreenInfo }) {
  return (
    <Surface
      screen={props.screen}
      anchor={["bottom", "right"]}
      margins={[0, 24, 24, 0]}
      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("HH:mm")}
        </Text>
        <Text style={{ fontSize: 13, color: muted }}>{clock.format("EEEE, d MMMM")}</Text>
      </Box>
    </Surface>
  );
}

function Launcher(props: { screen: ScreenInfo }) {
  return (
    <Surface
      screen={props.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,
        }}
      >
        {["com.apple.finder", "com.apple.Safari", "com.apple.Terminal"].map((app) => (
          <Image app={app} size={40} />
        ))}
      </Box>
    </Surface>
  );
}

function Osd(props: { screen: ScreenInfo }) {
  const visible = signal(true);
  setTimeout(() => visible.set(false), 3000);
  return (
    <Surface
      screen={props.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>
  );
}

function Load(props: { screen: ScreenInfo }) {
  return (
    <Surface
      screen={props.screen}
      anchor={["top", "right"]}
      margins={[48, 8, 0, 0]}
      size={{ width: 220, height: 96 }}
      layer="aboveMenu"
    >
      <LoadGraph />
    </Surface>
  );
}

export default () => (
  <PerScreen>
    {(screen) => (
      <>
        <TopBar screen={screen} />
        <DesktopClock screen={screen} />
        <Launcher screen={screen} />
        <Osd screen={screen} />
        <Load screen={screen} />
      </>
    )}
  </PerScreen>
);

load.tsx

import { Box, Text, exec, poll } 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.
export 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>
  );
}