Construí o onyx para aprender, não para competir
A mesma superfície de módulos em Go e Rust. paths, files, shell, clipboard, notify, keyring. Duas crates que existem porque as escrevi.
Esta semana abri o código de duas crates. Um módulo Go, uma crate Rust. Mesmo nome, mesma superfície: osinfo, paths, files, shell, clipboard, notify, keyring. O conjunto chama-se onyx. Código em github.com/akira-io/onyx (Go) e github.com/akira-io/onyx-rs (Rust).
Não o construí para competir com nada.
O onyx é um projeto de estudo que deixei escapar para MIT. O ponto era a forma da cola cross-platform de desktop, não o mercado. dirs, arboard, notify-rust, keyring-rs. Existem todas. Funcionam todas. Ler as bibliotecas dos outros ensina-te como eles pensam. Escrever a tua ensina-te no que acreditas.
O problema
Cada aplicação de desktop que escrevi reescreve os mesmos shims de SO:
- Encontrar o diretório de config do utilizador.
- Abrir um ficheiro com a app por omissão.
- Revelá-lo no gestor de ficheiros.
- Resolver um binário de CLI no PATH com fallbacks específicos da plataforma.
- Ler e escrever o clipboard.
- Mostrar uma notificação.
- Guardar um segredo no keyring do sistema.
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
}
Esse bloco vive em cada app de desktop que entreguei. O custo não são as linhas de código. O custo é o imposto cognitivo de confirmar, outra vez, que SO o Linux significa e que caminho o Windows quer esta semana.
A regra
O onyx substitui esse bloco por uma linha.
// github.com/akira-io/onyx/paths
config, err := paths.For("Hyperion").Config()
Uma função. Um verbo. Uma forma de retorno. O Resolver segue a mesma disciplina.
// github.com/akira-io/onyx/shell
claude, err := shell.NewResolver().
Lookup("claude").
Lookup("/opt/homebrew/bin/claude").
Resolve()
Lookup aceita ou um nome resolvido via PATH ou um caminho absoluto verificado como ficheiro. O package descobre qual porque a diferença é mecânica: se a string tem um separador, é um caminho. Quem chama não escolhe.
Não acho que esta seja a melhor API. Acho que é a API à qual vou pegar, o que é uma afirmação diferente e mais útil.
Porquê o espelho em Rust
O onyx-rs é o mesmo exercício numa segunda linguagem. Mesmos nomes de módulos, mesmas convenções, idiomático em cada uma. paths.For("Hyperion").Config() do lado Go torna-se paths::for_app("Hyperion").config() do lado Rust. shell.NewResolver() torna-se shell::Resolver::new(). O módulo Go devolve (string, error). A crate Rust devolve Result<PathBuf, ShellError>.
O ponto do port em Rust não é portabilidade para os utilizadores. É portabilidade para mim. Escrever as mesmas formas em Rust obrigou-me a olhar para cada default que não tinha reparado na versão Go: variantes de erro, ergonomia de builder, tipos de caminho, o que derivar, o que expor, o que manter privado à crate.
O contra-argumento óbvio
“Já há bibliotecas para isto.”
Sim. dirs, arboard, notify-rust, keyring-rs. Têm mais contribuidores, mais downloads, mais teste de batalha do que o onyx alguma vez terá. Se queres uma dependência em que te apoiar, usa-as.
O onyx existe pela mesma razão que leva as pessoas a reescrever o currículo em texto simples de dois em dois anos. Não porque o formato importa. Porque o ato de o escrever obriga-te a lembrar o que fazes.
Fecho
Abri o código porque entregar sob o teu nome é a única forma de manteres o código honesto. README público. Convenções públicas. Erros públicos. A crate que vive numa pasta privada de vendor para sempre não é um projeto de estudo. É um rascunho.
O onyx é um rascunho que me comprometi a entregar.