---
title: "Gerenciador de Tarefas CLI"
url: "https://rustlang.com.br/projetos/todo-cli/"
markdown_url: "https://rustlang.com.br/projetos/todo-cli.MD"
description: "Construa um gerenciador de tarefas completo para o terminal em Rust com persistência em JSON, filtros e saída colorida."
date: "2026-02-24"
author: "Equipe Rust Brasil"
---

# Gerenciador de Tarefas CLI

Construa um gerenciador de tarefas completo para o terminal em Rust com persistência em JSON, filtros e saída colorida.


Neste projeto, vamos construir um gerenciador de tarefas (to-do list) completo para a linha de comando. Aplicativos de gerenciamento de tarefas são um clássico do aprendizado de programação porque cobrem operações CRUD (criar, listar, atualizar, deletar), persistência de dados e uma interface com o usuário. Em Rust, esse projeto é uma excelente oportunidade para praticar serialização com `serde`, manipulação de arquivos e o uso elegante de enums para comandos.

Ao final deste walkthrough, você terá uma ferramenta prática que pode usar no seu dia a dia para organizar tarefas diretamente do terminal.

## O Que Vamos Construir

Nosso gerenciador `tarefas` terá os seguintes recursos:

- Adicionar tarefas com descrição e prioridade
- Listar todas as tarefas com status colorido
- Marcar tarefas como concluídas
- Remover tarefas por ID
- Filtrar por status (pendente/concluída)
- Persistência automática em arquivo JSON
- Exibição formatada com cores no terminal

## Estrutura do Projeto

```
tarefas/
├── Cargo.toml
└── src/
    ├── main.rs
    ├── cli.rs
    ├── tarefa.rs
    └── armazenamento.rs
```

## Configurando o Projeto

```bash
cargo new tarefas
cd tarefas
```

Configure o `Cargo.toml`:

```toml
[package]
name = "tarefas"
version = "0.1.0"
edition = "2021"

[dependencies]
clap = { version = "4", features = ["derive"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
colored = "2"
chrono = { version = "0.4", features = ["serde"] }
```

Usamos `serde` e `serde_json` para serializar as tarefas em JSON, `chrono` para registrar datas e `colored` para a saída no terminal.

## Passo 1: Modelando a Tarefa

O módulo `tarefa.rs` define a estrutura de dados central do nosso aplicativo.

```rust
// src/tarefa.rs
use chrono::{DateTime, Local};
use serde::{Deserialize, Serialize};
use std::fmt;

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum Prioridade {
    Baixa,
    Media,
    Alta,
}

impl fmt::Display for Prioridade {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Prioridade::Baixa => write!(f, "Baixa"),
            Prioridade::Media => write!(f, "Média"),
            Prioridade::Alta => write!(f, "Alta"),
        }
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Tarefa {
    pub id: u32,
    pub descricao: String,
    pub concluida: bool,
    pub prioridade: Prioridade,
    pub criada_em: DateTime<Local>,
    pub concluida_em: Option<DateTime<Local>>,
}

impl Tarefa {
    pub fn nova(id: u32, descricao: String, prioridade: Prioridade) -> Self {
        Self {
            id,
            descricao,
            concluida: false,
            prioridade,
            criada_em: Local::now(),
            concluida_em: None,
        }
    }

    pub fn concluir(&mut self) {
        self.concluida = true;
        self.concluida_em = Some(Local::now());
    }
}
```

Usamos `#[derive(Serialize, Deserialize)]` para que o `serde` possa converter automaticamente nossas structs de e para JSON. O campo `concluida_em` é `Option<DateTime<Local>>` — `None` enquanto a tarefa está pendente e `Some(data)` quando concluída.

## Passo 2: Persistência com JSON

O módulo `armazenamento.rs` cuida de ler e escrever as tarefas no disco.

```rust
// src/armazenamento.rs
use crate::tarefa::Tarefa;
use std::fs;
use std::io;
use std::path::PathBuf;

/// Retorna o caminho do arquivo de dados
fn caminho_arquivo() -> PathBuf {
    let mut caminho = dirs_or_default();
    caminho.push("tarefas.json");
    caminho
}

/// Tenta usar o diretório home; caso contrário, usa o diretório atual
fn dirs_or_default() -> PathBuf {
    std::env::var("HOME")
        .map(PathBuf::from)
        .unwrap_or_else(|_| PathBuf::from("."))
}

/// Carrega as tarefas do arquivo JSON
pub fn carregar() -> io::Result<Vec<Tarefa>> {
    let caminho = caminho_arquivo();

    if !caminho.exists() {
        return Ok(Vec::new());
    }

    let conteudo = fs::read_to_string(&caminho)?;

    if conteudo.trim().is_empty() {
        return Ok(Vec::new());
    }

    let tarefas: Vec<Tarefa> = serde_json::from_str(&conteudo)
        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;

    Ok(tarefas)
}

/// Salva as tarefas no arquivo JSON
pub fn salvar(tarefas: &[Tarefa]) -> io::Result<()> {
    let caminho = caminho_arquivo();
    let json = serde_json::to_string_pretty(tarefas)
        .map_err(|e| io::Error::new(io::ErrorKind::Other, e))?;

    fs::write(&caminho, json)?;
    Ok(())
}

/// Gera o próximo ID disponível
pub fn proximo_id(tarefas: &[Tarefa]) -> u32 {
    tarefas.iter().map(|t| t.id).max().unwrap_or(0) + 1
}
```

