halo DocsGitHub

halo

Components

Box

function Box(props: BoxProps): NativeNode

A flexbox container. Strings among its children become <Text>.

For

function For<T>(props: ForProps<T>): Accessor<NativeNode[]>

A keyed list: only new keys render, removed keys are disposed (playing their exit), and kept ones are reused and moved. Use it inside a <Box>, instead of {() => items().map(...)}.

Image

function Image(props: ImageProps): NativeNode

An SF Symbol, an app icon or an image file.

Input

function Input(props: InputProps): NativeNode

A native single-line text field (IME, selection, clipboard). Needs an input="keyboard" surface.

PerScreen

function PerScreen(props: PerScreenProps): null

Renders its children once per display, and disposes an instance when its display goes away.

Shape

function Shape(props: ShapeProps): NativeNode

Vector paths: rings, graphs, icons. Strokes keep their width in points and stay inside the node.

Surface

function Surface(props: SurfaceProps): null

A top-level window. Its child is laid out to fill it.

Text

function Text(props: TextProps): NativeNode

Text, one line by default (see TextStyle.lines).

Functions

animate

function animate(node: NativeNode, tracks: KeyframeTracks, options?: KeyframeOptions): Animation

Plays keyframes on a node on top of its style, e.g. animate(icon, { rotate: [0, 360] }, { duration: 800, repeat: Infinity, curve: "linear" }). Cancelled automatically when the calling component is disposed.

exec

function exec(command: string, args?: string[]): Promise<ExecResult>

Runs a program with arguments (no shell) and resolves with its output.

focus

function focus(node: NativeNode): void

Moves keyboard focus into an <Input> (its surface must have focus).

hide

function hide(name: string): void

Hides every surface named name.

onFrame

function onFrame(callback: (time: number, dt: number) => boolean | void): () => void

Calls callback(time, dt) (milliseconds) on every display refresh until it returns false or its owner is disposed. For anything Core Animation can't express. Returns a function that stops it.

poll

function poll(command: string, intervalMs: number, initial?: string): Accessor<string>

Runs a shell command every intervalMs and exposes its trimmed stdout. Stops with its owner.

readFile

function readFile(path: string): Promise<string>

Reads a UTF-8 file. ~ is your home folder; relative paths start at the config folder. Rejects with the path and reason when it can't be read.

ring

function ring(): string

A full circle starting at the top, going clockwise: with style.strokeEnd (0–1) it's a progress ring. <Shape path={ring()} style={{ fill: "transparent", stroke: "#89b4fa", strokeWidth: 4, strokeEnd: 0.6 }} />

sh

function sh(command: string): Promise<ExecResult>

Runs a command with /bin/sh -c and resolves with its output.

show

function show(name: string): void

Shows every surface named name (like halo show <name>).

sparkline

function sparkline(values: number[], options?: SparklineOptions): string

A line graph of values, left to right, spanning the 100×100 viewBox. Use it with fit="stretch": <Shape path={() => sparkline(history())} fit="stretch" style={{ height: 24, fill: "transparent", stroke: "#a6e3a1" }} />

spawn

function spawn(command: string, args?: string[], options?: SpawnOptions): Spawned

Runs a long-lived program (no shell; use "/bin/sh", ["-c", command] for one) and streams its output line by line. Killed with its owner, along with anything it started.

toggle

function toggle(name: string): void

Shows or hides every surface named name.

tween

function tween(target: Accessor<number>, timing?: Timing): Accessor<number>

A number that follows target smoothly: each change animates from wherever the value currently is. Runs per frame in JS only while moving, e.g. const shown = tween(() => count(), { duration: 400 }).

watchFile

function watchFile(path: string): Accessor<string | undefined>

A file's contents as a signal, re-read whenever the file changes (including atomic saves and deletes). undefined until the first read and while the file doesn't exist. Stops with its owner.

writeFile

function writeFile(path: string, text: string): Promise<void>

Writes a UTF-8 file atomically, creating missing folders. Paths work like readFile's. Keep state outside the config folder (e.g. ~/.cache/…): writes there reload the config.

Values

screens

const screens: Accessor<ScreenInfo[]>

The connected displays, updated when displays change.

Interfaces

Animatable

interface Animatable

Every paint property that can animate.

PropertyTypeDescription
background?string
borderColor?string
borderWidth?number
opacity?number
radius?number
rotate?numberDegrees, clockwise.
scale?number
translateX?number
translateY?number

Animation

interface Animation

A running animate().

PropertyTypeDescription
finishedPromise<boolean>Resolves with true when it ran to the end, false when cancelled.
cancel()(): void

