/pt-br/pub/fosforo-on-the-app-store.md


fosforo na App Store: um terminal com shell próprio

· #c #swift #terminal #ssh #desenvolvimento

O fosforo está na App Store. Grátis, para iPad e iPhone; no Mac é um cask do Homebrew. É um emulador de terminal em C e Swift, desenhado com Metal, e no iOS ele carrega o próprio shell, o próprio cliente SSH e o próprio cliente Mosh. Abra o app num iPad sem rede e você tem um prompt, um filesystem, pipes e uns sessenta comandos. Essa é a parte de que eu mais gosto.

fosforo num iPad, com o shell rocchetto e o gato Gohan de banner, e num iPhone com um monitor de sistema aberto por SSH

O repositório foi criado em 29 de setembro. O primeiro build subiu para o App Store Connect em 8 de outubro, e a 1.0.1 foi aprovada em 20 de outubro.

Um shell que roda no iOS

O iOS não tem fork nem exec. Não existe /bin/sh para abrir e não existe como abrir processo nenhum. Então o fosforo carrega o shell dentro do processo: o rocchetto, um shell POSIX escrito em C, sem libc por baixo do núcleo. O mesmo código roda como binário de desktop, numa aba de browser em wasm, numa placa de microcontrolador com tela de 20x8 e dentro do fosforo como biblioteca estática. Pipes, redirects, $(...), funções, if/for/while, histórico, Tab completion, editor de linha.

mobile@iPad:~$ ls /bin | cut -c1 | sort | uniq -c | sort -rn | head -3
     10 c
      6 t
      5 s

Esse pipeline roda no iPad, offline, sem servidor nenhum por trás. Cada estágio é um programa no /bin.

Os comandos são Filo

ls, grep, sed, sort, find, wc, tr, xargs e o resto dos 64 comandos do /bin não são C. São programas em Filo, compilados para bytecode da VM do Filo e embarcados no app. O que está no /bin é o programa compilado; o fonte de cada um fica em commands/bin no repositório do rocchetto, grep.filo incluso.

Filo é a minha linguagem de script. Nasceu para dar poder de script ao usuário sem entregar a máquina, que é exatamente a restrição que uma loja de apps impõe. E se mostrou um bom encaixe para o userland de um shell: um comando é um programa curto, o conjunto inteiro compila em segundos, e um bug no sort é um fix de um arquivo só. expr, sed e date foram os que deram trabalho de verdade; o resto saiu rápido depois que o padrão ficou pronto.

A configuração do terminal também é Filo. O ~/.config/fosforo/init.filo define fonte, grid, cores, bell. Temas são arquivos. E existem hooks de eventos, então o terminal reage sem eu precisar inventar um checkbox para cada caso:

(set on-blur (fn (arg) (theme "phosphor")))
(set on-focus (fn (arg) (theme "default")))

Perdeu o foco, a janela fica verde fósforo. Voltou, volta. Quatro linhas, nenhuma tela de settings.

No Mac nada disso é necessário e nada disso está lá. O app do Mac é emulador e só: o seu login shell num pty, o ssh do sistema, o git do sistema. Terminal que traz o próprio shell para uma máquina que já tem um está resolvendo um problema que a máquina não tem.

SSH e Mosh dentro do app

O ssh do iPad é o cliente do próprio app, escrito em Swift sobre o CryptoKit. Key exchange curve25519, e o híbrido mlkem768x25519-sha256 quando o servidor oferece; AES-GCM; strict kex; host keys ed25519, ECDSA e RSA; known_hosts com trust on first use; ~/.ssh/config com a semântica de sempre, todo Host que casa aplica e o primeiro valor vence; ProxyJump; -L, -R e -D; agent forwarding em que a assinatura acontece no aparelho e a chave nunca sai dele. Um segundo ssh para um host já conectado compartilha a conexão e abre na hora.

O Mosh é a mesma história uma camada abaixo: SSP sobre UDP com OCB3, o wire format feito à mão, testado contra um servidor de verdade com um terço dos pacotes descartados de propósito. O local echo aparece acima de 30 ms de RTT e sublinha acima de 80 ms, e a única regra que importa: depois do Return nada é previsto até o servidor confirmar um palpite, que é o que mantém a senha fora da tela. Testei isso com read -s contra um servidor real antes de confiar.

A rede de revisão da Apple é só IPv6, com NAT64. O cliente SSH resolve AF_INET e AF_INET6 em paralelo e fica com quem responder primeiro; o Mosh depois usa o endereço por onde a sessão SSH chegou. Descobri isso nas diretrizes antes de o revisor descobrir no app, que é a ordem barata.

Chaves no Secure Enclave

As chaves ficam em ~/.ssh dentro do app, modo 0600, sob a proteção de arquivo mais estrita que o iOS tem, e fora do backup do aparelho, para não viajarem para o aparelho seguinte. Esse é o padrão e a chave é um arquivo comum: ssh-keygen -t ed25519 no iPad funciona como num computador, e o ssh-copy-id também está lá.

key protect NOME vai além. A chave é selada pelo Secure Enclave deste aparelho, e cada uso pede Face ID, Touch ID ou o código. Copiou o arquivo para outro lugar, virou ruído. O app nunca vê dado biométrico; quem pergunta é o sistema.

Nada é movido, apagado ou cifrado sem um comando e uma confirmação. Não gosto de software que faz coisa sozinho, e não escrevi nenhum.

Nada coletado

Sem analytics, sem crash report, sem conta, sem anúncio, sem assinatura. O rótulo de privacidade na loja diz “Dados não coletados” e é isso mesmo: o app só conecta nas máquinas que você nomear, ou que um programa que você rodar pedir.

O terminal em si

O mesmo núcleo desenha as duas plataformas. O vt/ é um parser VT em C11 escrito do zero, sem I/O e sem estado global, passado por fuzz, uns 600 MB/s de ASCII puro neste Mac. O plano inicial era portar o parser de um terminal antigo meu; tinha lacunas demais, então as lições ficaram e o código não.

Box drawing, blocos, sombras, braille e os separadores do Powerline são desenhados por geometria, não tirados da fonte, então arte ANSI não tem emenda entre células. Eu me importo com isso mais do que é razoável. A fonte padrão é a IBM 3270. Programas de tela cheia renderizam como devem.

No Mac: várias sessões por janela, ⌘F pelo scrollback, um copy mode com hjkl, ⌘↑/⌘↓ entre prompts quando o shell emite OSC 133, notificação quando um comando longo termina numa sessão que não está na frente. O app do Mac não está na Mac App Store: o sandbox não cabe num terminal que roda o seu login shell. Ele é assinado, notarizado e distribuído como cask.

A revisão

Aprovado na primeira revisão. As notas ao revisor explicavam o que o shell é: roda scripts que o usuário escreve e não baixa funcionalidade, que é o que a diretriz 2.5.2 pede. Um login num servidor de demonstração deixou os comandos de SSH e Mosh serem exercitados. Uma coisa que aprendi no caminho: ícone de iOS não pode ter canal alfa, e o script que desenha o ícone agora tira o alfa no iOS e mantém no macOS.

Onde pegar


Cesar Gimenes


crg.eti.br · © 2026 Cesar Gimenes · CC BY 4.0 · github · en