Créer ses touches
Une touche ouvre une app, ou lance une commande : tout vit dans le config.json de ton Mac, au chapitre Installer le Mac. Ce chapitre détaille chaque champ, puis te donne dix-sept recettes à copier.
Dans les exemples, ton dossier personnel est /Users/moi, et l'écran a pour adresse 192.168.1.50.
1L'anatomie d'une touche
La page Touches groupe tes machines en pastilles, puis leurs touches en dessous : une icône, un libellé, et une ligne d'état qui dit ce qui se passe.
- La pastille de la machine
- Son nom, et un point plein si elle est connectée. Injoignable, le point se vide et la pastille dit depuis quand : « injoignable depuis 12 min », ou « jamais connectée » si elle ne s'est encore jamais montrée.
- Une touche d'application
- Son icône (celle de l'app), son libellé, et un petit trait sous l'icône tant que l'app est ouverte : comme le point du Dock. Terminal, sur la capture, porte ce trait.
- Une touche de commande
- Son icône (un emoji, ou un PNG), son libellé, et une ligne d'état : verte pour « fait », avec l'âge du résultat, rouge pour un échec ou pour « 2 tests rouges », ou « en cours… » pendant l'exécution.
2Les champs
Une touche vit dans le tableau touches de config.json. Six champs, dont deux exclusifs l'un de l'autre :
id- Minuscules, chiffres et tirets, de 1 à 32 caractères :
[a-z0-9-]{1,32}. Unique dans le fichier. Obligatoire. libelle- Ce que la touche affiche, 40 caractères au plus. Obligatoire.
application- Un bundle id (étape 3). Exactement l'un de
applicationoucommande, jamais les deux, jamais aucun des deux. commande- Une liste d'arguments (étape 4), exécutée sans shell. Le premier élément ne peut pas être vide.
icone- Un emoji, ou le chemin absolu d'un PNG (étape 5). Absente pour une application, c'est son icône qui s'affiche.
delai- En secondes, pour une
commandeseulement : 60 par défaut. Une application a son propre délai, fixé par l'écran. confirmertruepour une commande qui ne doit pas partir sur un frôlement :falsepar défaut.
Un champ inconnu (une faute de frappe dans son nom) est une erreur, pas un champ ignoré : voir Le rechargement à chaud.
3Trouver un bundle id
application attend un identifiant d'app (un bundle id, comme com.mitchellh.ghostty), pas son nom. Deux façons de le lire, avec Ghostty pour exemple :
osascript -e 'id of app "Ghostty"'
mdls -name kMDItemCFBundleIdentifier -r /Applications/Ghostty.app
4Une commande, jamais un shell
commande est une liste d'arguments : le premier est le programme, lancé directement, les suivants sont ses paramètres. Aucun shell ne les interprète : pas de &&, pas de |, pas de *, pas de variable comme $HOME. C'est voulu : config.json n'est pas un endroit où glisser un script.
Pour ces outils-là, donne-toi un shell, explicitement :
"commande": ["/bin/sh", "-c", "cd ~/code/mon-projet && make test"]
Là, c'est /bin/sh qui interprète la chaîne qui suit -c : &&, ~ et le reste fonctionnent, parce que c'est sh lui-même qui les lit.
5Des chemins absolus
ecran-agent est lancé par launchd, pas par ton shell de connexion : le premier élément de commande doit donc être un chemin absolu (/usr/bin/make, pas make) : sans shell, rien ne cherche dans ton PATH. Même règle pour le chemin d'une icône PNG : pas de ~, qui est une syntaxe de shell, et un chemin relatif partirait de /.
Ce qui suit /bin/sh -c "…" échappe à cette règle : c'est sh, une fois lancé, qui relit ~ et ton PATH à l'intérieur de sa propre chaîne (étape précédente).
6Les états d'une touche
Un appui traverse plusieurs états, que la touche affiche l'un après l'autre :
- déposé
- L'écran vient de transmettre l'appui à ta machine.
- pris
- Ta machine l'a reçu, et va l'exécuter.
- en cours…
- L'app se lance, ou la commande tourne.
- fait
- Terminé avec succès, en vert : reste affiché deux secondes, avec l'âge du résultat.
- échoué
- Terminé en échec, en rouge, avec le détail que ta machine a donné : reste affiché six secondes.
- sans nouvelles
- Aucune réponse dans le délai attendu : ta machine s'est peut-être endormie, ou la commande dépasse son
delai. Ce que fait vraiment la commande, l'écran ne le saura peut-être jamais.
Indépendamment de tout appui, une touche d'application porte un trait sous son icône tant que l'app est ouverte : c'est ta machine qui le dit, plusieurs fois par minute, pas seulement au moment d'un appui.
7Les limites
| Champ | Limite |
|---|---|
| Touches par machine | 60 au plus |
id | minuscules, chiffres, tirets ; 1 à 32 caractères ; unique |
libelle | 40 caractères au plus |
icone (emoji) | 16 octets au plus |
icone (PNG) | 64 Kio au plus |
delai | de 1 à 3600 secondes |
Ce sont les bornes du protocole : ta machine les applique elle-même avant de se connecter, avec le même code que l'écran. Une touche qui les dépasse n'est jamais transmise : voir Le rechargement à chaud.
8Le rechargement à chaud
Ta machine surveille config.json : dès qu'il change, elle le relit, sans qu'il faille redémarrer le service. Une config valide remplace l'ancienne aussitôt.
Une config fautive — champ inconnu, JSON invalide, touche qui dépasse une limite — ne remplace rien : tes touches d'avant restent actives, et l'erreur part vers l'écran, où elle s'affiche sous le nom de ta machine, jusqu'à ce que tu corriges et sauvegardes de nouveau.
Une icône PNG absente ou illisible ne fait pas échouer la config. Ta machine essaie de la lire au démarrage, puis à chaque relecture de config.json ; si le fichier manque, si sa conversion échoue, ou s'il ne fait pas un PNG valide, la touche garde son libellé, sans icône, et le journal le dit : tests : pas d'icône, libellé seul (avec l'id de la touche à la place de tests). Le chemin lui-même n'est vérifié qu'à cet instant-là, pas à la lecture de config.json.
9Le recueil
Dix-sept touches, prêtes à copier dans le tableau touches de ton config.json — chacune vérifiée contre le code de l'agent avant publication.
{ "id": "terminal", "libelle": "Terminal", "application": "com.apple.Terminal" }
{ "id": "ghostty", "libelle": "Ghostty", "application": "com.mitchellh.ghostty" }
{ "id": "musique", "libelle": "Musique", "application": "com.apple.Music" }
{ "id": "obs", "libelle": "OBS", "application": "com.obsproject.obs-studio" }
{ "id": "tests", "libelle": "Tests", "icone": "🧪", "commande": ["/bin/sh", "-c", "cd ~/code/mon-projet && make test"], "delai": 600 }
{ "id": "build", "libelle": "Build", "icone": "🔨", "commande": ["/usr/bin/make", "-C", "/Users/moi/code/mon-projet", "build"], "delai": 900 }
{ "id": "deployer", "libelle": "Déployer", "icone": "🚀", "commande": ["/bin/sh", "-c", "cd ~/code/mon-site && ./deploy.sh"], "delai": 1800, "confirmer": true }
{ "id": "projets", "libelle": "Projets", "icone": "📁", "commande": ["/usr/bin/open", "/Users/moi/code"] }
{ "id": "tableau", "libelle": "Tableau de bord", "icone": "📊", "commande": ["/usr/bin/open", "https://exemple.org/tableau"] }
{ "id": "visio", "libelle": "Visio", "icone": "🎥", "commande": ["/usr/bin/open", "https://visio.exemple.org/equipe"] }
{ "id": "concentration", "libelle": "Concentration", "icone": "🎧", "commande": ["/usr/bin/shortcuts", "run", "Concentration"] }
{ "id": "muet", "libelle": "Son coupé", "icone": "🔇", "commande": ["/usr/bin/osascript", "-e", "set volume output muted true"] }
{ "id": "ecrans", "libelle": "Écrans en veille", "icone": "🌙", "commande": ["/usr/bin/pmset", "displaysleepnow"] }
{ "id": "sauvegarde", "libelle": "Sauvegarde", "icone": "💾", "commande": ["/usr/bin/tmutil", "startbackup", "--auto"], "confirmer": true }
{ "id": "lumiere", "libelle": "Lumière atelier", "icone": "💡", "commande": ["/usr/bin/curl", "-fsS", "-X", "POST", "http://192.168.1.50:8123/api/webhook/lumiere-atelier"], "delai": 10 }
{ "id": "imprimante", "libelle": "Relancer OctoPrint", "icone": "🖨️", "commande": ["/usr/bin/ssh", "pi@192.168.1.60", "sudo systemctl restart octoprint"], "delai": 30, "confirmer": true }
{ "id": "rendu", "libelle": "Rendu", "icone": "/Users/moi/icones/rendu.png", "commande": ["/Users/moi/bin/rendre-la-scene"], "delai": 3600, "confirmer": true }
Trois demandent un accès à un autre appareil du réseau : adapte 192.168.1.50 (Lumière atelier) et 192.168.1.60 (Relancer OctoPrint) aux tiens. Les autres tournent sur ton Mac seul.