Attachment

interface Attachment

Where an attached surface (a popup) sits relative to a node.

PropertyTypeDescription
align?"start" | "center" | "end"
offset?number
side?"left" | "right" | "below" | "above"
toNativeNode | undefined

BoxProps

interface BoxProps extends Interactive

Props of <Box>.

PropertyTypeDescription
children?ElementNodes, strings, or functions returning them (re-rendered when what they read changes).
draggable?Reactive<{ files: string[] } | undefined>Drag these files out of the element (to Finder, Mail…), which moves or copies them as usual. Needs input="interactive".
enter?EnterTransitionPlayed once, when the node first appears: { from: { opacity: 0, translateY: 8 }, duration: 200 }.
exit?ExitTransitionPlayed when the node is removed (e.g. a list item going away); it leaves the layout right away.
onClick?(event: ClickEvent) => void
onDragEnter?(event: DropEvent) => voidFiles dragged over this element (from Finder or any app). Use it to highlight a drop zone.
onDragLeave?() => void
onDrop?(event: DropEvent) => voidFiles dropped here; you get their paths, nothing is copied. Drop-only elements let clicks through until files are being dragged, so an invisible strip along a screen edge stays out of the way. Needs a non-passthrough surface.
onHover?(hovered: boolean) => void
onScroll?(event: ScrollEvent) => void
ref?(node: NativeNode) => void
style?StyleProp<BoxStyle>

BoxStyle

interface BoxStyle

Layout and paint properties. Every value can be reactive.

PropertyTypeDescription
align?"start" | "center" | "end" | "stretch"
background?string
borderColor?string
borderWidth?number
bottom?Length
flexDirection?"row" | "column"
gap?number
grow?number
height?Length
justify?"start" | "center" | "end" | "space-between" | "space-around" | "space-evenly"
left?Length
opacity?number
origin?[x: number, y: number]Pivot for scale/rotate as fractions of the node's size; defaults to the center, [0.5, 0.5].
padding?Spacing
position?"relative" | "absolute""absolute" takes the node out of the row/column and places it over its siblings with top/right/bottom/left.
radius?number
right?Length
rotate?numberDegrees, clockwise.
scale?number
shrink?number
top?LengthOffset from the parent's top edge (absolute) or from the node's own place (relative). Points or "50%".
transition?TransitionsAnimate changes instead of jumping: { opacity: 150, scale: { duration: 300, curve: "spring" }, layout: 200 }.
translateX?number
translateY?number
width?Length

ClickEvent

interface ClickEvent

Passed to onClick. x/y are surface-local points.

PropertyTypeDescription
button"left" | "right"
clicksnumber
xnumber
ynumber

DropEvent

interface DropEvent

Passed to onDragEnter and onDrop: the dragged files' paths (nothing is copied) and the surface-local point.

PropertyTypeDescription
filesstring[]
xnumber
ynumber

ExecResult

interface ExecResult

What exec() and sh() resolve with.

PropertyTypeDescription
codenumber
stderrstring
stdoutstring

ForProps

interface ForProps

Props of <For>.

PropertyTypeDescription
children(item: Accessor<T>, index: Accessor<number>) => ElementCalled once per key. item and index update when that key's value or position changes.
eachReactive<readonly T[]>The list; usually a signal or computed.
key?(item: T, index: number) => unknownIdentifies an item across updates (an id); defaults to the item itself. Keys must be unique.

ImageProps

interface ImageProps extends Interactive

Props of <Image>: set one of symbol, app or file.

PropertyTypeDescription
app?Reactive<string>A bundle id ("com.apple.Safari") or an absolute .app path.
color?Reactive<string>Tint for SF Symbols.
draggable?Reactive<{ files: string[] } | undefined>Drag these files out of the element (to Finder, Mail…), which moves or copies them as usual. Needs input="interactive".
enter?EnterTransitionPlayed once, when the node first appears: { from: { opacity: 0, translateY: 8 }, duration: 200 }.
exit?ExitTransitionPlayed when the node is removed (e.g. a list item going away); it leaves the layout right away.
file?Reactive<string>An image file path; ~ is expanded.
onClick?(event: ClickEvent) => void
onDragEnter?(event: DropEvent) => voidFiles dragged over this element (from Finder or any app). Use it to highlight a drop zone.
onDragLeave?() => void
onDrop?(event: DropEvent) => voidFiles dropped here; you get their paths, nothing is copied. Drop-only elements let clicks through until files are being dragged, so an invisible strip along a screen edge stays out of the way. Needs a non-passthrough surface.
onHover?(hovered: boolean) => void
onScroll?(event: ScrollEvent) => void
ref?(node: NativeNode) => void
size?Reactive<number>Width and height in points. Default 16.
style?StyleProp<BoxStyle>
symbol?Reactive<string>An SF Symbol name, e.g. "battery.75percent".

