04 · project

PetApp — Un compagnon de bureau pour signaler l'état de mes tâches

Domain: Concept Created: 2026-05-02 Updated: 2026-05-02

OpenAI a lancé un mode `/pet` dans Codex, une créature animée signalant l’activité de l’agent. J'envisage de créer une surface ambiante similaire pour surveiller ses services lifeOS, crons et builds.

PetApp — Un compagnon de bureau pour signaler l'état de mes tâches

Type: Projet personnel · macOS native · Swift / SwiftUI
Status: POC validé, v1 en cours
Stack: Hammerspoon (POC) → Swift + SwiftUI + SpriteKit (v1)
Timeline: POC en une soirée, v1 sur quelques weekends


L'origine

OpenAI a sorti dans Codex un mode /pet : une petite créature animée vit en overlay flottant près du dock du Mac et signale ce que l'agent est en train de faire. Comme une Dynamic Island en miniature, mais avec du caractère. L'idée m'a immédiatement parlé : c'est exactement le type de surface ambiante qui pourrait me servir pour signaler l'état de mes services lifeOS, des crons, des builds, sans avoir à ramener des fenêtres au premier plan.

La question n'était pas "est-ce que je peux faire ça". La question était : est-ce que ça va vraiment me servir au quotidien ? Construire un truc joli est facile, construire un truc qu'on garde allumé tous les jours est une autre histoire.

D'où l'approche en deux temps : un POC brutal pour valider l'utilité, puis une vraie app native si le POC me convainc.


L'architecture (la décision la plus importante)

Avant de coder une ligne, j'ai posé l'archi pour qu'elle soit portable d'un runtime à l'autre. Le POC et la v1 partagent exactement la même plomberie :

┌─────────────────────────────────────────┐
│  Scripts producteurs                    │
│  (lifeos-enrich, crons, builds, ntfy…)  │
└──────────────────┬──────────────────────┘
                   │  petsay <state> "<msg>"
                   ▼
       /tmp/petstate.json   ← bus partagé (stable)
                   ▲
                   │
            [runtime de rendu]
            POC : Hammerspoon
            v1  : Swift app

Le contrat : un fichier JSON simple dans /tmp qui contient state, message, source, ts. N'importe quel script bash, Python ou autre peut pousser un état avec un helper CLI petsay. Le runtime de rendu (Hammerspoon ou app Swift) lit ce fichier et anime le pet en conséquence.

Pourquoi c'est la bonne décision :
- Découplage total producteur/consommateur : si l'app crash, mes scripts ne plantent pas.
- Migration sans casse : passer du POC à l'app native ne demande aucune modification côté producteurs. petsay continue de marcher.
- Multi-source : n'importe quel script ou microservice peut pousser, sans coordination.
- Pas de réseau, pas de daemon, pas de DB : le fichier /tmp suffit.

C'est exactement le pattern producer/consumer qu'utilisent mes microservices lifeOS qui parlent via MariaDB. Ici le "bus" est juste un fichier JSON.


États & contrat de données

Quatre états couvrent 95% des cas d'usage :

État Sens Comportement
idle Rien à signaler Animation calme ou statique
working Tâche en cours Animation active
done Tâche terminée avec succès Anim de joie + notif macOS, retour idle après 5s
error Échec Anim d'alerte + notif macOS, persiste jusqu'au prochain push

Format du fichier /tmp/petstate.json :

{
  "state": "working",
  "message": "Enrichissement batch 12/20",
  "source": "lifeos-enrich",
  "ts": 1730500000
}

