Skip to content

The Hive's screen ​

Laboratory. The screen language exists to press on the language and to drive Quantum OS; it carries no stability promise and may change in any release (SUPPORT_TIERS.md).

A q:application type="screen" is what the Hive of Quantum OS shows on the machine's screen: a small pixel-art scene and, per event of the Hive, what happens in it. It compiles to screen.toml, a font sheet and the sprite sheets, which hive-screen (os/crates/hive-screen) loads: a daemon of a few hundred kilobytes that draws the scene on the console, lists the Hive's offers for an administrator to approve with the keyboard, and sends the approval back through the same socket the dashboard uses. No engine, no compositor, no GPU: the scene is drawn on the CPU and scaled up by a whole number to fill the display.

bash
quantum run screen.q --out projects/screen/screen   # writes screen.toml, font.png and the sheets
python scripts/generate-screen-config.py            # the OS's own: os/config/screen/screen.q -> os/config/screen/

The vocabulary of the scene is the games' (qg:): a sheet of frames, an animation, a tween with the same curves, the same particle presets, with the same meaning. What moves the scene are the Hive's events, the same ones the rules are on, with the same {field}s, filled in from the event itself. There is nothing to compute: no statements, no expressions, and text where a tag goes is a compile error.

The shape of a screen ​

xml
<q:application id="screen" type="screen">
  <qs:sheet name="cell" src="art/cells.png" tile="24">
    <qs:animation name="smoke" frames="2,3,4,5,6,2" fps="4" loop="false" />
  </qs:sheet>
  <qs:particles name="stars" preset="stars" />

  <qs:scene width="320" height="180" background="#1a1a2e">
    <qs:bar side="top" title="Quantum OS" left="{host}" right="{level} · {clock}" />
    <qs:sprite name="nginx" sheet="cell" frame="1" x="6" y="22" />
    <qs:text value="nginx" x="34" y="29" />
    <qs:offers title="Offers" x="188" y="18" width="126" rows="9" />
    <qs:notices title="Notices" x="6" y="132" width="308" rows="2" />
  </qs:scene>

  <qs:on event="service-crash">
    <qs:animate target="{service}" name="smoke" />     <!-- the sprite named after the service -->
    <qs:say text="{service} stopped (exit {exit_code})" />
  </qs:on>
  <qs:on event="done">
    <qs:frame target="{service}" frame="0" />
    <qs:emit particles="stars" at="{service}" dx="12" dy="12" />
  </qs:on>
</q:application>

A sprite's name is how a rule reaches it: {service} on service-crash is nginx, and the sprite named nginx smokes. An action whose target no sprite is named, or whose {field} the event does not provide, is left out, as the rules leave out an offer they cannot fill in. The rule on service-crash compiles to this, in screen.toml:

toml
[[rules]]
event = "service-crash"

[[rules.actions]]
kind = "animate"
target = "{service}"
name = "smoke"

[[rules.actions]]
kind = "say"
text = "{service} stopped (exit {exit_code})"
seconds = 4.0

The font ​

Every screen gets the same face: a 5x7 pixel font of our own, in 6x10 cells, with the printable ASCII characters and the Latin-1 letters (the accents of Portuguese, Spanish, French, German composed from the base letters). It is written as font.png next to the scene, so to the daemon it is one more sheet. A character the font lacks shows as a box.

Attribute types ​

TypeWhat it is
texta string; in a rule, {field} placeholders allowed
identa name (letters, digits, _ @ . : -)
inta whole number
floata number
booltrue or false
color#rgb or #rrggbb
tilea frame size in pixels: 24, or 16x24
framesframe numbers separated by commas
srca PNG next to the .q, or in a folder below it
enumone of the values listed

The tags ​

qs:sheet ​

A PNG of equal frames, numbered left to right and top to bottom from 0.

Goes inside <q:application>.

AttributeTypeDefaultWhat it is
name=identrequired
src=srcrequired
tile=tilerequiredframe size in pixels: 24, or 16x24

qs:animation ​

Frames of its sheet, cycled: what a qs:sprite plays from the start (animation=) or a rule starts (qs:animate).

Goes inside <qs:sheet>.

AttributeTypeDefaultWhat it is
name=identrequired
frames=framesrequired
fps=float8.0
loop=boolTruefalse: it plays once and stays on its last frame

qs:particles ​

A particle effect: a preset and a few overrides, as in a game. qs:emit plays it once where a sprite is; qs:emitter keeps it going in the scene.

Goes inside <q:application>.

