dotfiles

Repositório dotfiles
git clone git://git.sivaldodavi.com/dotfiles.git
Log | Files | Refs | README

ptman (6627B)


      1 #!/usr/bin/env python3
      2 import os
      3 import sys
      4 import json
      5 import subprocess
      6 import urllib.request
      7 import urllib.error
      8 import time
      9 import re
     10 from concurrent.futures import ThreadPoolExecutor
     11 
     12 API_URL = "https://translate.disroot.org/translate"
     13 TARGET_DIR = os.path.expanduser("~/.local/share/man/pt")
     14 CHUNK_LIMIT = 3000
     15 TRANSLATE_WORKERS = 3  # nº de chunks traduzidos em paralelo
     16 REQUEST_DELAY = 1.0    # respiro entre disparos, por worker
     17 
     18 
     19 def clean_backspaces(text):
     20     """Remove sequências de overstrike (char + backspace) usadas para negrito/sublinhado."""
     21     while '\x08' in text:
     22         text = re.sub(r'.\x08', '', text)
     23     return text
     24 
     25 
     26 def split_long_line(line, limit):
     27     """Quebra uma única linha maior que o limite em pedaços menores."""
     28     return [line[i:i + limit] for i in range(0, len(line), limit)] or [line]
     29 
     30 
     31 def translate_chunk(text):
     32     """Envia um bloco de texto para a API de tradução, com retentativas."""
     33     payload = {"q": text, "source": "auto", "target": "pt", "api_key": ""}
     34     req = urllib.request.Request(
     35         API_URL, data=json.dumps(payload).encode('utf-8'),
     36         headers={'Content-Type': 'application/json'}, method='POST'
     37     )
     38     for attempt in range(5):
     39         try:
     40             with urllib.request.urlopen(req, timeout=30) as response:
     41                 res_data = json.loads(response.read().decode('utf-8'))
     42                 return res_data.get("translatedText", "")
     43         except urllib.error.HTTPError as e:
     44             if e.code == 429:
     45                 time.sleep(3 * (attempt + 1))
     46                 continue
     47             return None  # erro definitivo do servidor
     48         except Exception:
     49             time.sleep(2)
     50             continue
     51     return None
     52 
     53 
     54 def build_chunks(text):
     55     """Divide o texto em blocos respeitando CHUNK_LIMIT."""
     56     lines = text.splitlines()
     57     chunks, current_chunk, current_length = [], [], 0
     58 
     59     for line in lines:
     60         if len(line) > CHUNK_LIMIT:
     61             if current_chunk:
     62                 chunks.append("\n".join(current_chunk))
     63                 current_chunk, current_length = [], 0
     64             chunks.extend(split_long_line(line, CHUNK_LIMIT))
     65             continue
     66 
     67         if current_length + len(line) + 1 > CHUNK_LIMIT:
     68             chunks.append("\n".join(current_chunk))
     69             current_chunk, current_length = [line], len(line)
     70         else:
     71             current_chunk.append(line)
     72             current_length += len(line) + 1
     73 
     74     if current_chunk:
     75         chunks.append("\n".join(current_chunk))
     76 
     77     return chunks
     78 
     79 
     80 def translate_text(text, status_prefix=""):
     81     """Traduz um texto inteiro, fatiando em chunks e mostrando progresso em paralelo."""
     82     chunks = build_chunks(text)
     83     total_chunks = len([c for c in chunks if c.strip()])
     84     if total_chunks == 0:
     85         return text
     86 
     87     results = [None] * len(chunks)
     88     done_count = [0]
     89 
     90     def worker(idx_chunk):
     91         idx, chunk = idx_chunk
     92         if not chunk.strip():
     93             results[idx] = chunk
     94             return
     95         translated = translate_chunk(chunk)
     96         results[idx] = translated if translated is not None else chunk
     97         done_count[0] += 1
     98         if status_prefix:
     99             pct = int((done_count[0] / total_chunks) * 100)
    100             print(
    101                 f"\r{status_prefix} -> Traduzindo... {done_count[0]}/{total_chunks} partes ({pct}%)",
    102                 end="", flush=True
    103             )
    104         time.sleep(REQUEST_DELAY)
    105 
    106     with ThreadPoolExecutor(max_workers=TRANSLATE_WORKERS) as pool:
    107         list(pool.map(worker, enumerate(chunks)))
    108 
    109     if status_prefix:
    110         print(f"\r{status_prefix} -> Concluído!{' ' * 25}", flush=True)
    111 
    112     return "\n".join(results)
    113 
    114 
    115 def resolve_man_path(args):
    116     """Resolve o caminho físico do manual via `man -w`."""
    117     try:
    118         output = subprocess.check_output(
    119             ["man", "-w"] + args, stderr=subprocess.DEVNULL
    120         ).decode('utf-8').strip()
    121     except subprocess.CalledProcessError:
    122         return None
    123 
    124     candidates = [p for p in output.splitlines() if p.strip()]
    125     return candidates[0] if candidates else None
    126 
    127 
    128 def process_manual(args):
    129     """Busca, traduz (se necessário) e exibe o manual especificado."""
    130     src_path = resolve_man_path(args)
    131     if not src_path:
    132         print(f"Erro: Manual não encontrado para: {' '.join(args)}")
    133         return False
    134 
    135     parts = src_path.split('/')
    136     if len(parts) < 2:
    137         print("Erro: Caminho do manual inválido.")
    138         return False
    139 
    140     section_dir, filename = parts[-2], parts[-1]
    141     clean_filename = filename.replace('.gz', '')
    142     dest_path = os.path.join(TARGET_DIR, section_dir, f"{clean_filename}.txt")
    143 
    144     # Retorna a versão em cache, se existir
    145     if os.path.exists(dest_path):
    146         subprocess.run(["vim", dest_path])
    147         return True
    148 
    149     print(f"-> Origem localizada: {src_path}")
    150     package_origin = "Desconhecido"
    151     
    152     try:
    153         apk_out = subprocess.check_output(
    154             ["apk", "info", "-W", src_path], stderr=subprocess.DEVNULL
    155         ).decode('utf-8').strip()
    156         if "is owned by" in apk_out:
    157             package_origin = apk_out.split("is owned by")[-1].strip()
    158     except Exception:
    159         pass
    160         
    161     print(f"-> Pacote: [{package_origin}]")
    162     print("-> Traduzindo sob demanda...")
    163 
    164     try:
    165         raw_text = subprocess.check_output(
    166             ["mandoc", "-Tutf8", src_path], stderr=subprocess.DEVNULL
    167         ).decode('utf-8', errors='ignore')
    168         raw_text = clean_backspaces(raw_text)
    169 
    170         if not raw_text.strip():
    171             return False
    172 
    173         translated = translate_text(raw_text, f"-> {clean_filename}")
    174 
    175         os.makedirs(os.path.dirname(dest_path), exist_ok=True)
    176         with open(dest_path, "w", encoding="utf-8") as f:
    177             f.write(translated)
    178 
    179         subprocess.run(["vim", dest_path])
    180         return True
    181 
    182     except Exception as e:
    183         print(f"\nFalha na tradução: {e}")
    184         print("Abrindo fallback em inglês...\n")
    185         time.sleep(1.5)
    186         subprocess.run(["man"] + args)
    187         return False
    188 
    189 
    190 def main():
    191     args = sys.argv[1:]
    192 
    193     if not args:
    194         print("Uso:")
    195         print("  ptman <cmd>        (Ex: ptman git)")
    196         print("  ptman <sec> <cmd>  (Ex: ptman 5 crontab)")
    197         print("  ptman clean        (Apaga o cache atual)")
    198         sys.exit(1)
    199 
    200     cmd = args[0].lower()
    201 
    202     if cmd == "clean":
    203         if os.path.exists(TARGET_DIR):
    204             subprocess.run(["rm", "-rf", TARGET_DIR])
    205             print("Cache limpo.")
    206         else:
    207             print("Cache já estava vazio.")
    208         sys.exit(0)
    209 
    210     process_manual(args)
    211 
    212 
    213 if __name__ == "__main__":
    214     main()