Monitor de backup do Time Machine na barra de menus do Mac

Quem usa o Time Machine geralmente só percebe que algo deu errado quando precisa restaurar um arquivo. O backup roda em segundo plano, quase não avisa e, quando está em andamento, a interface mostra apenas um ícone girando. No meu caso, os backups vão para um servidor Ubuntu na rede, e eu queria mais informação: a porcentagem real, quantos arquivos faltam e quanto tempo ainda resta. Então montei um monitor próprio na barra de menus.

Por que monitorar o backup

Backups pela rede são mais lentos e mais sujeitos a interrupções do que um disco externo ligado direto ao Mac. Uma oscilação no Wi-Fi ou um servidor ocupado podem atrasar tudo sem que você perceba. Ter essas informações à vista, sem abrir as preferências do sistema, faz diferença no dia a dia.

O que o tmutil status entrega

O macOS já traz a ferramenta de linha de comando tmutil, e o subcomando status mostra o estado atual do backup em formato de texto estruturado. Ele informa se há um backup em execução, em qual fase ele está e alguns valores numéricos, como o progresso e o tempo restante estimado. É exatamente o tipo de saída que um script consegue processar.

External hard drive connected to a laptop, showcasing portable storage solution.
Foto: Budget Bizar / Pexels

O problema é que essa saída não é pensada para leitura humana. Os valores vêm misturados com nomes de chaves, e a porcentagem normalmente aparece como fração entre 0 e 1. Para resolver isso, usei o awk para filtrar as linhas certas e fazer as contas: transformar a fração em porcentagem e converter os segundos restantes em um formato legível, como “2 h 14 min”.

Montando o plugin no SwiftBar

O SwiftBar é um aplicativo gratuito e de código aberto que transforma scripts em itens da barra de menus. Você pode baixá-lo em swiftbar.app. Cada script é um arquivo executável cujo nome define o intervalo de atualização, e tudo o que ele imprime vira o conteúdo do item. O texto antes da linha separadora aparece sempre visível na barra; o que vem depois forma o menu suspenso.

No meu script, a barra mostra um ícone e a porcentagem atual enquanto o backup roda. Quando não há nada em andamento, o ícone muda para uma marca de verificação com a data do último backup concluído. No menu suspenso aparecem os detalhes: fase atual, arquivos processados, tempo restante e um atalho para abrir o log.

Log contínuo para ter histórico

Além do monitor visual, uma lógica em background registra tudo em um arquivo de log contínuo. Cada linha guarda data, hora, fase e progresso, então dá para reconstruir o histórico de qualquer backup depois.

O ponto que mais me ajudou foi medir a duração de cada fase. Quando um backup demora mais que o normal, o log mostra exatamente onde o tempo foi gasto. Assim deixei de chutar a causa da lentidão.

  • Início e fim de cada fase, com data e hora
  • Porcentagem e quantidade de arquivos em cada registro
  • Duração exata de cada etapa, calculada pela diferença de tempo

Script completo

Salve o conteúdo abaixo como um arquivo na pasta de plugins do SwiftBar, com a extensão que define o intervalo de atualização (por exemplo, tm.10s.sh para atualizar a cada 10 segundos). Depois, torne o arquivo executável com chmod +x.

#!/bin/bash

# Forçar a localidade para evitar problemas com ponto flutuante
export LC_ALL=C

# Configurações do Histórico (Caminho Automático)
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
LOG_FILE="$SCRIPT_DIR/tm_historico.log"
STATE_FILE="/tmp/tm_last_phase.txt"
START_FILE="/tmp/tm_phase_start.txt"

# Cria o arquivo de log se não existir
touch "$LOG_FILE"

STATUS=$(tmutil status)
RUNNING=$(echo "$STATUS" | awk '/Running =/ {print $3}' | tr -d ';')

# Captura a fase atual ou define como Ocioso
if [ "$RUNNING" == "0" ] || [ -z "$RUNNING" ]; then
    PHASE="Ocioso"
else
    PHASE=$(echo "$STATUS" | awk -F'"' '/BackupPhase/ {print $2}')
    if [ -z "$PHASE" ]; then
        PHASE=$(echo "$STATUS" | awk '/BackupPhase =/ {print $3}' | tr -d '";')
    fi
