From b2c902533305c6362fe9bdaddf61098cbc097b42 Mon Sep 17 00:00:00 2001 From: JohnsonKC201 Date: Fri, 9 Oct 2026 01:00:11 -0400 Subject: [PATCH 1/2] feat(launcher): the pet in the header reacts to what you type It was a still picture in the corner of the box. It now breathes, leans toward the text when there is a row to run, tilts its head when there is not, taps along with each key, hops when a search finally finds something or a row is run, and dozes with a small z when the box is left empty for eight seconds. The sprite is not redrawn. cat-preview.js is shared with the site and a test keeps the two copies identical, so the finished picture is moved instead: breathing animates scale, a mood is a rotate pose, a one-off is translate. The three are separate properties, so they never fight, and all of it stays on the compositor. Which mood to show is a pure function in launcher-pet.js that sees only whether a row can be run, never its wording. It holds still under the OS reduced-motion setting and the app's own: the pose stays, every animation and transition goes. From review: Escape and running a row count as activity, so a long sentence followed by Escape does not doze at once. The breathing speed is a variable, since one animation-duration on the doze rule outranked the hop and stretched it to six seconds. A reaction ends with its own animation and not with the next re-render, which the one-second timer countdown would otherwise cut short. Reopening starts from rest. --- src/launcher-pet.js | 33 ++++++++++++++ src/launcher-renderer.js | 44 ++++++++++++++++--- src/launcher.html | 43 +++++++++++++++++-- tests/launcher-pet.test.js | 88 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 200 insertions(+), 8 deletions(-) create mode 100644 src/launcher-pet.js create mode 100644 tests/launcher-pet.test.js diff --git a/src/launcher-pet.js b/src/launcher-pet.js new file mode 100644 index 0000000..c74b603 --- /dev/null +++ b/src/launcher-pet.js @@ -0,0 +1,33 @@ +// What the pet in the launcher header is doing. Pure rules, no DOM: the launcher +// page asks for a mood, puts it on , and CSS poses the +// picture. Nothing here reads a row's wording, only whether it can be run. +// +// Loaded as a classic + diff --git a/tests/launcher-pet.test.js b/tests/launcher-pet.test.js new file mode 100644 index 0000000..712925a --- /dev/null +++ b/tests/launcher-pet.test.js @@ -0,0 +1,88 @@ +// The pet in the launcher header reacts to what you type. Which mood it is in is +// a pure decision; the page only sets an attribute and lets CSS move the picture. +const test = require('node:test'); +const assert = require('node:assert'); +const fs = require('node:fs'); +const path = require('node:path'); + +const pet = require('../src/launcher-pet'); + +const SRC = path.join(__dirname, '..', 'src'); +const read = (f) => fs.readFileSync(path.join(SRC, f), 'utf8'); +const row = (enabled) => ({ enabled }); + +test('an empty box is idle, and dozes once it has been left alone', () => { + assert.strictEqual(pet.petMood({ query: '', items: [row(true)], idleMs: 0 }), 'idle'); + assert.strictEqual(pet.petMood({ query: ' ', items: [row(true)], idleMs: pet.PET_DOZE_MS - 1 }), 'idle'); + assert.strictEqual(pet.petMood({ query: '', items: [row(true)], idleMs: pet.PET_DOZE_MS }), 'doze'); + assert.strictEqual(pet.petMood({ query: 'x', items: [row(true)], idleMs: pet.PET_DOZE_MS * 9 }), 'listen', 'it never dozes mid-sentence'); +}); + +test('it listens when there is something to run and is curious when there is not', () => { + assert.strictEqual(pet.petMood({ query: 'gmail', items: [row(true), row(false)] }), 'listen'); + assert.strictEqual(pet.petMood({ query: 'todo ', items: [row(false)] }), 'curious', 'a hint row is not something to run'); + assert.strictEqual(pet.petMood({ query: 'zzqq', items: [] }), 'curious'); + assert.strictEqual(pet.petMood({ query: 'x' }), 'curious', 'no list at all'); + assert.strictEqual(pet.petMood({}), 'idle'); +}); + +test('a keystroke is a tap, finding something after nothing is a hop, and so is running a row', () => { + assert.strictEqual(pet.petReaction('type', 'listen', 'listen'), 'tap'); + assert.strictEqual(pet.petReaction('type', 'idle', 'listen'), 'tap'); + assert.strictEqual(pet.petReaction('type', 'curious', 'listen'), 'hop'); + assert.strictEqual(pet.petReaction('type', 'listen', 'curious'), 'tap'); + assert.strictEqual(pet.petReaction('run', 'listen', 'listen'), 'hop'); + assert.strictEqual(pet.petReaction('type', 'listen', 'idle'), null, 'clearing the box is not a keystroke to react to'); + assert.strictEqual(pet.petReaction('doze', 'idle', 'doze'), null); + assert.strictEqual(pet.petReaction('anything-else', 'idle', 'idle'), null); +}); + +test('the page loads the mood rules before the script that uses them', () => { + const scripts = [...read('launcher.html').matchAll(/