Marquee English

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 page Touches : les pastilles MacBook Pro et Mac mini (injoignable depuis 12 min), puis les touches du MacBook Pro — Terminal (application ouverte, trait sous l'icône), Tests (2 tests rouges), Sauvegarde, Navigateur, Réunion, Déployer, Build (fait · 12 min), Concentration.
La page Touches, dans la démo : deux machines, l'une injoignable.
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 application ou commande, 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 commande seulement : 60 par défaut. Une application a son propre délai, fixé par l'écran.
confirmer
true pour une commande qui ne doit pas partir sur un frôlement : false par 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 :

Avec osascript · l'app doit être lancée
osascript -e 'id of app "Ghostty"'
Avec mdls · sans la lancer
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

ChampLimite
Touches par machine60 au plus
idminuscules, chiffres, tirets ; 1 à 32 caractères ; unique
libelle40 caractères au plus
icone (emoji)16 octets au plus
icone (PNG)64 Kio au plus
delaide 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.