AttributeTypeDefaultWhat it is
name=identrequired
preset=enum: puff, sparks, smoke, dust, fire, rain, snow, confetti, starsrequiredpuff, sparks, smoke, dust, confetti and stars play once; fire, rain and snow keep going
color=colorthe preset's own when not given
amount=inthow many particles
lifetime=floatseconds each one lives
speed=floatpixels a second, about
size=floatpixels, about

qs:scene ​

What is on the screen, in pixels of the scene: the daemon scales it up by a whole number to fill the display. One per file.

Goes inside <q:application>.

AttributeTypeDefaultWhat it is
width=int320
height=int180
background=color#000000

qs:sprite ​

A picture in the scene: one frame of a sheet, or an animation of it. Its name is how a rule finds it: a sprite named after a service is the one {service} reaches.

Goes inside <qs:scene>.

AttributeTypeDefaultWhat it is
name=ident
sheet=identrequired
frame=int0
x=intrequired
y=intrequired
animation=identan animation of its sheet, played from the start
hidden=boolFalsenot drawn until a qs:show

qs:text ​

Fixed text in the scene, in the screen's font. What an event says goes in a qs:say or the qs:notices.

Goes inside <qs:scene>.

AttributeTypeDefaultWhat it is
name=ident
value=textrequired
x=intrequired
y=intrequired
color=color#ffffff
align=enum: left, center, rightleftwhich side of the text x is

qs:emitter ​

A particle effect that keeps going in the scene: rain over it, a fire on a spot.

Goes inside <qs:scene>.

AttributeTypeDefaultWhat it is
particles=identrequired
x=intrequired
y=intrequired
width=int0for rain and snow, how wide a band they fall across
level=int1the presentation level it plays from: 1 whenever the effects are on, 2 only at the fine level, which the daemon affords with the cores

qs:offers ​

Where the Hive's offers are listed, one a row, for an administrator to approve: up and down move, Enter approves the one with the focus (a dangerous action asks for Enter twice), Escape dismisses it. At most one.

Goes inside <qs:scene>.

AttributeTypeDefaultWhat it is
x=intrequired
y=intrequired
width=intrequired
rows=int4
title=textgiven, the rows sit in a panel with a bevelled frame, a shadow and this title on its bar; x, y and width are then the panel's, and it is as tall as its rows
color=color#ffffff
focus=color#ffd34dthe row with the focus

qs:notices ​

The last notices, one a row, newest at the bottom. At most one.

Goes inside <qs:scene>.

AttributeTypeDefaultWhat it is
x=intrequired
y=intrequired
width=intrequired
rows=int3
title=textgiven, the rows sit in a panel with a bevelled frame, a shadow and this title on its bar; x, y and width are then the panel's, and it is as tall as its rows
color=color#c0c0c0

qs:bar ​

A bar across the scene, as a window manager's: at the top or at the bottom, a title in the accent at the left, a text after it and one at the right. The texts may hold the screen's own {field}s (below). The bar at the bottom shows what a qs:say says while it lasts. At most one a side.

Goes inside <qs:scene>.

AttributeTypeDefaultWhat it is
side=enum: top, bottomrequired
title=textat the left, in the accent
left=textafter the title
right=textagainst the right edge
color=color#8a8aa0
accent=color#ffd34dthe title's colour

The fields a bar's texts may hold, which the daemon fills in as it runs (a test's screen shows none of it, so its frames are the same on every machine):

FieldWhat it is
{clock}the time, HH:MM (--:-- until the daemon has it)
{level}the presentation level: plain, effects or fine
{cores}how many cores the machine gives the screen
{host}the machine's name
{offers}how many offers wait for an approval

qs:on ​

What happens in the scene when an event arrives: the actions inside, in order. One per event.

Goes inside <q:application>.

AttributeTypeDefaultWhat it is
event=enum: service-crash, log-anomaly, port-conflict, disk-low, memory-pressure, command-observed, command-not-found, notice, offer, request, done, failedrequired

qs:animate ​

Plays an animation of the sprite's sheet on it, from its first frame.

Goes inside <qs:on>.

AttributeTypeDefaultWhat it is
target=textrequireda qs:sprite's name, or a {field} that is one (a service name); nothing happens when no sprite has that name
name=identrequired

qs:frame ​

Shows one frame of the sprite's sheet on it, and stops whatever it was playing.

Goes inside <qs:on>.

AttributeTypeDefaultWhat it is
target=textrequireda qs:sprite's name, or a {field} that is one (a service name); nothing happens when no sprite has that name
frame=intrequired

