Relógio Digital em Zig — Tutorial Passo a Passo

Relógio Digital em Zig — Tutorial Passo a Passo

Neste tutorial, vamos construir um relógio digital animado no terminal usando arte ASCII e códigos de escape ANSI. Este projeto explora temporização, manipulação de terminal e renderização de texto.

O Que Vamos Construir

Nosso relógio vai:

  • Exibir a hora atual em dígitos grandes (arte ASCII)
  • Atualizar a cada segundo sem flicker
  • Usar cores ANSI para destaque visual
  • Mostrar data por extenso em português
  • Funcionar em qualquer terminal compatível com ANSI

Por Que Este Projeto?

Um relógio digital é um projeto visual que ensina conceitos importantes: como obter a hora do sistema, como manipular o terminal (mover cursor, limpar tela), como usar sleep para temporização e como organizar renderização em camadas.

Passo 1: Dígitos em Arte ASCII

const std = @import("std");
const io = std.io;
const time = std.time;
const mem = std.mem;

/// Representação dos dígitos 0-9 e do separador ":" em arte ASCII.
/// Cada dígito tem 5 linhas de 4 caracteres.
/// Usamos um array de strings constantes porque esses dados são
/// conhecidos em tempo de compilação — sem alocação necessária.
const digitos_ascii = [11][5][]const u8{
    // 0
    .{ " ██ ", "█  █", "█  █", "█  █", " ██ " },
    // 1
    .{ "  █ ", " ██ ", "  █ ", "  █ ", " ███" },
    // 2
    .{ " ██ ", "   █", " ██ ", "█   ", " ███" },
    // 3
    .{ "███ ", "   █", " ██ ", "   █", "███ " },
    // 4
    .{ "█  █", "█  █", " ███", "   █", "   █" },
    // 5
    .{ " ███", "█   ", " ██ ", "   █", "██  " },
    // 6
    .{ " ██ ", "█   ", "███ ", "█  █", " ██ " },
    // 7
    .{ "████", "   █", "  █ ", " █  ", " █  " },
    // 8
    .{ " ██ ", "█  █", " ██ ", "█  █", " ██ " },
    // 9
    .{ " ██ ", "█  █", " ███", "   █", " ██ " },
    // : (separador, índice 10)
    .{ "    ", " ██ ", "    ", " ██ ", "    " },
};

/// Cores ANSI para terminal.
const Cor = struct {
    const reset = "\x1b[0m";
    const ciano = "\x1b[36m";
    const verde = "\x1b[32m";
    const amarelo = "\x1b[33m";
    const branco_bold = "\x1b[1;37m";
    const dim = "\x1b[2m";
};

/// Sequências de controle do terminal.
const Terminal = struct {
    const limpar_tela = "\x1b[2J";
    const cursor_inicio = "\x1b[H";
    const esconder_cursor = "\x1b[?25l";
    const mostrar_cursor = "\x1b[?25h";

    fn moverCursor(writer: anytype, linha: u32, coluna: u32) !void {
        try writer.print("\x1b[{d};{d}H", .{ linha, coluna });
    }
};

Passo 2: Renderização dos Dígitos

/// Renderiza um conjunto de dígitos na tela.
/// Cada dígito é composto de 5 linhas, e renderizamos
/// todos os dígitos lado a lado.
fn renderizarHora(
    writer: anytype,
    hora: u8,
    minuto: u8,
    segundo: u8,
    linha_base: u32,
    coluna_base: u32,
) !void {
    // Converte hora em dígitos individuais
    const digs = [8]usize{
        hora / 10,
        hora % 10,
        10, // separador
        minuto / 10,
        minuto % 10,
        10, // separador
        segundo / 10,
        segundo % 10,
    };

    // Renderiza linha por linha
    for (0..5) |linha| {
        try Terminal.moverCursor(writer, linha_base + @as(u32, @intCast(linha)), coluna_base);

        for (digs, 0..) |d, i| {
            // Aplica cor diferente para o separador (pisca visualmente)
            if (d == 10) {
                if (segundo % 2 == 0) {
                    try writer.print("{s}{s} ", .{ Cor.dim, digitos_ascii[d][linha] });
                } else {
                    try writer.print("{s}{s} ", .{ Cor.ciano, digitos_ascii[d][linha] });
                }
            } else {
                // Cores diferentes para hora, minuto e segundo
                const cor = if (i < 2) Cor.verde else if (i < 5) Cor.ciano else Cor.amarelo;
                try writer.print("{s}{s} ", .{ cor, digitos_ascii[d][linha] });
            }
        }
        try writer.print("{s}", .{Cor.reset});
    }
}

Passo 3: Data por Extenso

/// Nomes dos dias da semana em português.
const dias_semana = [7][]const u8{
    "Domingo", "Segunda-feira", "Terça-feira",
    "Quarta-feira", "Quinta-feira", "Sexta-feira", "Sábado",
};

