J'ai construit onyx pour apprendre, pas pour concurrencer
La même surface de modules en Go et en Rust. paths, files, shell, clipboard, notify, keyring. Deux crates qui existent parce que je les ai écrites.
Cette semaine, j’ai ouvert le code de deux crates. Un module Go, une crate Rust. Même nom, même surface : osinfo, paths, files, shell, clipboard, notify, keyring. L’ensemble s’appelle onyx. Code sur github.com/akira-io/onyx (Go) et github.com/akira-io/onyx-rs (Rust).
Je ne l’ai pas construit pour concurrencer quoi que ce soit.
Onyx est un projet d’étude que j’ai laissé s’échapper sous licence MIT. Le sujet, c’était la forme de la colle desktop cross-platform, pas le marché. dirs, arboard, notify-rust, keyring-rs. Elles existent toutes. Elles marchent toutes. Lire les bibliothèques des autres t’apprend comment ils pensent. Écrire la tienne t’apprend ce que tu crois.
Le problème
Chaque application desktop que j’ai écrite réécrit les mêmes shims d’OS :
- Trouver le répertoire de config de l’utilisateur.
- Ouvrir un fichier avec l’app par défaut.
- Le révéler dans le gestionnaire de fichiers.
- Résoudre un binaire CLI sur le PATH avec des fallbacks propres à la plateforme.
- Lire et écrire le presse-papiers.
- Afficher une notification.
- Stocker un secret dans le keyring du système.
switch runtime.GOOS {
case "darwin":
return filepath.Join(home, "Library", "Application Support", app), nil
case "linux":
if xdg := os.Getenv("XDG_CONFIG_HOME"); xdg != "" {
return filepath.Join(xdg, app), nil
}
return filepath.Join(home, ".config", app), nil
case "windows":
if v := os.Getenv("APPDATA"); v != "" {
return filepath.Join(v, app), nil
}
return filepath.Join(home, "AppData", "Roaming", app), nil
}
Ce bloc vit dans chaque app desktop que j’ai livrée. Le coût, ce ne sont pas les lignes de code. Le coût, c’est l’impôt cognitif de confirmer, encore, quel OS Linux désigne et quel chemin Windows veut cette semaine.
La règle
Onyx remplace ce bloc par une ligne.
// github.com/akira-io/onyx/paths
config, err := paths.For("Hyperion").Config()
Une fonction. Un verbe. Une forme de retour. Le Resolver suit la même discipline.
// github.com/akira-io/onyx/shell
claude, err := shell.NewResolver().
Lookup("claude").
Lookup("/opt/homebrew/bin/claude").
Resolve()
Lookup accepte soit un nom résolu via PATH, soit un chemin absolu vérifié comme fichier. Le package devine lequel parce que la différence est mécanique : si la chaîne a un séparateur, c’est un chemin. L’appelant ne choisit pas.
Je ne pense pas que ce soit la meilleure API. Je pense que c’est l’API vers laquelle je vais me tourner, ce qui est une affirmation différente et plus utile.
Pourquoi le miroir en Rust
onyx-rs est le même exercice dans un second langage. Mêmes noms de modules, mêmes conventions, idiomatique dans chacun. paths.For("Hyperion").Config() côté Go devient paths::for_app("Hyperion").config() côté Rust. shell.NewResolver() devient shell::Resolver::new(). Le module Go renvoie (string, error). La crate Rust renvoie Result<PathBuf, ShellError>.
Le but du port Rust n’est pas la portabilité pour les utilisateurs. C’est la portabilité pour moi. Écrire les mêmes formes en Rust m’a forcé à regarder chaque défaut que je n’avais pas remarqué dans la version Go : variantes d’erreur, ergonomie du builder, types de chemin, ce qu’il faut dériver, ce qu’il faut exposer, ce qu’il faut garder privé à la crate.
L’objection évidente
“Il existe des bibliothèques pour ça.”
Oui. dirs, arboard, notify-rust, keyring-rs. Elles ont plus de contributeurs, plus de téléchargements, plus d’épreuves du feu qu’onyx n’en aura jamais. Si tu veux une dépendance sur laquelle t’appuyer, utilise-les.
Onyx existe pour la même raison qui pousse les gens à réécrire leur CV en texte brut tous les deux ans. Pas parce que le format compte. Parce que l’acte de le taper te force à te rappeler ce que tu fais.
Clôture
J’ai ouvert le code parce que livrer sous ton nom est la seule façon de garder le code honnête. README public. Conventions publiques. Erreurs publiques. La crate qui vit dans un dossier vendor privé pour toujours n’est pas un projet d’étude. C’est un brouillon.
Onyx est un brouillon que je me suis engagé à livrer.