A função `carregar` lê o arquivo JSON e desserializa para um `Vec<Tarefa>`. Se o arquivo não existir, retornamos um vetor vazio. A função `salvar` serializa as tarefas com `to_string_pretty` para manter o arquivo legível.

## Passo 3: Interface de Linha de Comando

O módulo `cli.rs` define os subcomandos do aplicativo.

```rust
// src/cli.rs
use clap::{Parser, Subcommand, ValueEnum};

#[derive(Parser, Debug)]
#[command(name = "tarefas")]
#[command(about = "Gerenciador de tarefas para o terminal")]
pub struct Cli {
    #[command(subcommand)]
    pub comando: Comando,
}

#[derive(Subcommand, Debug)]
pub enum Comando {
    /// Adiciona uma nova tarefa
    Adicionar {
        /// Descrição da tarefa
        descricao: String,

        /// Prioridade: baixa, media, alta
        #[arg(short, long, default_value = "media")]
        prioridade: NivelPrioridade,
    },

    /// Lista as tarefas
    Listar {
        /// Filtrar por status: todas, pendentes, concluidas
        #[arg(short, long, default_value = "todas")]
        filtro: FiltroStatus,
    },

    /// Marca uma tarefa como concluída
    Concluir {
        /// ID da tarefa
        id: u32,
    },

    /// Remove uma tarefa
    Remover {
        /// ID da tarefa
        id: u32,
    },

    /// Limpa todas as tarefas concluídas
    Limpar,
}

#[derive(Debug, Clone, ValueEnum)]
pub enum NivelPrioridade {
    Baixa,
    Media,
    Alta,
}

#[derive(Debug, Clone, ValueEnum)]
pub enum FiltroStatus {
    Todas,
    Pendentes,
    Concluidas,
}
```

Usamos o recurso de subcomandos do `clap` — cada ação (adicionar, listar, concluir, remover, limpar) é um subcomando com seus próprios argumentos.

## Passo 4: Juntando Tudo no main.rs

```rust
// src/main.rs
mod armazenamento;
mod cli;
mod tarefa;

use clap::Parser;
use cli::{Cli, Comando, FiltroStatus, NivelPrioridade};
use colored::*;
use tarefa::{Prioridade, Tarefa};

fn main() {
    let cli = Cli::parse();

    let mut tarefas = match armazenamento::carregar() {
        Ok(t) => t,
        Err(e) => {
            eprintln!("{} Erro ao carregar tarefas: {}", "ERRO:".red().bold(), e);
            std::process::exit(1);
        }
    };

    match cli.comando {
        Comando::Adicionar {
            descricao,
            prioridade,
        } => {
            let nivel = match prioridade {
                NivelPrioridade::Baixa => Prioridade::Baixa,
                NivelPrioridade::Media => Prioridade::Media,
                NivelPrioridade::Alta => Prioridade::Alta,
            };
            let id = armazenamento::proximo_id(&tarefas);
            let nova = Tarefa::nova(id, descricao.clone(), nivel);
            tarefas.push(nova);
            println!(
                "{} Tarefa #{} adicionada: {}",
                "OK".green().bold(),
                id,
                descricao
            );
        }

        Comando::Listar { filtro } => {
            let filtradas: Vec<&Tarefa> = match filtro {
                FiltroStatus::Todas => tarefas.iter().collect(),
                FiltroStatus::Pendentes => tarefas.iter().filter(|t| !t.concluida).collect(),
                FiltroStatus::Concluidas => tarefas.iter().filter(|t| t.concluida).collect(),
            };

            if filtradas.is_empty() {
                println!("{}", "Nenhuma tarefa encontrada.".yellow());
                return;
            }

            println!(
                "{:<5} {:<10} {:<10} {}",
                "ID".bold(),
                "Status".bold(),
                "Prior.".bold(),
                "Descrição".bold()
            );
            println!("{}", "-".repeat(60));

            for tarefa in &filtradas {
                let status = if tarefa.concluida {
                    "Concluída".green().to_string()
                } else {
                    "Pendente".yellow().to_string()
                };

                let prioridade = match tarefa.prioridade {
                    Prioridade::Alta => "Alta".red().to_string(),
                    Prioridade::Media => "Média".yellow().to_string(),
                    Prioridade::Baixa => "Baixa".blue().to_string(),
                };

                println!(
                    "{:<5} {:<10} {:<10} {}",
                    tarefa.id, status, prioridade, tarefa.descricao
                );
            }

            println!(
                "\n{} tarefa(s) no total.",
                filtradas.len().to_string().bold()
            );
        }

        Comando::Concluir { id } => {
            if let Some(tarefa) = tarefas.iter_mut().find(|t| t.id == id) {
                if tarefa.concluida {
                    println!("{} Tarefa #{} já está concluída.", "AVISO:".yellow(), id);
                    return;
                }
                tarefa.concluir();
                println!(
                    "{} Tarefa #{} marcada como concluída!",
                    "OK".green().bold(),
                    id
                );
            } else {
                eprintln!("{} Tarefa #{} não encontrada.", "ERRO:".red().bold(), id);
                std::process::exit(1);
            }
        }

        Comando::Remover { id } => {
            let tamanho_antes = tarefas.len();
            tarefas.retain(|t| t.id != id);

            if tarefas.len() < tamanho_antes {
                println!("{} Tarefa #{} removida.", "OK".green().bold(), id);
            } else {
                eprintln!("{} Tarefa #{} não encontrada.", "ERRO:".red().bold(), id);
                std::process::exit(1);
            }
        }

        Comando::Limpar => {
            let quantidade = tarefas.iter().filter(|t| t.concluida).count();
            tarefas.retain(|t| !t.concluida);
            println!(
                "{} {} tarefa(s) concluída(s) removida(s).",
                "OK".green().bold(),
                quantidade
            );
        }
    }

    if let Err(e) = armazenamento::salvar(&tarefas) {
        eprintln!("{} Erro ao salvar tarefas: {}", "ERRO:".red().bold(), e);
        std::process::exit(1);
    }
}
```

