Rust é uma linguagem de programação de sistemas de baixo nível com bom suporte de compilação cruzada, o que a torna um candidato principal para escrever aplicativos de linha de comando. Exemplos proeminentes variam de reimplementações de ferramentas amplamente utilizadas, como ripgrep , exa e bat para ferramentas de IU de terminal completas, como GitUI , Spotify TUI , Bandwhich , KMon e Diskonaut .

Com Alacritty e Nushell , existem até implementações populares de shell disponíveis. Alguns motivos para essa infinidade de ferramentas, que continua crescendo continuamente, incluem Reescrever em Rust (RIIR) meme e o fantástico ecossistema para escrever aplicativos de linha de comando Rust.

Ao falar sobre o ecossistema de biblioteca de Rust para CLIs, gostaria de mencionar Clap e TUI em particular, que são grampos e usados ​​em muitos das ferramentas acima mencionadas. Clap é um analisador de linha de comando com uma API fantástica e um grande conjunto de recursos disponíveis, muitos dos quais podem ser desativados para tempos de compilação mais rápidos se não forem necessários.

O que é TUI?

TUI é basicamente uma estrutura para construir interfaces de usuário de terminal. Suporta vários “back-ends” para desenhar no terminal. Esses back-ends assumem a lógica real para interagir com o terminal, como definir os caracteres corretos, limpar a tela etc., enquanto a TUI é uma interface de nível superior que fornece widgets e outros auxiliares para compor uma interface de usuário.

Neste tutorial, daremos uma olhada em como implementar um aplicativo de terminal simples usando TUI com Crossterm como backend. Não interagiremos diretamente com o Crossterm além da configuração inicial do pipeline de processamento e tratamento de eventos, então o exemplo deve funcionar com mudanças mínimas para os outros back-ends de TUI também.

Para demonstrar como a TUI funciona, construiremos um aplicativo simples para gerenciar seus animais de estimação usando um arquivo JSON local para armazenamento de dados. O produto acabado terá a seguinte aparência:

Exemplo de interface de linha de comando Rust criado com TUI

Exemplo de interface de linha de comando Rust construída com TUI

A primeira imagem mostra a tela de boas-vindas com o menu. Os caracteres destacados no menu mostram as teclas de atalho que o usuário precisa pressionar para executar as ações. Ao selecionar p , o usuário é encaminhado para a segunda tela (Animais de estimação), onde pode gerenciar seus animais de estimação. O usuário pode navegar pela lista, adicionar novos animais de estimação aleatórios usando a e excluir o animal atualmente selecionado usando d . Pressionar q fecha o aplicativo.

O aplicativo é bastante simples, mas é o suficiente para mostrar como a TUI funciona e como construir os blocos básicos de tal aplicativo. Usando o exemplo de linha de comando Rust code que escrevi para este tutorial, você poderia estender isso com edição, adicionar um formulário para adicionar novos animais de estimação, etc.

Ao usar a TUI, o aplicativo pode ser redimensionado e será alterado responsivamente, mantendo as proporções configuradas dos diferentes elementos da interface do usuário no lugar. Esta é uma das muitas coisas que a TUI faz quando você usa os widgets existentes.

Sem mais delongas, vamos mergulhar!

Configurando nosso aplicativo Rust

Para acompanhar, você só precisa de uma instalação recente do Rust (1.45+; a versão mais recente no momento da escrita é Rust 1.49.0 ).

Primeiro, crie um novo projeto Rust:

 cargo new rust-cli-example
cd rust-cli-example

A seguir, edite o arquivo Cargo.toml e adicione as dependências de que você precisará:

 [dependências]
crossterm={versão="0,19", recursos=["serde"]}
serde={versão="1.0", recursos=["derivar"]}
serde_json="1.0"
chrono={version="0.4", features=["serde"]}
rand={version="0.7.3", default-features=false, features=["std"]}
tui={version="0.14", default-features=false, features=['crossterm','serde']}
thiserror="1.0"

Além de usar a biblioteca TUI com Crossterm como backend. Também usaremos Serde por lidar com JSON, Chrono para lidar com a data de criação de nossos animais de estimação e Rand para criar novos dados aleatórios.

Como você pode ver na configuração acima, selecionamos os recursos crossterm e serde na TUI.

Vamos começar definindo algumas estruturas e constantes de dados básicas.