qs:show ​

Shows or hides a sprite.

Goes inside <qs:on>.

AttributeTypeDefaultWhat it is
target=textrequireda qs:sprite's name, or a {field} that is one (a service name); nothing happens when no sprite has that name
visible=boolTrue

qs:tween ​

Eases a property of a sprite from what it is to a value, over some seconds, with the games' curves.

Goes inside <qs:on>.

AttributeTypeDefaultWhat it is
target=textrequireda qs:sprite's name, or a {field} that is one (a service name); nothing happens when no sprite has that name
property=enum: x, y, scale, alpharequiredx, y: where it is; scale: its size (1 is as drawn); alpha: how opaque (0 to 1)
to=floatrequired
from=floatwhere it starts (what it is now when not given)
seconds=float0.5
trans=enum: linear, sine, quad, cubic, back, bounce, elasticsinethe curve
ease=enum: in, out, in-outoutwhich end of the curve is slow
repeat=int0times it plays again (-1: for ever)
yoyo=boolFalseeach repeat goes back the other way

qs:emit ​

Plays a qs:particles effect once where a sprite is.

Goes inside <qs:on>.

AttributeTypeDefaultWhat it is
particles=identrequired
at=textrequireda qs:sprite's name, or a {field} that is one (a service name); nothing happens when no sprite has that name
dx=int0
dy=int0offset in pixels from the sprite's top left

qs:say ​

Shows a line of text at the bottom of the scene for a while: what happened, in words.

Goes inside <qs:on>.

AttributeTypeDefaultWhat it is
text=textrequired
seconds=float4.0

The events ​

What a qs:on can be on. The observations are the ones the rules name, as hive-cortex tells them to its clients, each with title, body and severity besides its own fields; the rest are the screen's own.

service-crash ​

A unit stopped unexpectedly (hive-nerve-journal saw it in the journal).

FieldTypeWhat it is
{unit}textthe unit, as systemd names it (nginx.service)
{service}textthe service name the actuator uses (nginx); absent when the unit is not a .service, and the offers that need it are left out
{exit_code}inthow it exited
{title}textthe notice, as hive-cortex titles it
{body}textwhat it says
{severity}textinfo, notice, warning, error or critical

log-anomaly ​

A unit logs an unusual number of errors.

FieldTypeWhat it is
{unit}textthe unit, as systemd names it
{service}textthe service name, as in service-crash
{title}textthe notice, as hive-cortex titles it
{body}textwhat it says
{severity}textinfo, notice, warning, error or critical

port-conflict ​

A program could not bind a port another one holds (hive-nerve-network).

FieldTypeWhat it is
{port}intthe port
{requester}textthe program that failed to bind
{holder}textthe program that holds the port
{title}textthe notice, as hive-cortex titles it
{body}textwhat it says
{severity}textinfo, notice, warning, error or critical

disk-low ​

A filesystem is nearly full.

FieldTypeWhat it is
{mount}textthe mount point
{percent}inthow full, in percent
{free_mb}intmegabytes free
{title}textthe notice, as hive-cortex titles it
{body}textwhat it says
{severity}textinfo, notice, warning, error or critical

memory-pressure ​

The system is running low on memory.

FieldTypeWhat it is
{free_mb}intmegabytes available
{total_mb}intmegabytes in all
{title}textthe notice, as hive-cortex titles it
{body}textwhat it says
{severity}textinfo, notice, warning, error or critical

command-observed ​

A user ran a command the Hive judges dangerous (hive-nerve-process, from the audit log).

FieldTypeWhat it is
{cmd}textthe command line
{program}textthe program
{title}textthe notice, as hive-cortex titles it
{body}textwhat it says
{severity}textinfo, notice, warning, error or critical

command-not-found ​

A user ran a program that is not installed, and a package of that name exists (grove).

FieldTypeWhat it is
{command}textwhat the user typed
{package}textthe package that would provide it
{title}textthe notice, as hive-cortex titles it
{body}textwhat it says
{severity}textinfo, notice, warning, error or critical

notice ​

A notice with no observation behind it (hive-nerve-fs reports its filesystem events this way).

FieldTypeWhat it is
{title}textthe notice, as hive-cortex titles it
{body}textwhat it says
{severity}textinfo, notice, warning, error or critical

offer ​

The Hive offered something an administrator may approve; the qs:offers lists it.

FieldTypeWhat it is
{count}inthow many offers came together
{title}textthe notice, as hive-cortex titles it
{body}textwhat it says
{severity}textinfo, notice, warning, error or critical

