Primitives / Events

The entries

keyboardEvents(), pointerEvents(), scrollEvents(), and activeElementEvents() are entries, not bundles: the parent owns whatever key, cursor, scroll, or focus state it keeps. Lift one with Subscription.persistentEntry, mapping into the parent's Message:

ts
import { Stream } from 'effect'
import * as Subscription from 'foldkit/subscription'
import { keyboardEvents, matchHotkey } from 'foldkit-primitives/events'

const subscriptions = Subscription.make<EditorModel, EditorMessage>()(() => ({
  keys: Subscription.persistentEntry(
    keyboardEvents({ preventDefault: press => matchHotkey('ctrl+s', press) }).pipe(
      Stream.filter(event => event._tag === 'Pressed' && matchHotkey('ctrl+s', event)),
      Stream.map(() => EditorMessage.SaveRequested()),
    ),
  ),
}))

They report presses (with repeat and modifiers) and releases, moves { x, y }, scroll positions (in the capture phase, so a container's scroll reports too), and focus as { tag, id }: an element crosses as its tag and id, never as a live node. Without a window each stream is empty rather than throwing.

matchHotkey('ctrl+shift+k', press) answers whether a press is that chord, so update stays a table of chords. Matching is exact, with Mac aliases, and an auto-repeat never matches.

keyboardEvents({ preventDefault }) cancels the default of the presses the predicate picks (the arrows scrolling a game's page, a browser's own ctrl+s), decided inside the listener, because a default can only be cancelled while the event dispatches. The predicate reads the press, not the DOM event.

When the observed target depends on state, scope the entry through subscription dependencies and it restreams on change.

Failure

Without a window every stream is empty and every init a safe default.