halo
Components
Box
function Box(props: BoxProps): NativeNodeA 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): NativeNodeAn SF Symbol, an app icon or an image file.
Input
function Input(props: InputProps): NativeNodeA native single-line text field (IME, selection, clipboard). Needs an input="keyboard" surface.
PerScreen
function PerScreen(props: PerScreenProps): nullRenders its children once per display, and disposes an instance when its display goes away.
Shape
function Shape(props: ShapeProps): NativeNodeVector paths: rings, graphs, icons. Strokes keep their width in points and stay inside the node.
Surface
function Surface(props: SurfaceProps): nullA top-level window. Its child is laid out to fill it.
Text
function Text(props: TextProps): NativeNodeText, one line by default (see TextStyle.lines).
Functions
animate
function animate(node: NativeNode, tracks: KeyframeTracks, options?: KeyframeOptions): AnimationPlays 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): voidMoves keyboard focus into an <Input> (its surface must have focus).
hide
function hide(name: string): voidHides every surface named name.
onFrame
function onFrame(callback: (time: number, dt: number) => boolean | void): () => voidCalls 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(): stringA 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): voidShows every surface named name (like halo show <name>).
sparkline
function sparkline(values: number[], options?: SparklineOptions): stringA 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): SpawnedRuns 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): voidShows 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 AnimatableEvery paint property that can animate.
| Property | Type | Description |
|---|---|---|
background? | string | |
borderColor? | string | |
borderWidth? | number | |
opacity? | number | |
radius? | number | |
rotate? | number | Degrees, clockwise. |
scale? | number | |
translateX? | number | |
translateY? | number |
Animation
interface AnimationA running animate().
| Property | Type | Description |
|---|---|---|
finished | Promise<boolean> | Resolves with true when it ran to the end, false when cancelled. |
cancel() | (): void |
Attachment
interface AttachmentWhere an attached surface (a popup) sits relative to a node.
| Property | Type | Description |
|---|---|---|
align? | "start" | "center" | "end" | |
offset? | number | |
side? | "left" | "right" | "below" | "above" | |
to | NativeNode | undefined |
BoxProps
interface BoxProps extends InteractiveProps of <Box>.
| Property | Type | Description |
|---|---|---|
children? | Element | Nodes, 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? | EnterTransition | Played once, when the node first appears: { from: { opacity: 0, translateY: 8 }, duration: 200 }. |
exit? | ExitTransition | Played 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) => void | Files dragged over this element (from Finder or any app). Use it to highlight a drop zone. |
onDragLeave? | () => void | |
onDrop? | (event: DropEvent) => void | Files 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 BoxStyleLayout and paint properties. Every value can be reactive.
| Property | Type | Description |
|---|---|---|
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? | number | Degrees, clockwise. |
scale? | number | |
shrink? | number | |
top? | Length | Offset from the parent's top edge (absolute) or from the node's own place (relative). Points or "50%". |
transition? | Transitions | Animate changes instead of jumping: { opacity: 150, scale: { duration: 300, curve: "spring" }, layout: 200 }. |
translateX? | number | |
translateY? | number | |
width? | Length |
ClickEvent
interface ClickEventPassed to onClick. x/y are surface-local points.
| Property | Type | Description |
|---|---|---|
button | "left" | "right" | |
clicks | number | |
x | number | |
y | number |
DropEvent
interface DropEventPassed to onDragEnter and onDrop: the dragged files' paths (nothing is copied) and the surface-local point.
| Property | Type | Description |
|---|---|---|
files | string[] | |
x | number | |
y | number |
ExecResult
interface ExecResultWhat exec() and sh() resolve with.
| Property | Type | Description |
|---|---|---|
code | number | |
stderr | string | |
stdout | string |
ForProps
interface ForPropsProps of <For>.
| Property | Type | Description |
|---|---|---|
children | (item: Accessor<T>, index: Accessor<number>) => Element | Called once per key. item and index update when that key's value or position changes. |
each | Reactive<readonly T[]> | The list; usually a signal or computed. |
key? | (item: T, index: number) => unknown | Identifies an item across updates (an id); defaults to the item itself. Keys must be unique. |
ImageProps
interface ImageProps extends InteractiveProps of <Image>: set one of symbol, app or file.
| Property | Type | Description |
|---|---|---|
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? | EnterTransition | Played once, when the node first appears: { from: { opacity: 0, translateY: 8 }, duration: 200 }. |
exit? | ExitTransition | Played 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) => void | Files dragged over this element (from Finder or any app). Use it to highlight a drop zone. |
onDragLeave? | () => void | |
onDrop? | (event: DropEvent) => void | Files 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 InputPropsProps of <Input>.
| Property | Type | Description |
|---|---|---|
autofocus? | boolean | Take focus whenever its surface does. |
onInput? | (value: string) => void | Every edit, with the new text. |
onKeyDown? | (event: KeyEvent) => boolean | void | Every editing key (Enter, Escape, arrows, Tab, Home/End…) with modifiers; return true to stop the default. |
onSubmit? | (value: string) => void | Enter, 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 InteractiveEvent props shared by Box, Text and Image. Events bubble to the closest ancestor listening for them.
| Property | Type | Description |
|---|---|---|
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? | EnterTransition | Played once, when the node first appears: { from: { opacity: 0, translateY: 8 }, duration: 200 }. |
exit? | ExitTransition | Played 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) => void | Files dragged over this element (from Finder or any app). Use it to highlight a drop zone. |
onDragLeave? | () => void | |
onDrop? | (event: DropEvent) => void | Files 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 KeyEventPassed to onKeyDown.
| Property | Type | Description |
|---|---|---|
alt | boolean | |
ctrl | boolean | |
key | string | "Enter", "Escape", "ArrowUp", "Tab", "Backspace", "Space"… or the typed character. |
meta | boolean | |
repeat | boolean | |
shift | boolean |
KeyframeOptions
interface KeyframeOptionsTiming for animate().
| Property | Type | Description |
|---|---|---|
alternate? | boolean | Plays back and forth. |
curve? | Curve | Cubic-bezier or named curves only; springs don't apply to keyframes. |
delay? | number | |
duration? | number | |
fill? | boolean | Keep 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? | number | Number of runs; Infinity loops until cancelled. |
NativeNode
interface NativeNodeA handle to a rendered element, from ref. Pass it to attach, focus() or animate().
| Property | Type | Description |
|---|---|---|
id | number |
PerScreenProps
interface PerScreenPropsProps of <PerScreen>.
| Property | Type | Description |
|---|---|---|
children | (screen: ScreenInfo) => Element | Called once per display; return its surfaces. |
Rect
interface RectA rectangle in points, top-left origin.
| Property | Type | Description |
|---|---|---|
height | number | |
width | number | |
x | number | |
y | number |
ScreenInfo
interface ScreenInfoA connected display, as screens() and <PerScreen> report it.
| Property | Type | Description |
|---|---|---|
frame | Rect | |
id | number | |
index | number | 1-based position in macOS's display list (AeroSpace's monitor-appkit-nsscreen-screens-id). |
menuBarHeight | number | |
name | string | |
notch | Rect | null | |
scale | number |
ScrollEvent
interface ScrollEventPassed to onScroll, in points.
| Property | Type | Description |
|---|---|---|
dx | number | |
dy | number |
ShapeProps
interface ShapeProps extends InteractiveProps of <Shape>. Give it a size (style.width/height), like a Box with no content.
| Property | Type | Description |
|---|---|---|
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? | EnterTransition | Played once, when the node first appears: { from: { opacity: 0, translateY: 8 }, duration: 200 }. |
exit? | ExitTransition | Played 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) => void | Files dragged over this element (from Finder or any app). Use it to highlight a drop zone. |
onDragLeave? | () => void | |
onDrop? | (event: DropEvent) => void | Files 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 | |
path | Reactive<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 BoxStyleBox properties plus shape paint. Shape paint animates with transition like any other property.
| Property | Type | Description |
|---|---|---|
align? | "start" | "center" | "end" | "stretch" | |
background? | string | |
borderColor? | string | |
borderWidth? | number | |
bottom? | Length | |
fill? | string | Fill 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? | number | Degrees, clockwise. |
scale? | number | |
shrink? | number | |
stroke? | string | Stroke color; none by default. |
strokeEnd? | number | |
strokeStart? | number | Where the stroke starts and ends along the path, 0–1. Animate strokeEnd for progress rings. |
strokeWidth? | number | In points, whatever the shape's scale. Default 1. |
top? | Length | Offset from the parent's top edge (absolute) or from the node's own place (relative). Points or "50%". |
track? | string | Color of the whole path under the stroke: the empty part of a progress ring or bar. |
transition? | Transitions | Animate changes instead of jumping: { opacity: 150, scale: { duration: 300, curve: "spring" }, layout: 200 }. |
translateX? | number | |
translateY? | number | |
width? | Length |
SparklineOptions
interface SparklineOptionsOptions for sparkline.
| Property | Type | Description |
|---|---|---|
area? | boolean | Close the line down to the baseline, for a filled area. |
max? | number | Top of the scale; defaults to the largest value. |
min? | number | Bottom of the scale; defaults to the smallest value. |
Spawned
interface SpawnedA running spawn.
| Property | Type | Description |
|---|---|---|
kill() | (): void | Stops the program and everything it started. |
SpawnOptions
interface SpawnOptionsCallbacks for spawn. Exceptions in them are reported, never thrown into the host.
| Property | Type | Description |
|---|---|---|
onError? | (line: string) => void | Each line of stderr. Unhandled stderr goes to the log as warnings. |
onExit? | (code: number) => void | The exit code (negative for a signal; -1 if it couldn't start). Not called after kill(). |
onLine? | (line: string) => void | Each line of stdout, without the newline. |
SurfaceProps
interface SurfacePropsHow a surface is placed, stacked and made interactive.
| Property | Type | Description |
|---|---|---|
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? | boolean | Start hidden the first time this name appears (e.g. a launcher opened by a hotkey). |
enter? | EnterTransition | Played on the content every time the surface appears (including halo show). |
exit? | ExitTransition | Played 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? | string | Target for halo show|hide|toggle <name>; shared by this widget's instance on every display. |
onBlur? | () => void | |
onClickOutside? | () => void | A click anywhere outside this surface, in any app. |
onFocus? | () => void | |
onKeyDown? | (event: KeyEvent) => boolean | void | |
screen | Reactive<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 InteractiveProps of <Text>.
| Property | Type | Description |
|---|---|---|
children? | Element | Strings 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? | EnterTransition | Played once, when the node first appears: { from: { opacity: 0, translateY: 8 }, duration: 200 }. |
exit? | ExitTransition | Played 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) => void | Files dragged over this element (from Finder or any app). Use it to highlight a drop zone. |
onDragLeave? | () => void | |
onDrop? | (event: DropEvent) => void | Files 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 BoxStyleBox properties plus text properties.
| Property | Type | Description |
|---|---|---|
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? | number | Lines 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? | number | Degrees, clockwise. |
scale? | number | |
shrink? | number | |
textAlign? | "left" | "right" | "center" | |
top? | Length | Offset from the parent's top edge (absolute) or from the node's own place (relative). Points or "50%". |
transition? | Transitions | Animate 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 = unknownPlayed when a node or surface appears: animates from these values to its style.
| Property | Type | Description |
|---|---|---|
curve? | Curve | |
delay? | number | |
duration? | number | |
from | Animatable |
ExitTransition
type ExitTransition = unknownPlayed before a node or surface goes away: animates from its style to these values.
| Property | Type | Description |
|---|---|---|
curve? | Curve | |
delay? | number | |
duration? | number | |
to | Animatable |
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).