/// Nomes dos meses em português.
const meses = [12][]const u8{
    "janeiro", "fevereiro", "março", "abril",
    "maio",    "junho",     "julho", "agosto",
    "setembro","outubro",  "novembro","dezembro",
};

/// Converte timestamp epoch para componentes de data/hora.
/// Usamos a função da stdlib que já lida com anos bissextos.
fn epochParaComponentes(epoch_secs: i64) struct {
    ano: u16,
    mes: u8,
    dia: u8,
    hora: u8,
    minuto: u8,
    segundo: u8,
    dia_semana: u8,
} {
    const epoch_day = @divFloor(epoch_secs, 86400);
    const day_seconds = @mod(epoch_secs, 86400);

    const hora: u8 = @intCast(@divFloor(day_seconds, 3600));
    const minuto: u8 = @intCast(@divFloor(@mod(day_seconds, 3600), 60));
    const segundo: u8 = @intCast(@mod(day_seconds, 60));

    // Cálculo do dia da semana (0 = quinta para epoch, ajustamos)
    const dow: u8 = @intCast(@mod(epoch_day + 4, 7)); // 0=domingo

    // Cálculo da data usando o algoritmo civil
    const z = epoch_day + 719468;
    const era = @divFloor(if (z >= 0) z else z - 146096, 146097);
    const doe = z - era * 146097;
    const yoe = @divFloor(doe - @divFloor(doe, 1460) + @divFloor(doe, 36524) - @divFloor(doe, 146096), 365);
    const y = yoe + era * 400;
    const doy = doe - (365 * yoe + @divFloor(yoe, 4) - @divFloor(yoe, 100));
    const mp = @divFloor(5 * doy + 2, 153);
    const d = doy - @divFloor(153 * mp + 2, 5) + 1;
    const m = if (mp < 10) mp + 3 else mp - 9;
    const ano = if (m <= 2) y + 1 else y;

    return .{
        .ano = @intCast(ano),
        .mes = @intCast(m),
        .dia = @intCast(d),
        .hora = hora,
        .minuto = minuto,
        .segundo = segundo,
        .dia_semana = dow,
    };
}

Passo 4: Loop Principal

pub fn main() !void {
    const stdout = io.getStdOut().writer();

    // Esconde cursor e limpa tela
    try stdout.print("{s}{s}", .{ Terminal.esconder_cursor, Terminal.limpar_tela });

    // Garante que o cursor será restaurado ao sair
    defer stdout.print("{s}{s}", .{ Terminal.mostrar_cursor, Terminal.limpar_tela }) catch {};

    // Cabeçalho fixo
    try Terminal.moverCursor(stdout, 2, 10);
    try stdout.print("{s}╔══════════════════════════════════════════╗{s}", .{ Cor.dim, Cor.reset });
    try Terminal.moverCursor(stdout, 3, 10);
    try stdout.print("{s}║        Relógio Digital Zig              ║{s}", .{ Cor.dim, Cor.reset });
    try Terminal.moverCursor(stdout, 4, 10);
    try stdout.print("{s}╚══════════════════════════════════════════╝{s}", .{ Cor.dim, Cor.reset });

    while (true) {
        // Obtém hora atual
        const timestamp = time.timestamp();
        const comp = epochParaComponentes(timestamp);

        // Renderiza hora grande
        try renderizarHora(stdout, comp.hora, comp.minuto, comp.segundo, 6, 12);

        // Renderiza data
        try Terminal.moverCursor(stdout, 12, 12);
        try stdout.print("{s}{s}, {d} de {s} de {d}   {s}", .{
            Cor.branco_bold,
            dias_semana[comp.dia_semana],
            comp.dia,
            meses[comp.mes - 1],
            comp.ano,
            Cor.reset,
        });

        // Rodapé
        try Terminal.moverCursor(stdout, 14, 12);
        try stdout.print("{s}Pressione Ctrl+C para sair{s}", .{ Cor.dim, Cor.reset });

        // Espera 1 segundo
        // Usamos nanosleep que é mais preciso que sleep
        time.sleep(time.ns_per_s);
    }
}

Passo 5: Testes

test "epoch para componentes - Unix epoch" {
    const comp = epochParaComponentes(0);
    try std.testing.expectEqual(@as(u8, 0), comp.hora);
    try std.testing.expectEqual(@as(u8, 0), comp.minuto);
    try std.testing.expectEqual(@as(u8, 0), comp.segundo);
}

test "dígitos ASCII têm 5 linhas" {
    for (digitos_ascii) |digito| {
        try std.testing.expectEqual(@as(usize, 5), digito.len);
    }
}

Compilando e Executando

zig build run

Conceitos Aprendidos

  • Obtenção da hora do sistema com std.time
  • Códigos de escape ANSI para controle de terminal
  • Arrays constantes em tempo de compilação
  • Sleep e temporização precisa
  • Renderização sem flicker com reposicionamento de cursor

Próximos Passos

Continue aprendendo Zig

Explore mais tutoriais e artigos em português para dominar a linguagem Zig.