O `main.rs` segue um padrão simples: carregar dados, processar o comando e salvar dados. Cada comando faz a operação correspondente no vetor de tarefas e exibe uma mensagem de confirmação.

## Como Executar

```bash
cargo build --release
```

Exemplos de uso:

```bash
# Adicionar tarefas
./target/release/tarefas adicionar "Estudar ownership em Rust" --prioridade alta
# OK Tarefa #1 adicionada: Estudar ownership em Rust

./target/release/tarefas adicionar "Configurar CI/CD" --prioridade media
# OK Tarefa #2 adicionada: Configurar CI/CD

./target/release/tarefas adicionar "Ler documentação do serde"
# OK Tarefa #3 adicionada: Ler documentação do serde

# Listar todas
./target/release/tarefas listar
# ID    Status     Prior.     Descrição
# ------------------------------------------------------------
# 1     Pendente   Alta       Estudar ownership em Rust
# 2     Pendente   Média      Configurar CI/CD
# 3     Pendente   Média      Ler documentação do serde
#
# 3 tarefa(s) no total.

# Concluir uma tarefa
./target/release/tarefas concluir 1
# OK Tarefa #1 marcada como concluída!

# Filtrar pendentes
./target/release/tarefas listar --filtro pendentes

# Remover tarefa
./target/release/tarefas remover 3
# OK Tarefa #3 removida.

# Limpar todas as concluídas
./target/release/tarefas limpar
# OK 1 tarefa(s) concluída(s) removida(s).
```

## Desafios para Expandir

1. **Tags e categorias**: Adicione suporte a tags (ex: `--tag trabalho --tag pessoal`) para organizar tarefas e permitir filtragem por tag.

2. **Data de vencimento**: Implemente um campo de prazo (`--vencimento 2026-03-15`) e destaque em vermelho as tarefas vencidas ao listar.

3. **Busca por texto**: Adicione um subcomando `buscar` que encontre tarefas pela descrição usando correspondência parcial ou regex.

4. **Exportação para Markdown**: Crie um subcomando `exportar` que gere um arquivo Markdown formatado com todas as tarefas, ideal para compartilhar relatórios.

5. **Múltiplas listas**: Permita que o usuário crie e alterne entre diferentes listas de tarefas (ex: `tarefas --lista trabalho adicionar "Reunião"`).

## Veja Também

- [Trabalhando com Vec](/stdlib/vec/) — o tipo vetor em Rust
- [HashMap](/stdlib/hashmap/) — mapas de chave-valor
- [Serializar e Desserializar JSON](/receitas/serializar-json/) — trabalhando com serde_json
- [Lendo Arquivos](/receitas/ler-arquivo/) — padrões de leitura de arquivos
- [Escrevendo em Arquivos](/receitas/escrever-arquivo/) — persistência de dados em disco