fi

# Tradução das Fases para o Log e Interface
case "$PHASE" in
    "Copying") PHASE_PT="Copiando"; ICON="💾" ;;
    "HealthCheckFsck") PHASE_PT="Verificando FS"; ICON="🩺" ;;
    "ThinningPreBackup") PHASE_PT="Limpando"; ICON="🧹" ;;
    "ThinningPostBackup") PHASE_PT="Limpeza final"; ICON="🧹" ;;
    "FindingBackupFiles") PHASE_PT="Procurando"; ICON="🔍" ;;
    "FindingChanges") PHASE_PT="Procurando alterações"; ICON="🔍" ;;
    "Preparing") PHASE_PT="Preparando"; ICON="⚙️" ;;
    "PreparingSourceVolumes") PHASE_PT="Preparando discos"; ICON="💽" ;;
    "Mounting") PHASE_PT="Montando"; ICON="💿" ;;
    "MountingDiskImage") PHASE_PT="Montando imagem"; ICON="💿" ;;
    "Ocioso") PHASE_PT="Ocioso"; ICON="⏱️" ;;
    *) PHASE_PT="$PHASE"; ICON="🔄" ;;
esac

# ---------------------------------------------------------
# LÓGICA DO HISTÓRICO E ROTAÇÃO DE LOG
# ---------------------------------------------------------
CURRENT_TIME=$(date +%s)
CURRENT_DATE=$(date +"%d/%m/%Y %H:%M:%S")

LAST_PHASE_PT=$(cat "$STATE_FILE" 2>/dev/null)
PHASE_START=$(cat "$START_FILE" 2>/dev/null)

# Se a fase mudou, registra o tempo da fase anterior e inicia a nova
if [ "$PHASE_PT" != "$LAST_PHASE_PT" ]; then
    
    # Se existia uma fase anterior registrada, calcula a duração
    if [ -n "$LAST_PHASE_PT" ] && [ -n "$PHASE_START" ]; then
        DURATION=$((CURRENT_TIME - PHASE_START))
        H=$((DURATION / 3600))
        M=$(((DURATION % 3600) / 60))
        S=$((DURATION % 60))
        
        FORMATTED_DURATION=""
        [ $H -gt 0 ] && FORMATTED_DURATION="${H}h "
        [ $M -gt 0 ] && FORMATTED_DURATION="${FORMATTED_DURATION}${M}m "
        FORMATTED_DURATION="${FORMATTED_DURATION}${S}s"

        echo "[$CURRENT_DATE] ⏹ Concluído: $LAST_PHASE_PT (Duração: $FORMATTED_DURATION)" >> "$LOG_FILE"
    fi

    # Registra o início da nova fase
    if [ "$PHASE_PT" != "Ocioso" ]; then
        echo "[$CURRENT_DATE] ▶ Iniciado: $PHASE_PT" >> "$LOG_FILE"
    else
        echo "[$CURRENT_DATE] ✅ Backup Finalizado / Inativo" >> "$LOG_FILE"
        echo "---" >> "$LOG_FILE"
    fi

    # Rotação automática do log: limite de 1000 linhas
    MAX_LINES=1000
    LINES=$(wc -l < "$LOG_FILE" | awk '{print $1}')
    if [ -n "$LINES" ] && [ "$LINES" -gt "$MAX_LINES" ]; then
        tail -n 500 "$LOG_FILE" > "${LOG_FILE}.tmp"
        mv "${LOG_FILE}.tmp" "$LOG_FILE"
    fi

    # Atualiza os arquivos de estado
    echo "$PHASE_PT" > "$STATE_FILE"
    echo "$CURRENT_TIME" > "$START_FILE"
fi
# ---------------------------------------------------------

# Pegando as variáveis do sistema para a interface
PERCENT=$(echo "$STATUS" | awk -F'=' '/^[[:space:]]*Percent =/ {print $2}' | tr -d ' ";')
BYTES=$(echo "$STATUS" | awk -F'=' '/^[[:space:]]*bytes =/ {print $2}' | tr -d ' ";')
TOTAL_BYTES=$(echo "$STATUS" | awk -F'=' '/^[[:space:]]*totalBytes =/ {print $2}' | tr -d ' ";')
FILES=$(echo "$STATUS" | awk -F'=' '/^[[:space:]]*files =/ {print $2}' | tr -d ' ";')
TIME_REMAINING=$(echo "$STATUS" | awk -F'=' '/^[[:space:]]*TimeRemaining =/ {print $2}' | tr -d ' ";')