InputProps

interface InputProps

Props of <Input>.

PropertyTypeDescription
autofocus?booleanTake focus whenever its surface does.
onInput?(value: string) => voidEvery edit, with the new text.
onKeyDown?(event: KeyEvent) => boolean | voidEvery editing key (Enter, Escape, arrows, Tab, Home/End…) with modifiers; return true to stop the default.
onSubmit?(value: string) => voidEnter, unless onKeyDown handled it.
placeholder?Reactive<string>
ref?(node: NativeNode) => void
style?StyleProp<TextStyle>
value?Reactive<string>The text; pass the signal you update in onInput.

Interactive

interface Interactive

Event props shared by Box, Text and Image. Events bubble to the closest ancestor listening for them.

PropertyTypeDescription
draggable?Reactive<{ files: string[] } | undefined>Drag these files out of the element (to Finder, Mail…), which moves or copies them as usual. Needs input="interactive".
enter?EnterTransitionPlayed once, when the node first appears: { from: { opacity: 0, translateY: 8 }, duration: 200 }.
exit?ExitTransitionPlayed when the node is removed (e.g. a list item going away); it leaves the layout right away.
onClick?(event: ClickEvent) => void
onDragEnter?(event: DropEvent) => voidFiles dragged over this element (from Finder or any app). Use it to highlight a drop zone.
onDragLeave?() => void
onDrop?(event: DropEvent) => voidFiles dropped here; you get their paths, nothing is copied. Drop-only elements let clicks through until files are being dragged, so an invisible strip along a screen edge stays out of the way. Needs a non-passthrough surface.
onHover?(hovered: boolean) => void
onScroll?(event: ScrollEvent) => void
ref?(node: NativeNode) => void

KeyEvent

interface KeyEvent

Passed to onKeyDown.

PropertyTypeDescription
altboolean
ctrlboolean
keystring"Enter", "Escape", "ArrowUp", "Tab", "Backspace", "Space"… or the typed character.
metaboolean
repeatboolean
shiftboolean

KeyframeOptions

interface KeyframeOptions

Timing for animate().

PropertyTypeDescription
alternate?booleanPlays back and forth.
curve?CurveCubic-bezier or named curves only; springs don't apply to keyframes.
delay?number
duration?number
fill?booleanKeep the last frame once finished instead of returning to the style.
offsets?number[]Position of each keyframe in 0…1 (defaults to evenly spaced).
repeat?numberNumber of runs; Infinity loops until cancelled.

NativeNode

interface NativeNode

A handle to a rendered element, from ref. Pass it to attach, focus() or animate().

PropertyTypeDescription
idnumber

PerScreenProps

interface PerScreenProps

Props of <PerScreen>.

PropertyTypeDescription
children(screen: ScreenInfo) => ElementCalled once per display; return its surfaces.

Rect

interface Rect

A rectangle in points, top-left origin.

PropertyTypeDescription
heightnumber
widthnumber
xnumber
ynumber

ScreenInfo

interface ScreenInfo

A connected display, as screens() and <PerScreen> report it.

PropertyTypeDescription
frameRect
idnumber
indexnumber1-based position in macOS's display list (AeroSpace's monitor-appkit-nsscreen-screens-id).
menuBarHeightnumber
namestring
notchRect | null
scalenumber

ScrollEvent

interface ScrollEvent

Passed to onScroll, in points.

PropertyTypeDescription
dxnumber
dynumber

ShapeProps

interface ShapeProps extends Interactive

Props of <Shape>. Give it a size (style.width/height), like a Box with no content.

