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
- Aprenda sobre controle de terminal para interfaces mais elaboradas
- Explore std.time para temporização avançada
- Construa o próximo projeto: Conversor de Moedas