Definição de estruturas de dados

Primeiro, vamos definir uma constante para nosso arquivo JSON de”banco de dados”local e uma estrutura para definir a aparência de um animal de estimação:

 const DB_PATH: & str="./data/db.json"; # [derivar (serializar, desserializar, clonar)]
struct Pet { id: usize, nome: String, categoria: String, idade: usize, created_at: DateTime ,
}

Ao lidar com o arquivo de banco de dados, podemos encontrar erros de E/S. Embora a implementação do tratamento total de erros na IU para todos os erros possíveis esteja fora do escopo, ainda gostaríamos de ter alguns tipos de erros internos:

 # [derive (erro, depuração)]
pub enum Error { # [erro ("erro ao ler o arquivo DB: {0}")] ReadDBError (# [from] io:: Error), # [erro ("erro ao analisar o arquivo DB: {0}")] ParseDBError (# [from] serde_json:: Error),
}

O arquivo db.json é simplesmente uma representação JSON de uma lista de estruturas Pet :

 [ { "id": 1, "nome":"Chip", "categoria":"gatos", "idade": 4, "created_at":"2020-09-01T12: 00: 00Z" }, ...
]

Também precisamos de uma estrutura de dados para eventos de entrada. Usaremos a mesma abordagem que usamos nos exemplos de Crossterm dentro do Repositório de exemplos de TUI .

 enum Event  { Entrada (I), Carraça,
}

Um evento é uma entrada do usuário ou simplesmente um tique . Definiremos uma taxa de tick (por exemplo, 200 milissegundos) e se dentro dessa taxa de tick nenhum evento de entrada acontecer, emitiremos um Tick . Caso contrário, a entrada será emitida.

Finalmente, vamos definir um enum para a estrutura do menu para que possamos determinar facilmente onde estamos no aplicativo:

 # [derivar (copiar, clonar, depurar)]
enum MenuItem { Casa, Animais de estimação,
} impl De  para usize { fn from (input: MenuItem)-> usize { match input { MenuItem:: Home=> 0, MenuItem:: Animais de estimação=> 1, } }
}

Temos apenas duas páginas agora-Casa e Animais de estimação-e implementamos uma forma de convertê-las para usize . Isso nos permite usar o enum dentro do componente Tabs da TUI para destacar a guia atualmente selecionada no menu.

Com essa configuração inicial resolvida, vamos configurar a TUI e o Crossterm para que possamos começar a renderizar as coisas na tela e reagir aos eventos do usuário.

Renderização e entrada