PropertyTypeDescription
draggable?Reactive<{ files: string[] } | undefined>Drag these files out of the element (to Finder, Mail…), which moves or copies them as usual. Needs input="interactive".
enter?EnterTransitionPlayed once, when the node first appears: { from: { opacity: 0, translateY: 8 }, duration: 200 }.
exit?ExitTransitionPlayed when the node is removed (e.g. a list item going away); it leaves the layout right away.
fit?Reactive<"stretch" | "contain">"contain" (default) keeps proportions, centered; "stretch" fills each axis (graphs).
onClick?(event: ClickEvent) => void
onDragEnter?(event: DropEvent) => voidFiles dragged over this element (from Finder or any app). Use it to highlight a drop zone.
onDragLeave?() => void
onDrop?(event: DropEvent) => voidFiles dropped here; you get their paths, nothing is copied. Drop-only elements let clicks through until files are being dragged, so an invisible strip along a screen edge stays out of the way. Needs a non-passthrough surface.
onHover?(hovered: boolean) => void
onScroll?(event: ScrollEvent) => void
pathReactive<string>SVG path data ("M 0 0 L 100 100", arcs included), in viewBox coordinates. ring() and sparkline() make common ones.
ref?(node: NativeNode) => void
style?StyleProp<ShapeStyle>
viewBox?Reactive<[width: number, height: number]>The path's coordinate space. Default [100, 100].

ShapeStyle

interface ShapeStyle extends BoxStyle

Box properties plus shape paint. Shape paint animates with transition like any other property.

PropertyTypeDescription
align?"start" | "center" | "end" | "stretch"
background?string
borderColor?string
borderWidth?number
bottom?Length
fill?stringFill color; white by default, "transparent" for none.
flexDirection?"row" | "column"
gap?number
grow?number
height?Length
justify?"start" | "center" | "end" | "space-between" | "space-around" | "space-evenly"
left?Length
lineCap?"butt" | "round" | "square"
lineJoin?"round" | "miter" | "bevel"
opacity?number
origin?[x: number, y: number]Pivot for scale/rotate as fractions of the node's size; defaults to the center, [0.5, 0.5].
padding?Spacing
position?"relative" | "absolute""absolute" takes the node out of the row/column and places it over its siblings with top/right/bottom/left.
radius?number
right?Length
rotate?numberDegrees, clockwise.
scale?number
shrink?number
stroke?stringStroke color; none by default.
strokeEnd?number
strokeStart?numberWhere the stroke starts and ends along the path, 0–1. Animate strokeEnd for progress rings.
strokeWidth?numberIn points, whatever the shape's scale. Default 1.
top?LengthOffset from the parent's top edge (absolute) or from the node's own place (relative). Points or "50%".
track?stringColor of the whole path under the stroke: the empty part of a progress ring or bar.
transition?TransitionsAnimate changes instead of jumping: { opacity: 150, scale: { duration: 300, curve: "spring" }, layout: 200 }.
translateX?number
translateY?number
width?Length

SparklineOptions

interface SparklineOptions

Options for sparkline.

PropertyTypeDescription
area?booleanClose the line down to the baseline, for a filled area.
max?numberTop of the scale; defaults to the largest value.
min?numberBottom of the scale; defaults to the smallest value.

Spawned

interface Spawned

A running spawn.

PropertyTypeDescription
kill()(): voidStops the program and everything it started.

SpawnOptions

interface SpawnOptions

Callbacks for spawn. Exceptions in them are reported, never thrown into the host.

