halo DocsGitHub

Services

Every built-in service: a status strip (front app, CPU, memory, network, volume, battery) and a volume OSD that appears on changes; scroll the volume chip to adjust it.

Services

Run it

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

Source

main.tsx

// Every built-in service: a status strip (front app, CPU, memory, network, volume, battery, plus Low Power Mode and
// throttling chips when they apply), the last wake from sleep, and a volume OSD that appears when the volume or
// mute changes. Scroll over the volume chip to change it.

import { Box, Image, PerScreen, Surface, Text, type ScreenInfo } from "halo";
import { audio, battery, frontApp, network, power, session, system } from "halo/services";
import { computed, effect, signal, untrack } from "halo/signals";

const card = "#1e1e2ef0";
const foreground = "#cdd6f4";
const muted = "#7f849c";
const accent = "#89b4fa";

const percent = (value: number | undefined) =>
  value === undefined ? "โ€“" : `${Math.round(value * 100)}%`;

function Chip(props: {
  icon: () => string;
  text: () => string;
  onScroll?: (dy: number) => void;
}) {
  return (
    <Box
      style={{ flexDirection: "row", align: "center", gap: 5 }}
      onScroll={props.onScroll && ((event) => props.onScroll?.(event.dy))}
    >
      <Image symbol={props.icon} size={13} color={foreground} />
      <Text style={{ fontSize: 12, fontWeight: "medium", color: foreground }}>
        {props.text}
      </Text>
    </Box>
  );
}

function batteryIcon() {
  const level = battery.percent() ?? 0;
  if (battery.charging()) return "battery.100.bolt";
  return level > 87
    ? "battery.100"
    : level > 62
      ? "battery.75"
      : level > 37
        ? "battery.50"
        : "battery.25";
}

function Status(props: { screen: ScreenInfo }) {
  return (
    <Surface
      screen={props.screen}
      anchor={["top", "right"]}
      margins={[48, 12, 0, 0]}
      layer="aboveMenu"
      input="interactive"
    >
      <Box
        style={{
          flexDirection: "row",
          align: "center",
          gap: 14,
          padding: [6, 12],
          background: card,
          radius: 10,
        }}
      >
        <Box style={{ flexDirection: "row", align: "center", gap: 6 }}>
          <Image app={() => frontApp.bundleId() ?? ""} size={16} />
          <Text style={{ fontSize: 12, fontWeight: "semibold", color: foreground }}>
            {() => frontApp.name() ?? ""}
          </Text>
        </Box>
        <Chip icon={() => "cpu"} text={() => percent(system.cpu())} />
        <Chip icon={() => "memorychip"} text={() => percent(system.memory())} />
        <Chip
          icon={() =>
            network.connected()
              ? network.type() === "ethernet"
                ? "cable.connector"
                : "wifi"
              : "wifi.slash"
          }
          text={() => network.type() ?? "โ€“"}
        />
        <Chip
          icon={() => (audio.muted() ? "speaker.slash.fill" : "speaker.wave.2.fill")}
          text={() => (audio.volume() === undefined ? "fixed" : percent(audio.volume()))}
          onScroll={(dy) => audio.setVolume((untrack(audio.volume) ?? 0) + dy * 0.01)}
        />
        {() =>
          battery.present() ? (
            <Chip icon={batteryIcon} text={() => `${battery.percent() ?? "โ€“"}%`} />
          ) : null
        }
        {() =>
          power.lowPowerMode() ? (
            <Chip icon={() => "leaf.fill"} text={() => "low power"} />
          ) : null
        }
        {() => {
          const thermal = power.thermal();
          return thermal && thermal !== "nominal" ? (
            <Chip icon={() => "thermometer.high"} text={() => thermal} />
          ) : null;
        }}
        {() => {
          const woke = session.lastWake();
          if (woke === undefined) return null;
          const time = new Date(woke);
          const pad = (n: number) => String(n).padStart(2, "0");
          return (
            <Chip
              icon={() => "sun.horizon"}
              text={() => `woke ${pad(time.getHours())}:${pad(time.getMinutes())}`}
            />
          );
        }}
      </Box>
    </Surface>
  );
}

function VolumeOsd(props: { screen: ScreenInfo }) {
  const visible = signal(false);
  const level = computed(() => (audio.muted() ? 0 : (audio.volume() ?? 0)));
  let first = true;
  let timer = 0;
  effect(() => {
    level();
    if (first) {
      first = false;
      return;
    }
    visible.set(true);
    clearTimeout(timer);
    timer = setTimeout(() => visible.set(false), 1500);
  });

  return (
    <Surface
      screen={props.screen}
      anchor={["bottom"]}
      margins={[0, 0, 80, 0]}
      layer="overlay"
      visible={visible}
      enter={{
        from: { opacity: 0, translateY: 16, scale: 0.94 },
        curve: { stiffness: 380, damping: 26 },
      }}
      exit={{ to: { opacity: 0, translateY: 10 }, duration: 160, curve: "ease-in" }}
    >
      <Box
        style={{
          flexDirection: "row",
          align: "center",
          gap: 12,
          padding: [14, 18],
          background: card,
          radius: 16,
        }}
      >
        <Image
          symbol={() => (audio.muted() ? "speaker.slash.fill" : "speaker.wave.3.fill")}
          size={20}
          color={foreground}
        />
        <Box style={{ width: 180, height: 6, radius: 3, background: "#ffffff1f" }}>
          <Box
            style={{
              height: 6,
              radius: 3,
              background: () => (audio.muted() ? muted : accent),
              width: () => `${Math.round(level() * 100)}%`,
              transition: { layout: { duration: 120 }, background: 120 },
            }}
          />
        </Box>
        <Text
          style={{
            fontSize: 13,
            fontWeight: "semibold",
            color: foreground,
            width: 36,
            textAlign: "right",
          }}
        >
          {() => percent(level())}
        </Text>
      </Box>
    </Surface>
  );
}

export default () => (
  <PerScreen>
    {(screen) => (
      <>
        <Status screen={screen} />
        <VolumeOsd screen={screen} />
      </>
    )}
  </PerScreen>
);