AkiraAkira.dev
3 min de lecture

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.

aussi en EN PT

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 :

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.

share
Caption copied — paste in the compose box