PropertyTypeDescription
onError?(line: string) => voidEach line of stderr. Unhandled stderr goes to the log as warnings.
onExit?(code: number) => voidThe exit code (negative for a signal; -1 if it couldn't start). Not called after kill().
onLine?(line: string) => voidEach line of stdout, without the newline.

SurfaceProps

interface SurfaceProps

How a surface is placed, stacked and made interactive.

PropertyTypeDescription
anchor?Reactive<Edge[]>
attach?Reactive<Attachment>Place next to a node of another surface instead of anchoring to the screen. Follows the node as it moves.
avoid?Reactive<("notch" | "menuBar")[]>
children?Element
defaultHidden?booleanStart hidden the first time this name appears (e.g. a launcher opened by a hotkey).
enter?EnterTransitionPlayed on the content every time the surface appears (including halo show).
exit?ExitTransitionPlayed before the surface hides; it stays on screen until the animation ends.
input?Reactive<"hover" | "passthrough" | "interactive" | "keyboard">
layer?Reactive<"desktop" | "belowMenu" | "aboveMenu" | "overlay">
margins?Reactive<Spacing>
name?stringTarget for halo show|hide|toggle <name>; shared by this widget's instance on every display.
onBlur?() => void
onClickOutside?() => voidA click anywhere outside this surface, in any app.
onFocus?() => void
onKeyDown?(event: KeyEvent) => boolean | void
screenReactive<number | ScreenInfo>
size?Reactive<{ height?: number | "fill" | "auto"; width?: number | "fill" | "auto" }>Per axis: points, "fill" (between both anchors), or "auto" (the content's size). Omitted axes are "fill" when anchored to both edges of that axis and "auto" otherwise.
visible?Reactive<boolean>

TextProps

interface TextProps extends Interactive

Props of <Text>.

PropertyTypeDescription
children?ElementStrings and numbers; only function children update: {() => WS ${ws()}}.
draggable?Reactive<{ files: string[] } | undefined>Drag these files out of the element (to Finder, Mail…), which moves or copies them as usual. Needs input="interactive".
enter?EnterTransitionPlayed once, when the node first appears: { from: { opacity: 0, translateY: 8 }, duration: 200 }.
exit?ExitTransitionPlayed when the node is removed (e.g. a list item going away); it leaves the layout right away.
onClick?(event: ClickEvent) => void
onDragEnter?(event: DropEvent) => voidFiles dragged over this element (from Finder or any app). Use it to highlight a drop zone.
onDragLeave?() => void
onDrop?(event: DropEvent) => voidFiles dropped here; you get their paths, nothing is copied. Drop-only elements let clicks through until files are being dragged, so an invisible strip along a screen edge stays out of the way. Needs a non-passthrough surface.
onHover?(hovered: boolean) => void
onScroll?(event: ScrollEvent) => void
ref?(node: NativeNode) => void
style?StyleProp<TextStyle>

TextStyle

interface TextStyle extends BoxStyle

Box properties plus text properties.

PropertyTypeDescription
align?"start" | "center" | "end" | "stretch"
background?string
borderColor?string
borderWidth?number
bottom?Length
color?string
flexDirection?"row" | "column"
fontFamily?string
fontSize?number
fontWeight?number | "bold" | "medium" | "regular" | "semibold"
gap?number
grow?number
height?Length
justify?"start" | "center" | "end" | "space-between" | "space-around" | "space-evenly"
left?Length
lines?numberLines before truncating; 1 by default, 0 for no limit. More than one wraps at the box's width.
opacity?number
origin?[x: number, y: number]Pivot for scale/rotate as fractions of the node's size; defaults to the center, [0.5, 0.5].
padding?Spacing
position?"relative" | "absolute""absolute" takes the node out of the row/column and places it over its siblings with top/right/bottom/left.
radius?number
right?Length
rotate?numberDegrees, clockwise.
scale?number
shrink?number
textAlign?"left" | "right" | "center"
top?LengthOffset from the parent's top edge (absolute) or from the node's own place (relative). Points or "50%".
transition?TransitionsAnimate changes instead of jumping: { opacity: 150, scale: { duration: 300, curve: "spring" }, layout: 200 }.
translateX?number
translateY?number
width?Length

Types

Curve

type Curve = "linear" | "ease-in" | "ease-out" | "ease-in-out" | "spring" | [x1: number, y1: number, x2: number, y2: number] | { damping?: number; mass?: number; stiffness?: number }

An easing curve: a name, cubic-bezier control points, or a spring. Springs ignore duration.

Edge

type Edge = "top" | "bottom" | "left" | "right"

A screen edge to anchor a surface to.

Element

type Element = NativeNode | string | number | boolean | null | undefined | Accessor<unknown> | Element[]

Anything a component or child slot can return.

EnterTransition

type EnterTransition = unknown

Played when a node or surface appears: animates from these values to its style.

PropertyTypeDescription
curve?Curve
delay?number
duration?number
fromAnimatable

ExitTransition

type ExitTransition = unknown

Played before a node or surface goes away: animates from its style to these values.

PropertyTypeDescription
curve?Curve
delay?number
duration?number
toAnimatable

KeyframeTracks

type KeyframeTracks = { [K in keyof Animatable]?: NonNullable<Animatable[K]>[] }

Values per property for animate(), e.g. { rotate: [0, 360], opacity: [1, 0.5, 1] }.

Reactive

type Reactive<T> = T | Accessor<T>

A value, or a function returning it that is re-read when the signals it reads change.

ShapeTransitionKey

type ShapeTransitionKey = "fill" | "stroke" | "track" | "strokeWidth" | "strokeStart" | "strokeEnd" | "path"

<Shape> properties that transition can animate (path morphs between paths with the same commands).

StyleProp

type StyleProp<S> = Reactive<{ [K in keyof S]?: Reactive<S[K]> }>

A style object (or a function returning one); each property can also be reactive on its own.

Timing

type Timing = number | { curve?: Curve; delay?: number; duration?: number }

Milliseconds, or the full form. Springs ignore duration and settle on their own.

Transitions

type Transitions = { [K in keyof Animatable | "all" | "layout" | ShapeTransitionKey]?: Timing }

style.transition: per property, plus all (any paint property) and layout (position/size changes).