Primeiro, defina o terminal para o modo raw (ou não canônico), o que elimina a necessidade de esperar por um Enter do usuário para reagir à entrada.

 fn main ()-> Resultado <(), Caixa > { enable_raw_mode (). expect ("pode ​​ser executado em modo bruto");
...

Em seguida, configure um canal mpsc (multiprodutor, consumidor único) para se comunicar entre o manipulador de entrada e o loop de renderização.

 let (tx, rx)=mpsc:: channel (); deixe tick_rate=Duração:: from_millis (200); thread:: spawn (mover || { deixe mut last_tick=Instant:: now (); ciclo { deixe tempo limite=tick_rate .checked_sub (last_tick.elapsed ()) .unwrap_or_else (|| Duração:: from_secs (0)); if event:: poll (timeout).expect ("poll funciona") { if let CEvent:: Key (key)=event:: read (). expect ("pode ​​ler eventos") { tx.send (Event:: Input (key)). expect ("pode ​​enviar eventos"); } } if last_tick.elapsed ()>=tick_rate { if let Ok (_)=tx.send (Event:: Tick) { last_tick=Instant:: now (); } } } });

Depois de criar o canal, defina a taxa de ticks mencionada acima e crie um tópico. É aqui que faremos nosso loop de entrada. Este snippet é a maneira recomendada de configurar um loop de entrada usando TUI e Crossterm.

A ideia é calcular o próximo tick ( timeout ), então usar event:: poll para esperar até aquela hora por um evento e se houver, enviar aquele evento de entrada através de nosso canal com a tecla que o usuário pressionou.

Se nenhum evento de usuário acontecer dentro desse tempo limite, simplesmente enviaremos um evento Tick e começaremos do início. Com o tick_rate , você pode ajustar a capacidade de resposta do seu aplicativo. Mas defini-lo muito baixo também significa que esse loop será executado muito e consumirá recursos.

Esta lógica é gerada em outro thread porque precisamos do nosso thread principal para renderizar o aplicativo. Dessa forma, nosso loop de entrada não bloqueia a renderização.

Existem algumas etapas necessárias para configurar um Terminal TUI com o backend Crossterm :

 let stdout=io:: stdout (); deixe backend=CrosstermBackend:: new (stdout); let terminal mut=Terminal:: new (backend) ?; terminal.clear () ?;

Definimos um CrosstermBackend usando stdout e o usamos em um Terminal TUI, limpando-o inicialmente e checando implicitamente se tudo funciona. Se isso falhar, simplesmente entramos em pânico e o aplicativo para; realmente não há uma boa maneira de reagir aqui, já que não recebemos a alteração para renderizar o evento.

Isso conclui a configuração padrão de que precisamos para criar um loop de entrada e um terminal para o qual podemos desenhar.

Vamos construir nosso primeiro elemento de IU-o menu baseado em guias!

Renderizando widgets em TUI

Antes de criar o menu, precisamos implementar um loop de renderização, que chama terminal.draw () em cada iteração.

 loop { terminal.draw (| rect | { let size=rect.size (); let chunks=Layout:: default () .direction (Direction:: Vertical) .margin (2) .restrições( [ Restrição:: Comprimento (3), Restrição:: Min (2), Restrição:: Comprimento (3), ] .as_ref (), ) .split (tamanho);
...

Fornecemos a função draw com um encerramento, que recebe um Rect . Este é simplesmente um layout primitivo para um retângulo usado na TUI, que é usado para definir onde os widgets devem ser renderizados.

O próximo passo é definir os chunks de nosso layout. Temos um layout vertical tradicional com três caixas:

  1. Menu
  2. Conteúdo
  3. rodapé

Podemos definir isso usando um Layout na TUI, definindo a direção e algumas restrições. Essas restrições definem como as diferentes partes do layout devem ser compostas. Em nosso exemplo, estamos definindo a parte do menu com comprimento 3 (para três linhas, essencialmente), a parte do conteúdo do meio como pelo menos 2 e o rodapé 3. Isso significa que, se você executar este aplicativo por completo tela, o menu e o rodapé sempre permanecerão constantes nas três linhas de altura, enquanto a parte do conteúdo aumentará para o restante do tamanho.

Essas restrições são um sistema poderoso e também existem opções para definir porcentagens e proporções, que veremos mais tarde. No final, dividimos o layout em pedaços de layout.

O componente de IU mais simples em nosso layout é o rodapé estático com direitos autorais falsos, então vamos começar com isso para ter uma ideia de como criar e renderizar um widget:

 let copyright=Paragraph:: new ("pet-CLI 2020-todos os direitos reservados") .style (Style:: default (). fg (Color:: LightCyan)) .alignment (Alignment:: Center) .quadra( Block:: default () .borders (Borders:: ALL) .style (Style:: default (). fg (Color:: White)) .title ("Copyright") .border_type (BorderType:: Plain), ); rect.render_widget (copyright, chunks [2]);

Usamos o widget Paragraph , que é um dos muitos widgets na TUI. Definimos o parágrafo com um texto codificado, definimos o estilo usando uma cor de primeiro plano diferente usando .fg no estilo padrão, definimos o alinhamento para o centro e, em seguida, definimos um bloco.

Este bloco é importante porque é um widget”base”, o que significa que pode ser usado por todos os outros widgets para renderização. Um bloco define uma área onde você pode colocar um título e uma borda opcional ao redor do conteúdo que você está renderizando dentro da caixa.

Nesse caso, criamos uma caixa com o título “Copyright” que tem bordas inteiras para criar nosso belo layout vertical de três caixas.

Então, usamos o rect do nosso método draw e chamamos render_widget , renderizando nosso parágrafo em pedaços [2] , que é a terceira parte do layout (a parte inferior).

Isso é tudo para renderizar widgets na TUI. Muito simples, certo?

Construindo um menu baseado em guias

Agora podemos finalmente abordar o menu baseado em guias. Felizmente, a TUI vem com um widget Tabs pronto para uso e não precisamos fazer muito para que funcione. Para gerenciar o estado em relação a qual item de menu está ativo, precisamos adicionar duas linhas antes do loop de renderização:

... let menu_titles=vec! ["Home","Pets","Add","Delete","Quit"]; let mut active_menu_item=MenuItem:: Home;
...
ciclo { terminal.draw (| rect | {
...

Na primeira linha, definimos nossos títulos de menu embutidos em código e active_menu_item armazena o item de menu, que está atualmente selecionado, definindo-o inicialmente como Home .

Uma vez que temos apenas duas páginas, isso só será definido como Home ou Pets , mas você provavelmente pode imaginar como essa abordagem poderia ser usada para roteamento de nível superior.

Para renderizar o menu, primeiro precisamos criar uma lista de Span (sim, como HTML tag ) elementos para segurar os rótulos de menu e colocá-los dentro de Tabs widget:

 let menu=menu_titles .iter () .map (| t | { let (primeiro, descanso)=t.split_at (1); Spans:: from (vec! [ Span:: estilizado ( primeiro, Style:: default () .fg (cor:: amarelo) .add_modifier (Modifier:: UNDERLINED), ), Span:: styled (rest, Style:: default (). Fg (Color:: White)), ]) }) .collect (); let tabs=Tabs:: new (menu) .select (active_menu_item.into ()) .block (Block:: default (). title ("Menu"). border (Borders:: ALL)) .style (Style:: default (). fg (Color:: White)) .highlight_style (Style:: default (). fg (Color:: Yellow)) .divider (Span:: raw ("|")); rect.render_widget (tabs, chunks [0]);

Fazemos iterações sobre os rótulos de menu codificados e, para cada um, dividimos a string no primeiro caractere. Fazemos isso para que possamos estilizar o primeiro caractere de maneira diferente, dando a ele uma cor diferente e um sublinhado, para indicar ao usuário que este é o caractere que ele precisa digitar para ativar este item de menu.

O resultado desta operação é um elemento Spans , que é simplesmente uma lista de Span . Isso nada mais é do que vários pedaços de texto com estilo opcional. Esses períodos são então colocados em um widget Tabs .

Chamamos .select () com o active_menu_item e definimos um estilo de destaque, que é diferente do estilo normal. Isso significa que se um item de menu for selecionado, será totalmente em amarelo, enquanto os não selecionados terão apenas o primeiro caractere em amarelo e o resto será branco. Também definimos uma divisão e novamente definimos um bloco básico com bordas e um título para manter nosso estilo consistente.

Da mesma forma que com os direitos autorais, renderizamos o menu de guias na primeira parte do layout usando chunks [0] .

Agora, dois dos nossos três elementos de layout estão prontos. No entanto, definimos um elemento com estado aqui com active_menu_item como armazenamento para o qual o elemento deve estar ativo. Como isso muda?

Vamos dar uma olhada no tratamento de entrada a seguir para resolver esse mistério.

Manipulação de entrada em TUI

Dentro do loop {} de renderização, após a chamada terminal.draw () ser concluída, adicionamos outro trecho de código: nosso tratamento de entrada.

Isso significa que sempre renderizamos o estado atual primeiro e, em seguida, reagimos à nova entrada. Fazemos isso simplesmente esperando por entradas na extremidade receptora de nosso canal , que configuramos no início:

 corresponde a rx.recv ()? { Event:: Input (event)=> match event.code { KeyCode:: Char ('q')=> { disable_raw_mode () ?; terminal.show_cursor () ?; pausa; } KeyCode:: Char ('h')=> active_menu_item=MenuItem:: Home, KeyCode:: Char ('p')=> active_menu_item=MenuItem:: Animais de estimação, _=> {} }, Evento:: Tick=> {} }
}//fim do loop de renderização

Quando um evento chega, nós combinamos com o código de entrada. Se o usuário pressionar q , queremos fechar o aplicativo, o que significa que queremos limpar, então devolvemos o terminal no mesmo estado em que o recebemos. Em um aplicativo do mundo real, essa limpeza também deve acontecer em quaisquer erros fatais. Caso contrário, o terminal permanece em modo bruto e parecerá bagunçado.

Desativamos o modo bruto e mostramos o cursor novamente, então o usuário deve estar em um estado de terminal “normal” como antes de iniciar o aplicativo.

Se encontrarmos um h , definimos o menu ativo como Home e se for p , definimos como Pets . Essa é toda a lógica de roteamento de que precisamos.

Além disso, dentro do encerramento terminal.draw , precisamos adicionar alguma lógica para este roteamento:

 corresponde ao item_menu_ativo { MenuItem:: Home=> rect.render_widget (render_home (), chunks [1]), MenuItem:: Animais de estimação=> { ... } }
... fn render_home <'a> ()-> Parágrafo <'a> { let home=Paragraph:: new (vec! [ Spans:: from (vec! [Span:: raw ("")]), Spans:: from (vec! [Span:: raw ("Welcome")]), Spans:: from (vec! [Span:: raw ("")]), Spans:: from (vec! [Span:: raw ("to")]), Spans:: from (vec! [Span:: raw ("")]), Spans:: from (vec! [Span:: styled ( "pet-CLI", Style:: default (). Fg (Color:: LightBlue), )]), Spans:: from (vec! [Span:: raw ("")]), Spans:: from (vec! [Span:: raw ("Pressione'p'para acessar animais de estimação,'a'para adicionar novos animais de estimação aleatórios e'd'para excluir o animal atualmente selecionado.")]), ]) .alignment (Alignment:: Center) .quadra( Block:: default () .borders (Borders:: ALL) .style (Style:: default (). fg (Color:: White)) .title ("Home") .border_type (BorderType:: Plain), ); casa
}

Nós combinamos em active_menu_item e, se for Home , simplesmente renderizamos uma mensagem básica de boas-vindas informando ao usuário onde ele está e dando-lhe instruções sobre como interagir com o app.

Isso é tudo de que precisamos agora em termos de tratamento de entrada. Se fôssemos executar isso, já poderíamos brincar com nossas guias. Mas estamos perdendo a essência de nosso aplicativo de gerenciamento de animais de estimação: o manejo de animais de estimação.

Criação de widgets com estado na TUI

Tudo bem, a primeira etapa é carregar os animais de estimação do arquivo JSON e exibir a lista de nomes de animais de estimação à esquerda, mostrando os detalhes do animal de estimação selecionado (o padrão é o primeiro), à direita.

Como uma isenção de responsabilidade aqui, devido ao escopo limitado deste tutorial, este aplicativo não lida com erros adequadamente; se a leitura do arquivo falhar, o aplicativo irá travar em vez de mostrar um erro útil. Essencialmente, o tratamento de erros=funcionaria da mesma forma que em outros aplicativos de IU: você ramifica no erro e, por exemplo, mostra ao usuário um erro útil Parágrafo , com uma dica para solucionar o problema ou um apelo à ação.

Com isso resolvido, vamos dar uma olhada na parte que falta da correspondência active_menu_item da seção anterior:

 corresponde ao item_menu_ativo { MenuItem:: Home=> rect.render_widget (render_home (), chunks [1]), MenuItem:: Animais de estimação=> { let pets_chunks=Layout:: default () .direction (Direction:: Horizontal) .restrições( [Restrição:: Porcentagem (20), Restrição:: Porcentagem (80)]. As_ref (), ) .split (pedaços [1]); let (esquerda, direita)=render_pets (& pet_list_state); rect.render_stateful_widget (à esquerda, pets_chunks [0], & mut pet_list_state); rect.render_widget (à direita, pets_chunks [1]); } }

Se estivermos na página Animais , criamos um novo layout. Desta vez, queremos um layout horizontal, pois queremos exibir dois elementos um ao lado do outro: a visualização de lista e a visualização de detalhes. Nesse caso, queremos que a exibição de lista ocupe cerca de 20% da tela, deixando o restante para a tabela de detalhes.

Observe que, em .split , em vez do tamanho do retângulo, usamos chunks [1] . Isso significa que dividimos o retângulo que descreve a área de chunks [1] (nosso bloco de layout Content do meio) em duas visualizações horizontais. Em seguida, chamamos render_pets , retornando-nos as partes esquerda e direita para renderizar e simplesmente os renderizamos nos pets_chunks correspondentes.

Mas espere, o que é isso? Chamamos render_stateful_widget para nossa visualização de lista. O que isso significa?

TUI, sendo a estrutura completa e fantástica que é, tem a capacidade de criar widgets com estado. O widget List é um dos widgets que podem ter estado. Para que isso funcione, precisamos criar um ListState primeiro, antes do loop de renderização:

 let mut pet_list_state=ListState:: default (); pet_list_state.select (Some (0)); ciclo {
...

Inicializamos o estado da lista de animais de estimação e selecionamos o primeiro item por padrão. Este pet_list_state também é o que passamos para a função render_pets . Esta função faz toda a lógica para:

  • Buscando animais de estimação no arquivo de banco de dados
  • Convertendo-os em itens de lista
  • Criação da lista de animais de estimação
  • Encontrar o animal de estimação atualmente selecionado
  • Renderizando uma mesa com os dados do animal de estimação selecionado
  • Retornando ambos os widgets

Isso é bastante, então o código é um pouco longo, mas vamos examiná-lo depois:

 fn read_db ()-> Resultado , Erro> { let db_content=fs:: read_to_string (DB_PATH) ?; vamos analisar: Vec =serde_json:: from_str (& db_content) ?; Ok (analisado)
} fn render_pets <'a> (pet_list_state: & ListState)-> (Lista <'a>, Tabela <'a>) { let pets=Block:: default () .borders (Borders:: ALL) .style (Style:: default (). fg (Color:: White)) .title ("Animais de estimação") .border_type (BorderType:: Plain); deixe pet_list=read_db (). expect ("pode ​​buscar a lista de animais"); deixar itens: Vec <_>=pet_list .iter () .map (| pet | { ListItem:: new (Spans:: from (vec! [Span:: styled ( pet.name.clone (), Style:: default (), )])) }) .collect (); deixe selected_pet=pet_list .pegue( pet_list_state .selecionado() .expect ("há sempre um animal de estimação selecionado"), ) .expect ("existe") .clone(); let list=List:: new (items).block (pets).highlight_style ( Style:: default () .bg (cor:: amarelo) .fg (cor:: preto) .add_modifier (Modifier:: BOLD), ); let pet_detail=Table:: new (vec! [Row:: new (vec! [ Cell:: from (Span:: raw (selected_pet.id.to_string ())), Cell:: from (Span:: raw (selected_pet.name)), Cell:: from (Span:: raw (selected_pet.category)), Cell:: from (Span:: raw (selected_pet.age.to_string ())), Cell:: from (Span:: raw (selected_pet.created_at.to_string ())), ])]) .header (Row:: new (vec! [ Cell:: from (Span:: styled ( "EU IRIA", Style:: default (). Add_modifier (Modifier:: BOLD), )), Cell:: from (Span:: styled ( "Nome", Style:: default (). Add_modifier (Modifier:: BOLD), )), Cell:: from (Span:: styled ( "Categoria", Style:: default (). Add_modifier (Modifier:: BOLD), )), Cell:: from (Span:: styled ( "Era", Style:: default (). Add_modifier (Modifier:: BOLD), )), Cell:: from (Span:: styled ( "Criado em", Style:: default (). Add_modifier (Modifier:: BOLD), )), ])) .quadra( Block:: default () .borders (Borders:: ALL) .style (Style:: default (). fg (Color:: White)) .title ("Detalhe") .border_type (BorderType:: Plain), ) .widths (& [ Restrição:: Porcentagem (5), Restrição:: Porcentagem (20), Restrição:: Porcentagem (20), Restrição:: Porcentagem (5), Restrição:: Porcentagem (20), ]); (lista, pet_detail)
}

Primeiro, definimos a função read_db , que simplesmente lê o arquivo JSON e o analisa em um Vec de Pets.

Então, dentro de render_pets , que retorna uma tupla de List e Table (ambos widgets TUI), definimos o circundante animais bloquear para a exibição de lista primeiro.

Depois de obter os dados do animal de estimação, transformamos os nomes do animal em ListItems . Em seguida, tentamos encontrar o animal de estimação selecionado dentro da lista com base no pet_list_state . Se isso falhar ou se não tivermos um animal de estimação, o aplicativo irá travar nesta versão simples, já que não temos nenhum tratamento de erros significativo, conforme mencionado acima.

Assim que tivermos o animal de estimação selecionado, criamos o widget Lista com os itens da lista, definindo um estilo em destaque, para que possamos ver qual animal está selecionado no momento.

Finalmente, criamos a tabela pet_details , onde definimos um cabeçalho embutido em código para os nomes das colunas da estrutura pet. Também definimos uma lista de Rows , que contém os dados de cada animal transformados em uma string.

Renderizamos esta tabela em um bloco básico com o título Detalhes e uma borda e definimos as larguras relativas das cinco colunas usando porcentagens. Portanto, esta tabela também se comporta de forma responsiva no redimensionamento.

Essa é toda a lógica de renderização para os animais de estimação. As únicas coisas que faltam adicionar são a funcionalidade para adicionar novos bichinhos aleatórios e excluir os bichinhos selecionados para dar ao app um pouco mais de interatividade.

Primeiro, adicionamos duas funções auxiliares para adicionar e excluir animais de estimação:

 fn add_random_pet_to_db ()-> Resultado , Erro> { deixe mut rng=rand:: thread_rng (); let db_content=fs:: read_to_string (DB_PATH) ?; deixe mut analisado: Vec =serde_json:: from_str (& db_content) ?; deixe catsdogs=match rng.gen_range (0, 1) { 0=>"gatos", _=>"cachorros", }; let random_pet=Pet { id: rng.gen_range (0, 9999999), nome: rng.sample_iter (alfanumérico).take (10).collect (), categoria: catsdogs.to_owned (), idade: rng.gen_range (1, 15), created_at: Utc:: now (), }; parsed.push (random_pet); fs:: write (DB_PATH, & serde_json:: to_vec (& parsed)?) ?; Ok (analisado)
} fn remove_pet_at_index (pet_list_state: & mut ListState)-> Resultado <(), Erro> { if let Some (selected)=pet_list_state.selected () { let db_content=fs:: read_to_string (DB_PATH) ?; deixe mut analisado: Vec =serde_json:: from_str (& db_content) ?; parsed.remove (selecionado); fs:: write (DB_PATH, & serde_json:: to_vec (& parsed)?) ?; pet_list_state.select (Alguns (selecionados-1)); } Está bem(())
}

Em ambos os casos, carregamos a lista de animais de estimação, manipulamos e gravamos de volta no arquivo. No caso de remove_pet_at_index , também precisamos decrementar o pet_list_state , portanto, se estivermos no último animal da lista, saltaremos para o anterior automaticamente.

Finally, we need to add input handlers for navigating in the pet list using Up and Down, as well as using a and d for adding and deleting pets:

 KeyCode::Char('a')=> { add_random_pet_to_db().expect("can add new random pet"); } KeyCode::Char('d')=> { remove_pet_at_index(&mut pet_list_state).expect("can remove pet"); } KeyCode::Down=> { if let Some(selected)=pet_list_state.selected() { let amount_pets=read_db().expect("can fetch pet list").len(); if selected >=amount_pets-1 { pet_list_state.select(Some(0)); } outro { pet_list_state.select(Some(selected + 1)); } } } KeyCode::Up=> { if let Some(selected)=pet_list_state.selected() { let amount_pets=read_db().expect("can fetch pet list").len(); if selected > 0 { pet_list_state.select(Some(selected-1)); } outro { pet_list_state.select(Some(amount_pets-1)); } } }

In the Up and Down cases, we need to make sure to not underflow or overflow the list. In this simple example, we just read the pet list each time these keys are pressed, which is very inefficient, but simple enough. In a real app, the current amount of pets could be held in shared memory somewhere, for example, but for this tutorial, this naive way should suffice.

In any case, we simply increment, or decrement in the case of Up, the pet_list_state selection, which will enable the user to use Up and Down to scroll through the list of pets. If you start the application using cargo run, you can test out the different key binds, add some random pets using a, navigate to them and delete them again using d.

You can find the full example code at GitHub.

Conclusion

As I mentioned in the introduction of this tutorial, Rust’s ecosystem for creating terminal applications is fantastic and I hope this post helped you catch a glimpse of what you can do with powerful libraries such as TUI and Crossterm.

I’m a big fan of command-line applications. They’re usually fast, lightweight, and minimal and, as a Vim user, I’m not afraid of using my keyboard to navigate and interact with an application. Another great advantage of terminal UIs is that they can also be used in a headless environment, such as on a remote server you’re SSHed in, or in a recovery/debug scenario.

Rust and its rich ecosystem makes building command-line utilities a breeze and I’m excited for what people will build and publish in this area in the future.

The post Rust and TUI: Building a command-line interface in Rust appeared first on LogRocket Blog.

Source link