Règles automatiques côté pet :
- done → revient à idle après 5 secondes
- Si now - ts > 30 minutes → force idle (anti-stale, au cas où un script a planté avant d'envoyer le done)


Phase 1 — POC Hammerspoon

Objectif : valider l'utilité en une soirée, avec le minimum d'investissement technique.

Pourquoi Hammerspoon : outil macOS gratuit, scripté en Lua, qui peut dessiner des canvas transparents flottants natifs. Setup en 5 minutes. Parfait pour un POC.

Ce qui est livré :
- ~/.hammerspoon/init.lua — le pet, ~150 lignes de Lua
- ~/bin/petsay — helper bash, ~30 lignes
- Intégration dans le worker d'enrichissement nocturne lifeOS (2 lignes ajoutées)

Le pet en POC :
- Position : coin bas-droit, 80px au-dessus du dock
- Sprite : emoji XL qui change selon l'état (😴 ⚙️ ✅ ⚠️) — zéro création d'asset
- Animation : aucune en idle (économie CPU), pulse simple en working, bounce sur done
- Click : popup avec dernier message reçu
- Notif macOS native sur done et error

Critères de validation après une semaine d'usage :
- [ ] Je laisse le pet tourner toute la journée sans le tuck away
- [ ] J'ai connecté au moins 2 sources
- [ ] Je me surprends à attendre le ✅ pour passer à autre chose
- [ ] CPU au repos < 5%

Si tous les critères sont cochés, je passe à la v1. Sinon, je jette le POC sans regret. C'est la version "produit" du lifeos_correlations : on observe avant de conclure.


Phase 2 — v1 : app Swift native

Une fois le POC validé, on remplace uniquement la couche de rendu. L'archi découplée rend ça possible : le bus reste stable, on change le runtime.

Pourquoi Swift natif et pas Electron / Tauri / WebView dans Hammerspoon :

Critère Hammerspoon (POC) Swift natif (v1)
Animation fluide 60fps Non (canvas limité) Oui (SpriteKit GPU)
CPU au repos Moyen < 3% (FSEvents + paused scene)
Distribuable Non (config user) Oui (.app, .dmg, signable)
Time to first pixel 30 min 1 weekend
Multi-tâche / multi-écran robuste Bricolable Natif

Comme je suis Genius Apple Store et que j'ai commencé à expérimenter Xcode, le coût d'apprentissage de Swift est un investissement qui m'intéresse pour d'autres projets aussi. Et pour ce cas précis (overlay flottant non-activant, performance, intégration système), Swift natif est techniquement la meilleure solution, pas juste un caprice.

Stack technique

Couche Tech Rôle
Window NSPanel + .nonactivatingPanel Flotte au-dessus du dock sans voler le focus
Bridge NSHostingView Embarque SwiftUI dans AppKit
UI SwiftUI Layout, menus contextuels, popups
Animation SpriteKit (SpriteView + SKScene) Sprites 60fps, atlas de textures
State ObservableObject + @Published Source de vérité observée par la View
IPC FSEvents API Détection sans polling des changements de fichier
Menu bar NSStatusItem Icône + menu (show/hide, preferences, quit)
Notifs UserNotifications Bannières macOS natives

Décisions techniques notables

NSPanel plutôt que NSWindow. NSWindow standard vole le focus au clic et apparaît dans Mission Control. NSPanel avec .nonactivatingPanel ne prend jamais le focus et peut suivre l'utilisateur sur tous les Spaces. SwiftUI ne l'expose pas nativement → subclass + NSHostingView. Pattern documenté, pas un hack.

LSUIElement = YES dans Info.plist. Cache l'icône du Dock et le nom dans la barre de menus. Le pet est un agent en menu bar, pas une app classique.

SpriteKit plutôt que SwiftUI TimelineView + Image. Pour de l'animation frame-by-frame avec sprite sheets, SpriteKit est l'outil pensé pour ça : SKTextureAtlas, SKAction.animate, transitions GPU-accélérées. Faire la même chose en SwiftUI pur, c'est réinventer mal SpriteKit.

FSEvents plutôt qu'un Timer. Polling = CPU constant pour rien. FSEvents notifie quand un fichier change. Zéro coût quand rien ne se passe. C'est exactement le bug Codex (overhead GPU constant) que j'évite.

Pause de la SKScene quand state == .idle. view.isPaused = true → la scene ne rend plus rien. Économie CPU/GPU significative pour un état qui peut durer des heures.

Roadmap d'implémentation

Sprint 1 — Skeleton (1 weekend)
Une fenêtre transparente flotte au bon endroit avec un emoji statique. Brique fondamentale : NSPanel, LSUIElement, NSHostingView, positionnement au-dessus du dock.

Sprint 2 — State pipeline (1 soirée)
StateManager observable, FSEventStream qui watch /tmp/petstate.json, JSON decode robuste, auto-revert done → idle, stale detection 30min.

Sprint 3 — Sprites & SpriteKit (1 weekend)
Choix du character design, dessin des 4 boucles dans Aseprite, export en sprite atlas Xcode, PetScene: SKScene, intégration via SpriteView.

Sprint 4 — Polish (1 weekend)
Click → popup, notifs natives, menu bar enrichi, transitions entre états, préférences, app icon.

Bonus (post-v1)
Animation de Tuck Away (sticker qui se décolle et tombe), hot-reload des sprites, endpoint HTTP local pour pilotage depuis Pi5, packaging dmg signé.


Le character design — la vraie question créative

C'est là où le côté technique passe au second plan. Le pet doit avoir une référence ancrée. Le Lil Finder Guy de Codex marche parce qu'il convoque l'iconographie Susan Kare (Mac OS classique). Sans ancrage culturel, c'est juste un blob.

Méthode : dessiner 8 expressions statiques avant toute animation. Si elles ont du caractère statiques, l'animation viendra naturellement.

Pistes alignées avec mon univers :
- Vector miniature — référence directe à mon projet de résurrection d'Anki Vector
- Style Susan Kare 1-bit — pixel art noir et blanc, grammaire originale du Mac
- Mascotte lofi japonaise — formes simples, palette restreinte (Tanaami, Yokoo)
- Pet typographique — un caractère animé (lettre, ligature, glyphe vivant)

Spécifications techniques :
- Résolution : 64×64 px ou 96×96 px (export retina ×2/×3)
- Format : PNG avec alpha, pas de fond blanc
- Frames : idle 4-8 (12fps), working 8-12 (18fps), done 6-10 (joué une fois), error 4-6 (12fps)
- Naming Apple atlas : idle_001.png, idle_002.png, etc.


Bonus animation — Le sticker qui se décolle

Pour les transitions importantes (Tuck Away, quit), je veux un effet narratif fort : un autocollant qui se détache progressivement du bureau, s'enroule en révélant son verso (papier blanc avec trace de colle), puis tombe en feuille morte en alternant recto et verso avant de sortir de l'écran.

Pourquoi c'est un effet rare : une animation aussi élaborée vue plusieurs fois par jour devient agaçante. Vue 2-3 fois par semaine, elle reste mémorable et donne du caractère à l'app. À garder pour les transitions marquantes, pas pour le quotidien.

Découpage narratif sur 6 secondes :

Acte Timing Phase Face visible
1 0,0 – 1,2s Tension : la colle craque Recto
2 1,2 – 3,0s Enroulement progressif Recto + verso
3 3,0 – 3,6s Lâcher complet Verso
4 3,6 – 6,0s Chute en feuille morte Alternance

Les 3 ingrédients qui vendent l'effet :

  1. L'ombre vit sa propre vie. Petite et collée quand le sticker est sur le bureau, grande et diffuse quand il décolle, qui s'évanouit pendant la chute. C'est le principal vendeur de la 3D.

  2. Rotations désynchronisées. Pendant la chute, les trois axes ont des fréquences différentes et non-harmoniques (3,1π / 2,3π / 2,7π). Si toutes les fréquences sont identiques ou multiples entiers, le mouvement devient mécanique. Le décalage = vivant.

  3. Easings hétérogènes. Chaque acte a son propre easing : sin amorti pour la tension, easeOutCubic pour l'enroulement, easeInCubic pour la chute, sinusoïdal pour la dérive. Tout en easeInOut → trop lisse, irréaliste.

Approche technique : faux enroulement par "strips". Le sticker est découpé en 8 bandes horizontales superposées, chacune avec sa propre rotation X progressive autour de transform-origin: top center. Vu de loin → illusion d'enroulement cylindrique continu. Chaque bande contient un recto et un verso pré-tourné de 180°, le navigateur (ou SwiftUI avec gestion manuelle d'opacité) choisit quelle face afficher.