FILES_TEXT=""
if [ -n "$FILES" ]; then
    FILES_TEXT=" ($FILES arqs)"
fi

TIME_TEXT=""
if [ -n "$TIME_REMAINING" ] && [ "$TIME_REMAINING" != "0" ]; then
    TIME_TEXT=$(awk -v t="$TIME_REMAINING" 'BEGIN {
        t = int(t)
        h = int(t / 3600)
        m = int((t % 3600) / 60)
        if (h > 0) printf " ~%dh%02dm", h, m
        else if (m > 0) printf " ~%dm", m
        else printf " <1m"
    }')
fi

# Exibição principal na barra de menus
if [ "$PHASE_PT" == "Ocioso" ]; then
    echo "$ICON TM: Ocioso"
elif [ -z "$PERCENT" ] || [ "$PERCENT" == "-1" ] || [ "$PERCENT" == "-1.0" ]; then
    echo "$ICON TM: $PHASE_PT"
else
    PCT_TEXT=$(awk -v p="$PERCENT" 'BEGIN { printf "%.1f%%", p * 100 }')
    echo "$ICON TM: $PCT_TEXT$FILES_TEXT$TIME_TEXT"
fi

# Conteúdo do Dropdown (ao clicar)
echo "---"
if [ "$PHASE_PT" == "Ocioso" ]; then
    echo "Nenhum backup em andamento."
else
    echo "Fase Atual: $PHASE_PT"
    if [ -n "$BYTES" ] && [ -n "$TOTAL_BYTES" ] && [ "$TOTAL_BYTES" != "0" ]; then
        COPIED_GB=$(awk -v b="$BYTES" 'BEGIN { printf "%.2f", b / 1000000000 }')
        TOTAL_GB=$(awk -v t="$TOTAL_BYTES" 'BEGIN { printf "%.2f", t / 1000000000 }')
        echo "Tamanho: $COPIED_GB GB de $TOTAL_GB GB"
    fi
fi

# Exibe o histórico diretamente no menu
echo "---"
echo "Histórico Recente:"
if [ -s "$LOG_FILE" ]; then
    tail -n 15 "$LOG_FILE" | while read -r line; do
        echo "$line | font=Menlo size=11 trim=false"
    done
else
    echo "Nenhum histórico registrado ainda."
fi

echo "---"
if [ "$PHASE_PT" == "Ocioso" ]; then
    echo "Iniciar Backup | bash='tmutil' param1='startbackup' terminal=false"
else
    echo "Parar Backup | bash='tmutil' param1='stopbackup' terminal=false"
fi
echo "Abrir Log Completo | bash='open' param1=\"$LOG_FILE\" terminal=false"

Resultado na prática

Hoje consigo olhar para a barra de menus e saber, em um segundo, se o backup está andando e quanto falta. Não preciso mais abrir o Terminal nem ficar adivinhando se algo travou.

Dual external hard drives on a docking station with an orange backdrop.
Foto: Alpha En / Pexels

Dois aprendizados ficaram. O primeiro é que o formato de saída do tmutil pode variar entre versões do macOS, então vale testar o script no terminal antes de colocá-lo no SwiftBar. O segundo é que o log cresce com o tempo, e uma rotação simples evita que ele ocupe espaço à toa.

Se você quiser replicar

  • Instale o SwiftBar e defina uma pasta para os plugins.
  • Escreva o script primeiro no terminal, imprimindo apenas os dados que precisa.
  • Escolha um intervalo de atualização razoável para não consumir recursos desnecessários.
  • Crie o processo de log separado, para que o registro continue mesmo com o menu fechado.

Se você usa o Time Machine com um servidor próprio, esse pequeno projeto dá uma tranquilidade que o sistema não oferece de fábrica. Em poucas horas dá para ter algo funcional, e depois é só ajustar ao seu gosto.

Posts Similares

Deixe um comentário

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *