Rastreamento de Foco com Python: O Sistema Anti-Procrastinação Que Rastrea Seu Tempo Sem Você Perceber
Você já sentiu que seu dia escorreu entre os dedos como água? Sentou às 8h da manhã, piscou, e eram 18h — sem conseguir apontar exatamente onde o tempo foi gasto? Se a resposta é sim, este post é sobre resolver isso de vez. Não com planilhas manuais que você abandona na terceira linha. Com Python rodando em segundo plano, rastreando seu foco enquanto você trabalha.
Eu passei meses acumulando “vou começar a controlar meu tempo amanhã” até perceber que o problema não era disciplina — era atrito. Qualquer sistema que exige ação manual para registrar o que você está fazendo vai falhar. Porque no meio do fluxo, a última coisa que você quer é parar para preencher um formulário. A solução? Automatizar o rastreamento.

Por Que o Rastreamento Manual Sempre Falha
Vamos ser honestos por um segundo. Ferramentas de time tracking como Toggl, Clockify e RescueTime são excelentes — no papel. Na prática, elas sofrem do mesmo mal: dependem da sua memória e disposição para iniciar e parar o timer. E quando você está no meio de uma sprint de coding, debugging de produção ou aquela reunião que se arrasta, lembrar de clicar em “start” é a última prioridade.
O resultado é sempre o mesmo: dados incompletos, estimativas enviesadas e a frustração de saber que algo está vazando tempo, mas não saber o quê.
A virada de chave aconteceu quando percebi que meu computador já sabe o que estou fazendo. Janela ativa, aplicativo em foco, horas de atividade — tudo isso é informação acessível via API do sistema operacional. O único missing link era alguém para coletar, agregar e apresentar isso de forma útil.
🔧 Perrengue real: Na primeira versão, eu usava
psutilpara pegar o processo em foco a cada 5 segundos. Funcionou por dois dias até o script consumir 40% de CPU num dia de reuniões com 47 abas do Chrome abertas. Lesson learned: polling agressivo mata produtividade antes de medir.
A Arquitetura do Rastreador Automático
O sistema que construí segue três princípios simples:
- Coleta passiva — o script roda como daemon, sem interação
- Agregação inteligente — janelas similares são agrupadas automaticamente
- Relatórios acionáveis — não apenas gráficos bonitos, mas insights que mudam comportamento
A stack é minimalista de propósito:
- Python 3.10+ — linguagem de automação por excelência
- SQLite — banco embutido, zero configuração, perfeito para dados pessoais
- pygetwindow / pyautogui — para detectar janela ativa (Linux/Windows)
- python-Xlib — alternativa nativa para Linux
Detecção da Janela Ativa
O coração do sistema é um loop que verifica qual janela está em foco a intervalos regulares. No Linux, podemos usar a biblioteca python-xlib para acessar o X11 diretamente:
from Xlib import X, display
import time
def get_active_window():
"""Retorna título e classe da janela ativa no Linux."""
d = display.Display()
root = d.screen().root
atom_net_active = d.get_atom('_NET_ACTIVE_WINDOW')
window_id = root.get_full_property(atom_net_active, X.AnyPropertyType)
if window_id and window_id.value:
win = d.create_resource_object('window', window_id.value[0])
wm_name = win.get_wm_name() or 'Sem título'
wm_class = win.get_wm_class()
app_name = wm_class[1] if wm_class else 'desconhecido'
return wm_name, app_name
return None, None
# Coleta a cada 10 segundos — equilíbrio entre precisão e consumo
while True:
title, app = get_active_window()
if title and app:
print(f"[{time.strftime('%H:%M:%S')}] {app}: {title}")
time.sleep(10)
Para Windows, a abordagem é via pygetwindow + ctypes:
import ctypes
import time
def get_active_window_win():
"""Detecção de janela ativa no Windows via ctypes."""
hwnd = ctypes.windll.user32.GetForegroundWindow()
length = ctypes.windll.user32.GetWindowTextLengthW(hwnd)
buff = ctypes.create_unicode_buffer(length + 1)
ctypes.windll.user32.GetWindowTextW(hwnd, buff, length + 1)
return buff.value or 'Sem título'
while True:
title = get_active_window_win()
print(f"[{time.strftime('%H:%M:%S')}] {title}")
time.sleep(10)

Armazenando em SQLite: Schema Eficiente
Um bom schema de banco de dados é a diferença entre “tenho dados” e “tenho insights”. Aqui está o schema que uso:
CREATE TABLE IF NOT EXISTS time_sessions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
app_name TEXT NOT NULL,
window_title TEXT,
category TEXT DEFAULT 'não_classificado',
start_time TIMESTAMP NOT NULL,
end_time TIMESTAMP,
duration_seconds INTEGER GENERATED ALWAYS AS (
CAST(strftime('%s', end_time) - strftime('%s', start_time) AS INTEGER)
) STORED
);
CREATE INDEX idx_app_time ON time_sessions(app_name, start_time);
CREATE INDEX idx_category ON time_sessions(category);
O duration_seconds como coluna computada evita recalcular toda vez. E os índices garantem que queries por período sejam instantâneas, mesmo com meses de dados acumulados.
Inserindo Registros
import sqlite3
from datetime import datetime, timedelta
class TimeTracker:
def __init__(self, db_path='time_tracking.db'):
self.db = sqlite3.connect(db_path)
self.current_session = None
self.threshold_seconds = 30 # troca de contexto < 30s = mesma sessão
def record_activity(self, app_name, window_title):
now = datetime.now()
if self.current_session is None:
self._start_session(app_name, window_title, now)
return
last_app = self.current_session['app_name']
elapsed = (now - self.current_session['start_time']).total_seconds()
# Mesma app? Estende sessão
if app_name == last_app:
self.current_session['end_time'] = now
return
# Troca rápida? Ignora (alt-tab, notificação)
if elapsed < self.threshold_seconds:
return
# Troca real de contexto: salva anterior e inicia nova
self._close_session(now)
self._start_session(app_name, window_title, now)
def _start_session(self, app, title, timestamp):
self.current_session = {
'app_name': app,
'window_title': title,
'start_time': timestamp,
'end_time': timestamp
}
def _close_session(self, end_time):
s = self.current_session
self.db.execute(
'''INSERT INTO time_sessions
(app_name, window_title, start_time, end_time)
VALUES (?, ?, ?, ?)''',
(s['app_name'], s['window_title'],
s['start_time'], end_time)
)
self.db.commit()
Categorização Automática por Contexto
Dados brutos de janelas são inúteis sem contexto. "chrome" pode ser YouTube ou Stack Overflow. A solução é um mapeamento baseado em título que classifica automaticamente:
import re
CATEGORIAS = {
'programação': [
(r'(?i)vscode|pycharm|intellij|vim|neovim|sublime'),
(r'(?i)github|gitlab|bitbucket'),
(r'(?i)stackoverflow|dev\.to|medium\.com/.*programming'),
],
'comunicação': [
(r'(?i)slack|discord|telegram|whatsapp|teams'),
(r'(?i)gmail|outlook|thunderbird'),
],
'reunião': [
(r'(?i)zoom|google meet|teams.*call|webex'),
],
'pesquisa': [
(r'(?i)google\.com|wikipedia|docs\.python\.org'),
],
'lazer': [
(r'(?i)youtube|netflix|spotify|twitch|reddit|twitter'),
],
}
def categorizar(app_name, window_title):
texto = f"{app_name} {window_title}".lower()
for categoria, patterns in CATEGORIAS.items():
for pattern in patterns:
if re.search(pattern, texto):
return categoria
return 'não_classificado'
Com essa abordagem, um dia de trabalho vira automaticamente um pie chart honesto: 4h programação, 1h30 comunicação, 45min reuniões, 2h YouTube (sim, precisa melhorar isso).
Gerando Relatórios que Importam
Dados sem visualização são só números numa tela. O script gera um relatório diário em HTML com três métricas que realmente importam:
1. Distribuição por Categoria
def relatorio_diario(self, date=None):
"""Gera relatório do dia com distribuição por categoria."""
if date is None:
date = datetime.now().strftime('%Y-%m-%d')
query = '''
SELECT
category,
COUNT(*) as sessoes,
SUM(duration_seconds) as total_seg,
ROUND(AVG(duration_seconds)/60, 1) as media_min
FROM time_sessions
WHERE date(start_time) = ?
GROUP BY category
ORDER BY total_seg DESC
'''
rows = self.db.execute(query, (date,)).fetchall()
print(f"\n{'='*50}")
print(f"RELATÓRIO: {date}")
print(f"{'='*50}")
total_trabalho = 0
for cat, sessoes, total_seg, media_min in rows:
horas = total_seg / 3600
print(f"{cat:20s} | {horas:5.1f}h | {sessoes:3d} sessões | média {media_min:.0f}min")
if cat not in ('lazer', 'não_classificado'):
total_trabalho += horas
print(f"{'-'*50}")
print(f"Tempo produtivo: {total_trabalho:.1f}h")
print(f"{'='*50}")
2. Heatmap de Foco por Hora
def heatmap_foco(self, days=7):
"""Mostra em quais horas do dia você é mais produtivo."""
query = '''
SELECT
strftime('%H', start_time) as hora,
SUM(duration_seconds) / 3600 as horas
FROM time_sessions
WHERE start_time > datetime('now', ?)
AND category IN ('programação', 'pesquisa')
GROUP BY hora
ORDER BY hora
'''
rows = self.db.execute(query, (f'-{days} days',)).fetchall()
print(f"\n🔥 HEATMAP DE FOCO (últimos {days} dias)")
for hora, hrs in rows:
bar = '█' * int(hrs * 2)
print(f"{hora}:00 | {bar} ({hrs:.1f}h)")
3. Ranking de Apps Mais Usados
def ranking_apps(self, days=7):
"""Top 10 apps por tempo de uso."""
query = '''
SELECT
app_name,
SUM(duration_seconds)/3600 as horas,
COUNT(*) as vezes
FROM time_sessions
WHERE start_time > datetime('now', ?)
GROUP BY app_name
ORDER BY horas DESC
LIMIT 10
'''
rows = self.db.execute(query, (f'-{days} days',)).fetchall()
print(f"\n📱 TOP APPS (últimos {days} dias)")
for i, (app, hrs, vezes) in enumerate(rows, 1):
print(f"{i:2d}. {app:25s} | {hrs:5.1f}h | {vezes:4d} sessões")
Deploy como Serviço: Rodando 24/7 Sem Pensar
Um script que precisa de terminal aberto é um script que vai morrer no primeiro reboot. Para funcionar de verdade, o rastreador precisa rodar como serviço do sistema. No Linux, isso significa um unit do systemd.
[Unit]
Description=Time Tracker - Rastreamento automático de produtividade
After=graphical-session.target
[Service]
ExecStart=/usr/bin/python3 /opt/tracker/tracker.py
WorkingDirectory=/opt/tracker
Restart=on-failure
RestartSec=30
StandardOutput=append:/var/log/tracker/output.log
StandardError=append:/var/log/tracker/error.log
# Segurança
NoNewPrivileges=true
ProtectSystem=strict
ReadWritePaths=/opt/tracker/data
PrivateTmp=true
[Install]
WantedBy=default.target
Depois é só ativar:
sudo systemctl enable --now time-tracker.service
sudo journalctl -u time-tracker -f # para acompanhar os logs
As diretivas de segurança não são enfeite: ProtectSystem=strict impede que o script escreva em qualquer lugar do sistema. PrivateTmp=true isola o /tmp. NoNewPrivileges=true garante que mesmo que alguém explore o script, não escala privilégios. Segurança não é opcional, mesmo para automações pessoais.
Privacidade: Seus Dados Não Saem da Sua Máquina
Uma diferença crucial entre esse tracker e ferramentas como RescueTime ou Clockify é que todos os dados ficam locais. Nada é enviado para servidores terceiros. O SQLite vive na sua máquina, os relatórios são gerados localmente e, se você usar Telegram para os alertas, só o texto do relatório vai — nenhum detalhe de janela ou título de app.
Se quiser backup, use rsync ou rclone para o seu próprio storage. Se quiser acesso remoto, configure o tracker para servir um dashboard HTTP com Flask num endpoint protegido por autenticação básica — mas nunca, jamais, envie dados brutos para APIs de terceiros sem necessidade.
🔒 Princípio do AutoMente: Ferramentas de produtividade não podem virar ferramentas de vigilância. Seus dados de foco são seus. Ponto final.
Integração com Automações Existentes
O tracker não precisa viver isolado. Uma vez que você tem dados estruturados de produtividade, pode conectar com outros sistemas:
1. Git Hooks — Correlação Commits vs. Foco
Adicione um post-commit hook que registra quanto tempo você gastou em cada repositório:
#!/bin/bash
# .git/hooks/post-commit
REPO=$(basename $(pwd))
python3 /opt/tracker/log_git_activity.py "$REPO" $(git log -1 --format=%H)
Com isso, você pode responder: "quantos commits eu fiz no meu pico de produtividade?" Spoiler: a resposta geralmente te surpreende.
2. Notificações Inteligentes
Use os dados do tracker para silenciar notificações durante períodos de foco. Um script simples verifica se você está numa sessão de "programação" há mais de 20 minutos e muda seu perfil do sistema para "não perturbe":
import subprocess
def check_focus_mode():
query = '''
SELECT category FROM time_sessions
WHERE start_time > datetime('now', '-30 minutes')
AND end_time IS NULL
LIMIT 1
'''
# Se está em "programação" há 30+ min, ativa DND
if result == 'programação':
subprocess.run(['gsettings', 'set',
'org.gnome.desktop.notifications',
'show-banners', 'false'])
3. Integração com Calendário
Compare o tempo rastreado com os eventos do calendário. Quantas reuniões foram "tempo de foco" desperdiçado? Quantas vezes você tinha uma reunião marcada mas estava em deep flow e perdeu? Esse cross-check é revelador — e só funciona porque os dados estão na sua mão, não num SaaS que não expõe a API.
Transformando Dados em Hábito
O tracker sozinho não muda nada. A mágica acontece quando você revisa os dados regularmente e usa os insights para ajustar comportamento. Aqui está minha rotina:
- Todo dia às 18h — relatório diário via cron (enviado por Telegram)
- Toda segunda — review semanal: comparo metas vs. realidade
- Todo dia 1º — relatório mensal com trending: estou melhorando ou piorando?
O cron que dispara o relatório diário:
# Relatório diário às 18h
0 18 * * * cd /opt/tracker && python3 daily_report.py --send-telegram
# Review semanal segunda 9h
0 9 * * 1 cd /opt/tracker && python3 weekly_review.py --send-email
A integração com Telegram usa a API direta do bot — sem intermediários. Um POST simples com o relatório formatado em Markdown:
import requests
def enviar_telegram(token, chat_id, texto):
url = f"https://api.telegram.org/bot{token}/sendMessage"
requests.post(url, json={
"chat_id": chat_id,
"text": texto,
"parse_mode": "Markdown"
})
Lições Aprendidas (e Erros que Cometi)
Para ser transparente, nem tudo foram flores:
- Erro #1 — Polling muito agressivo: já mencionado. O script consumia mais CPU que o VS Code. Solução: aumentei para 10 segundos e adicionei debounce.
- Erro #2 — Não categorizar janelas: os primeiros relatórios mostravam "chrome" como 8h do dia. Inútil. A categorização por título resolveu.
- Erro #3 — SQLite sem WAL mode: com múltiplos processos lendo/escrevendo, o banco corrompeu duas vezes. Ativar
PRAGMA journal_mode=WALeliminou o problema. - Erro #4 — Ignorar contexto de reuniões: o tracker não distinguia "estou numa reunião ouvindo" de "estou numa reunião apresentando". Adicionei detecção de microfone ativo para refinar.
O Que Fazer com Esses Dados
Ter dados é meio caminho. A outra metade é agir com base neles. Aqui estão ações concretas que tomei nos meus primeiros 30 dias:
- Descobri que 2h/dia ia para notificações — Silenciei tudo exceto urgentes. Ganhei 40h/mês.
- Identifiquei meu pico de foco: 10h-12h — Movi tarefas complexas para esse slot. Produtividade subiu 30%.
- Vi que reuniões consumiam 25% da semana — Negociei formatos assíncronos para 3 das 8 reuniões semanais.
- Percebi que YouTube vinha às 15h — Cravei um bloco de 30min de "descanso intencional" nesse horário. Parou de vazar para o resto do dia.
Conclusão: Meça para Melhorar
A produtividade não é sobre trabalhar mais horas. É sobre saber para onde suas horas vão e ter dados reais para tomar decisões. Sem métricas, você está dirigindo de olhos vendados — pode até chegar somewhere, mas vai bater em muito poste no caminho.
O script completo está no meu GitHub e é open source. Fork, modifique, adapte para sua stack. E se você tem uma automação específica que quer ver aqui no blog — qual tracker de produtividade você quer que eu construa no próximo post? Um com integração ao Notion? Um que usa IA para classificar automaticamente? Manda nos comentários.
Porque no fim, a melhor automação é aquela que você usa todos os dias. E para usar, precisa medir. E para medir, precisa automatizar. O ciclo é virtuoso — comece agora.
