React dla terminala w Rust z Flexbox i Hooks
Jeśli kiedykolwiek pisałeś interfejs konsolowy w Rust, prawdopodobnie używałeś Ratatui. To solidna biblioteka, która napędza dziesiątki świetnych narzędzi. Ale gdy projekt wykracza poza kilka przycisków, zaczyna się znajoma żmudna praca: musisz ręcznie dzielić ekran na prostokąty używając Layout, obliczać procenty i marginesy oraz przeprowadzać zdarzenia przez masywne konstrukcje match. W tworzeniu stron internetowych od dawna jesteśmy przyzwyczajeni do podejścia opartego na komponentach i flexbox, ale w terminalu wciąż często piszemy tak, jakby był rok 1998.
Odkryłem ostatnio crate iocraft, którego autor rozwiązał ten problem radykalnie. Przeniósł doświadczenie z React, SwiftUI i Ink bezpośrednio do świata programowania systemowego w Rust.
Znana komponentowa architektura
Biblioteka opiera się na makrze element!. Buduje drzewo elementów używając składni, która mocno przypomina JSX lub deklaratywny Swift. Bez ręcznych obliczeń współrzędnych. Układ obsługuje silnik taffy, który wiernie implementuje specyfikację flexbox dla Rust.
Oto jak wygląda klasyczny Hello World:
use iocraft::prelude::*;
fn main() {
element! {
View(
border_style: BorderStyle::Round,
border_color: Color::Blue,
) {
Text(content: "Hello, world!")
}
}
.print();
}
Po prostu owijamy Text w kontener View, nadajemy mu zaokrąglone niebieskie obramowanie i wywołujemy .print(). Wynik trafia na standardowe wyjście bez potrzeby przechwytywania terminala w trybie pełnoekranowym. To przydatne, gdy potrzebujesz czytelnie sformatowanego wyniku polecenia konsolowego lub logów zamiast interaktywnego pulpitu.
Hooks i stan wewnątrz terminala
Podobieństwo do frontend developmentu nie kończy się na statycznym układzie. Dla dynamicznych interfejsów autor dodał makro #[component] i znajome hooks cyklu życia.
W poniższym kodzie komponent Counter przechowuje swój własny lokalny stan poprzez use_state i uruchamia asynchroniczny timer w tle poprzez use_future:
use iocraft::prelude::*;
use std::time::Duration;
#[component]
fn Counter(mut hooks: Hooks) -> impl Into<AnyElement<'static>> {
let mut count = hooks.use_state(|| 0);
hooks.use_future(async move {
loop {
smol::Timer::after(Duration::from_millis(100)).await;
count += 1;
}
});
element! {
Text(color: Color::Blue, content: format!("counter: {}", count))
}
}
fn main() {
smol::block_on(element!(Counter).render_loop()).unwrap();
}
Gdy wartość count ulega zmianie, iocraft wywołuje przerysowanie odpowiedniej części ekranu. Silnik optymalizuje przekazywanie propsów: kontekst i parametry są przekazywane przez referencję, więc nie występują tutaj niepotrzebne alokacje pamięci ani klonowanie ciężkich struktur.
Co biblioteka potrafi w praktyce
Wbudowane prymitywy wystarczają do budowania interfejsów dowolnej złożoności. Przykłady w repozytorium zawierają gotowe przepisy na najczęstsze zadania:
- Aplikacje pełnoekranowe z wprowadzaniem tekstu i obsługą fokusu
- Wielokolumnowe adaptacyjne tabele
- Interaktywne formularze z walidacją
- Kalkulatory i nakładki z pływającymi oknami modalnymi
Zobaczmy, jak wyglądają złożone układy elementów w praktyce:
Stylowanie jest wieloplatformowe. Aplikacje renderują się poprawnie zarówno w standardowych terminalach Linux/macOS, jak i w Windows PowerShell.
Czy warto przejść z Ratatui
Jeśli masz już złożonego klienta terminalowego napisanego w Ratatui, nie ma większego sensu przepisywać go od zera. Ratatui jest bardziej dojrzałe, z dużą społecznością wokół niego i dziesiątkami zewnętrznych widżetów napisanych dla niego.
Ale jeśli zaczynasz nowe narzędzie CLI, piszesz wewnętrzne narzędzie dla swojego zespołu lub po prostu masz dość ręcznego liczenia wierszy i kolumn terminala, iocraft oszczędza dużo czasu. Deklaratywna składnia sprawia, że kod jest bardziej zwięzły, a flexbox eliminuje problemy z rozmiarem okien.
Projekt jest rozpowszechniany na podwójnej licencji MIT / Apache-2.0. Dokumentacja na docs.rs jest wystarczająco szczegółowa, a kod źródłowy przykładów w repozytorium jasno pokazuje, jak budować własne komponenty.
Powiązane projekty