request ​

A user asked the Hive for a change (the Settings app, hive request): the offer carries it, for an administrator to approve as any other; the qs:offers lists it.

FieldTypeWhat it is
{user}textwho asked
{action}textwhat they asked for: the action's name (set-hostname)
{what}textwhat it does, in words (name the machine forge)
{count}inthow many offers came with it: one
{title}textthe notice, as hive-cortex titles it
{body}textwhat it says
{severity}textinfo, notice, warning, error or critical

done ​

An action an administrator approved on this screen ran and succeeded.

FieldTypeWhat it is
{action}textwhat ran: the action's name (restart-service)
{service}textthe service it was on, when it was on one
{package}textthe package it was about, when it was about one
{output}textwhat it printed, if anything

failed ​

An action an administrator approved on this screen ran and failed.

FieldTypeWhat it is
{action}textwhat ran: the action's name (restart-service)
{service}textthe service it was on, when it was on one
{package}textthe package it was about, when it was about one
{output}textwhat it said, if anything

The actions ​

What a qs:on holds, in order: qs:animate, qs:frame, qs:show, qs:tween, qs:emit, qs:say. A target is a sprite's name or a {field} that is one.

What is refused ​

A compile error names the line. The compiler refuses:

  • a tag, an attribute, an event or an action the schema does not list;
  • a sheet that is not there, is not a PNG, or is not a whole number of its frames; a frame a sheet does not have; an animation its sheet lacks; a sprite of a sheet that does not exist;
  • a {field} that is not a field of the rule's event; a {field} in the scene (it is filled in from an event, so it goes in a qs:on);
  • a literal target no sprite is named; an animation the target's sheet lacks; particles no qs:particles declares;
  • a q: statement, an expression, or text where a tag goes;
  • two qs:on on one event, a qs:on with nothing to do, two qs:scene, two qs:offers, two qs:bar on one side, two sprites of one name, a tween of no time; a {field} of an event in a qs:bar (a bar shows the screen's own fields) or in a panel's title.

hive-screen refuses a screen.toml that is not what the compiler writes, whole; it never guesses at a scene.

How it reaches the daemon ​

screen.toml, font.png and the sheets are installed under /etc/hive/screen/. The OS's own are written from os/config/screen/screen.q by scripts/generate-screen-config.py (the art by scripts/art/hive_screen.py), committed as written and checked in CI. hive-screen connects to hive-cortex's user socket as the dashboard does, and cortex lets it in as one: it receives every notice with the event's name and fields, and every offer, and may approve and dismiss. The Hive lab (os/crates/hive-lab) runs the real hive-screen with no display and reads what it shows as data ({"op":"screen","play":{"op":"until","offer":"Restart nginx"}}); a test of the daemon compares its frames pixel for pixel with references.

The levels ​

How much the daemon shows is a presentation level, chosen as it runs by the frame time it measures, never above what the machine's cores allow (read as it runs: a vCPU plugged in counts within half a second):

LevelNeedsShows
0 plain1 corethe scene as declared, no particles, at most 15 pictures a second
1 effects2 coresthe particles too (qs:emit, and every qs:emitter of level="1"), at most 30 a second
2 fine4 corestwo pixels to the scene's one: a tween or a particle lands on its own pixel, the font's pixels are blocks; and the emitters of level="2". A page on the screen is drawn pixel for pixel instead, so its window has 100 columns and up to 29 rows there, against 49 by up to 13

A level changes how the scene shows, never what happens in it: the same ticks run at every level, and a rule's actions run the same. The frame is drawn in bands, one thread a core, and is the same picture whatever their number. A scene at rest costs nothing at any level. The lab, the tests and --play play at level 1 unless asked ({"op":"level","level":2}).

The look of the scene ​

The scene is drawn in the look of a window manager in pixel art, the same the pages below take. A qs:bar spans the scene: at the top, 14 pixels (a light line, the bar, a rule), at the bottom 12 (a rule and the bar); its title sits at the left in the accent, the text after it and the text at the right in the bar's colour, 4 pixels in from each edge, and what a qs:say says takes the left of the bottom bar while it lasts (with no bottom bar it is a strip at the bottom of the scene). A qs:offers or qs:notices with a title is a panel: a 1-px bevel (light above and left, dark below and right), a 3-px shadow, a title bar of 10 pixels with a rule under it, and its rows inside, 3 pixels in from the frame, each a row of the font; the panel is as tall as its rows. The cells of the OS's honeycomb (scripts/art/hive_screen.py) carry their own bevel and a 2-px shadow in their frames.

Pages on the screen ​

The screen is also where the machine's applications show: quantum pixel, run by any local user in a project's folder, brings the application's pages over the scene (one app, many screens). The page's runtime is the application's own server, started in that process as quantum console starts it; it connects to hive-screen's pixel socket (/run/hive-screen/pixel.sock, anyone's) and sends each page's view tree as JSON ({"op":"show","page":{...}}), whole, every time it changes. The screen lays it out in cells of its font and draws it over the scene as a window: a frame with a 1-px bevel and a 3-px shadow, the scene dimmed around it, a honey title bar with the Esc key drawn on it, the content, and a status bar with the keys that apply; fields are sunken, buttons and badges raised, a scrollbar appears when the page is longer than the window. The window is as tall as its content, up to the canvas, and sits in the middle: 49 columns and up to 13 rows at 320×180; 100 by up to 29 at the fine level, where the page is drawn pixel for pixel on the canvas of two pixels to the scene's one, and flows again when the level changes, keeping its focus. The page moves as the window managers of 2008 did, in pixel art and in ticks: it opens with the jelly (grows from 60% with an overshoot, 12 ticks), closes with the lamp (bends and is sucked into the bottom left corner, 14 ticks), and a page shown again after an action replaces the one before through an ordered dither, the curtain (8 ticks); nothing moves at the plain level, and HIVE_SCREEN_MOTION=off in the unit's environment stops it all. It is drawn with the keyboard its own, and sends back what the user did, one JSON line an event: click and submit (with the form's fields, as a browser would post them), change (a search field, after its pause), navigate (a link), reload (F5) and close (Esc, after which the page's runtime ends). Only one page shows at a time; a second quantum pixel is told so. Keyboard only: Tab and Shift+Tab between the elements that react, the arrows inside one (Up and Down between elements when what has the focus does not take them, Home and End to the first and the last), Enter acts, Space marks, Esc closes; the last line says the keys that apply, and a scrollbar at the right edge says where in a long page the screen is. F1 opens the Hive's offers in a menu over the page (or over the scene), a drawer that slides in from the bottom left and pushes the comb aside: Up and Down choose, Enter approves (twice for a dangerous action), Del dismisses, Esc shuts it and the page has its keys back; the window's status bar says how many offers wait (F1 · 2 ofertas), and the bar at the bottom shows the open page as a button, as a taskbar does. The keyboard is read by the machine's layout: US, or ABNT2 (XKBLAYOUT="br" in /etc/default/keyboard, or HIVE_SCREEN_KEYBOARD=br in the unit's environment) with ç on its key and the dead accents (´ ` ^ ~ ¨) composed with the vowel after them; the font has every Latin-1 letter.

In the play protocol (--play), {"op":"show","page":{...}} shows a page with no runtime, act takes type (a text) and every key by name (tab, shift-tab, left, right, home, end, backspace, delete, space, pgup, pgdn, f1, f5, ctrl-u, or one character), until takes pixel (an event), text (shown on the page) and shown (how many times a page was shown), still (no transition runs), and the observation's window says the page as data and motion the transition it is in: its title, flash, the elements that react in order with their state and which has the focus, the texts, the help line. --pixel-socket PATH gives a test the socket too: the UI parity test runs its one script on the screen with quantum pixel as the page's runtime.

The screen Quantum OS ships ​

os/config/screen/screen.q, whole: a bar at the top with the machine's name, its cores, the level and the time, a honeycomb with a cell per service the whitelist names, a bee, the offers and the notices as panels, and a bar at the bottom where what the Hive says shows.

xml
<!-- The Hive's screen: what hive-screen draws on the machine's console, and what happens in it when
     the Hive's events arrive. Compiles to screen.toml and font.png next to this file
     (python scripts/generate-screen-config.py), which hive-screen loads from /etc/hive/screen/. -->
<q:application id="hive-screen" type="screen">

  <!-- the cells of the honeycomb: 24x24 frames, drawn by scripts/art/hive_screen.py -->
  <qs:sheet name="cell" src="art/cells.png" tile="24">
    <qs:animation name="smoke" frames="2,3,4,5,6,2" fps="4" loop="false" />
    <qs:animation name="worry" frames="7,1" fps="2" />
  </qs:sheet>
  <qs:sheet name="bee" src="art/bee.png" tile="16">
    <qs:animation name="fly" frames="0,1" fps="10" />
  </qs:sheet>

  <qs:particles name="stars" preset="stars" />
  <qs:particles name="dust" preset="dust" color="#8a7a5a" />
  <qs:particles name="snow" preset="snow" amount="24" color="#c8d0f0" />

  <qs:scene width="320" height="180" background="#1a1a2e">
    <!-- the fine level, which the daemon affords with the machine's cores: snow over the scene -->
    <qs:emitter particles="snow" x="0" y="0" width="320" level="2" />
    <!-- the bar at the top: the machine, its cores, the level the screen is at and the time -->
    <qs:bar side="top" title="Quantum OS" left="{host}" right="{cores} cores · {level} · {clock}" />

    <!-- one cell a service the whitelist names; a cell fills with honey when the Hive restarts it -->
    <qs:sprite name="nginx" sheet="cell" frame="1" x="6" y="18" />
    <qs:text value="nginx" x="34" y="24" />
    <qs:sprite name="apache2" sheet="cell" frame="1" x="6" y="46" />
    <qs:text value="apache2" x="34" y="52" />
    <qs:sprite name="postgresql" sheet="cell" frame="1" x="6" y="74" />
    <qs:text value="postgresql" x="34" y="80" />
    <qs:sprite name="mysql" sheet="cell" frame="1" x="6" y="102" />
    <qs:text value="mysql" x="34" y="108" />
    <qs:sprite name="redis-server" sheet="cell" frame="1" x="98" y="18" />
    <qs:text value="redis" x="126" y="24" />
    <qs:sprite name="mongodb" sheet="cell" frame="1" x="98" y="46" />
    <qs:text value="mongodb" x="126" y="52" />
    <qs:sprite name="docker" sheet="cell" frame="1" x="98" y="74" />
    <qs:text value="docker" x="126" y="80" />
    <qs:sprite name="containerd" sheet="cell" frame="1" x="98" y="102" />
    <qs:text value="containerd" x="126" y="108" />
    <qs:sprite name="bee" sheet="bee" x="168" y="36" animation="fly" />

    <!-- the offers and the notices as panels with their own title bars -->
    <qs:offers title="Offers" x="188" y="18" width="126" rows="9" />
    <qs:notices title="Notices" x="6" y="132" width="308" rows="2" />
    <!-- the bar at the bottom: what the Hive said last shows here while it lasts -->
    <qs:bar side="bottom" right="offers: {offers} · F1" />
  </qs:scene>

  <qs:on event="service-crash">
    <qs:animate target="{service}" name="smoke" />
    <qs:emit particles="dust" at="{service}" dx="12" dy="12" />
    <qs:say text="{service} stopped (exit {exit_code})" />
  </qs:on>
  <qs:on event="log-anomaly">
    <qs:animate target="{service}" name="worry" />
    <qs:say text="{service} is logging errors" />
  </qs:on>
  <qs:on event="port-conflict">
    <qs:say text="port {port}: {requester} wanted it, {holder} holds it" />
  </qs:on>
  <qs:on event="disk-low">
    <qs:say text="{mount} is {percent}% full, {free_mb} MB free" />
  </qs:on>
  <qs:on event="memory-pressure">
    <qs:say text="memory: {free_mb} of {total_mb} MB free" />
  </qs:on>
  <qs:on event="command-observed">
    <qs:say text="watched: {cmd}" />
  </qs:on>
  <qs:on event="command-not-found">
    <qs:say text="{command}: not installed; {package} would provide it" />
  </qs:on>
  <qs:on event="notice">
    <qs:say text="{title}: {body}" />
  </qs:on>
  <qs:on event="request">
    <qs:emit particles="stars" at="bee" dx="8" dy="8" />
    <qs:say text="{user} asks: {what}; Enter approves, Escape dismisses" seconds="6" />
  </qs:on>
  <qs:on event="offer">
    <qs:emit particles="stars" at="bee" dx="8" dy="8" />
    <qs:tween target="bee" property="y" to="42" from="36" seconds="0.4" trans="sine" ease="in-out" repeat="3" yoyo="true" />
    <qs:say text="the Hive offers {count}: up and down choose, Enter approves, Escape dismisses" seconds="6" />
  </qs:on>
  <qs:on event="done">
    <qs:frame target="{service}" frame="0" />
    <qs:emit particles="stars" at="{service}" dx="12" dy="12" />
    <qs:say text="{action}: done" />
  </qs:on>
  <qs:on event="failed">
    <qs:say text="{action} failed: {output}" seconds="8" />
  </qs:on>
</q:application>

MIT Licensed · Built with VitePress