Le détail qui fait tout : la trace de colle au verso est légèrement plus petite que le sticker. Sur un vrai autocollant, la colle ne va jamais jusqu'au bord — il y a toujours une marge sèche. C'est ce détail qui sépare un autocollant crédible d'un sticker générique.

Spec détaillée dans PET-STICKER-ANIM.md.


Pourquoi ce projet

Au-delà du gadget, ce projet coche plusieurs cases personnelles :

Pour mon Life OS. Un canal de notification ambiant en plus des notifs macOS classiques, qui me permet de garder un œil sur l'état de mes microservices lifeOS sans interrompre mon flow.

Pour mon apprentissage Swift. Ça me donne un projet concret pour apprendre SwiftUI, AppKit, SpriteKit et le pattern d'app menu bar — toutes des compétences réutilisables pour d'autres projets MyLastNight Systems.

Pour mon plaisir de design. Mon background BTS Comm Vis et ma fascination pour Susan Kare / typographie / lofi japonais trouvent une application directe dans le character design.

Comme méthode. Le pattern POC brutal validé en une soirée → v1 propre sur quelques weekends, avec une archi découplée qui rend la migration indolore, c'est exactement la méthode que j'applique sur tous mes projets MyLastNight. Ce projet est aussi une vitrine de cette méthode.


Ressources

NSPanel + SwiftUI
- Cindori — Make a floating panel in SwiftUI for macOS
- Markus Bodner — Spotlight/Alfred-like window
- Fazm Blog — SwiftUI Floating Panel: NSPanel Patterns

SpriteKit dans SwiftUI
- Apple Developer Docs — SpriteView
- Hacking with Swift — How to integrate SpriteKit using SpriteView
- Create with Swift — Sprite Animation with SpriteKit

Character design
- Susan Kare's design archive
- Aseprite tutorials
- Richard Williams — The Animator's Survival Kit

Inspiration
- OpenAI Codex /pet mode (avril 2026)
- Lil Finder Guy (9to5Mac)


Status au moment d'écrire ces lignes

POC Hammerspoon en cours de finalisation. Phase d'observation d'une semaine prévue. Si validé, démarrage du sprint 1 de la v1 Swift dans la foulée. Le character design (le vrai défi créatif) sera fait en parallèle des sprints techniques.

L'objectif : un compagnon discret qui m'aide à rester en flow, pas une mascotte décorative qui distrait. La validation finale viendra de l'usage : si je le garde allumé tous les jours pendant un mois, il a gagné sa place.

Resources


← All projects