"""openTARS 2.9.2: mouse preciso. Lógica pura (sem X, sem pyautogui) dos cliques e arrastos, testável sozinha: - ancora(): "top-left", "canto superior esquerdo", "oben links"... -> ponto da tela - caminho(): os pontos de um arrasto "de gente": sai devagar do lugar (apps como o Chrome/Brave só entendem que é arrasto depois de uns pixels com o botão apertado), acelera e freia no fim - recorte_em_volta(): a região em volta de um ponto - mudanca(): quanto a tela mudou entre duas capturas (o clique pegou?) """ import re import unicodedata # ------------------------------------------------------------ # Âncoras: lugares da tela por nome, em 5 idiomas # ------------------------------------------------------------ MARGEM_ANCORA = 48 # px da borda: soltar colado na borda liga o "encaixe" de janela do GNOME/KDE _VERTICAL = { "topo": ("top", "upper", "topo", "cima", "alto", "superior", "arriba", "haut", "oben", "obere", "oberen"), "base": ("bottom", "lower", "baixo", "inferior", "embaixo", "abajo", "bas", "unten", "untere", "unteren"), } _HORIZONTAL = { "esquerda": ("left", "esquerda", "esquerdo", "izquierda", "izquierdo", "gauche", "links", "linke", "linken"), "direita": ("right", "direita", "direito", "derecha", "derecho", "droite", "rechts", "rechte", "rechten"), } _CENTRO = ("center", "centre", "middle", "centro", "meio", "medio", "milieu", "mitte") def _normalizar(texto): texto = unicodedata.normalize("NFKD", str(texto or "").lower()) texto = "".join(c for c in texto if not unicodedata.combining(c)) return re.findall(r"[a-z]+", texto) def ancora(nome, largura, altura, margem=MARGEM_ANCORA): """(x, y) do lugar da tela chamado 'nome', ou None se não for um lugar. 'top-left' -> perto do canto superior esquerdo; 'left' -> meio da borda esquerda; 'center' -> centro.""" palavras = set(_normalizar(nome)) if not palavras: return None vertical = next((k for k, v in _VERTICAL.items() if palavras & set(v)), None) horizontal = next((k for k, v in _HORIZONTAL.items() if palavras & set(v)), None) centro = bool(palavras & set(_CENTRO)) if not (vertical or horizontal or centro): return None # "metade esquerda", "left half": o centro daquela metade metade = bool(palavras & {"half", "metade", "mitad", "moitie", "halfte", "haelfte"}) margem = max(0, min(margem, largura // 4, altura // 4)) if horizontal == "esquerda": x = largura // 4 if metade else margem elif horizontal == "direita": x = largura - largura // 4 if metade else largura - 1 - margem else: x = largura // 2 if vertical == "topo": y = altura // 4 if metade else margem elif vertical == "base": y = altura - altura // 4 if metade else altura - 1 - margem else: y = altura // 2 return x, y # ------------------------------------------------------------ # Caminho do arrasto # ------------------------------------------------------------ EMPURRAO_PX = 14 # o primeiro pedaço, devagar: vira "arrasto" no app PASSOS_ARRASTO = 24 def caminho(x0, y0, x1, y1, passos=PASSOS_ARRASTO, empurrao=EMPURRAO_PX): """Pontos inteiros de (x0, y0) até (x1, y1), sem o de partida e terminando exatamente no destino. Os primeiros cobrem só 'empurrao' px (em 3 passos); o resto vai com aceleração e freada (ease-in-out).""" dx, dy = x1 - x0, y1 - y0 distancia = (dx * dx + dy * dy) ** 0.5 if distancia < 1: return [(int(x1), int(y1))] pontos = [] inicio = min(1.0, empurrao / distancia) for i in (1, 2, 3): f = inicio * i / 3 pontos.append((round(x0 + dx * f), round(y0 + dy * f))) resto = max(2, passos - 3) for i in range(1, resto + 1): s = i / resto suave = s * s * (3 - 2 * s) # smoothstep f = inicio + (1 - inicio) * suave pontos.append((round(x0 + dx * f), round(y0 + dy * f))) pontos[-1] = (int(x1), int(y1)) limpos = [] for p in pontos: if not limpos or limpos[-1] != p: limpos.append(p) return limpos def recorte_em_volta(x, y, raio, largura, altura): """(x0, y0, x1, y1) do quadrado de lado 2*raio em volta de (x, y), empurrado pra dentro da imagem (sem encolher, se couber).""" lado = 2 * raio x0 = min(max(0, x - raio), max(0, largura - lado)) y0 = min(max(0, y - raio), max(0, altura - lado)) return x0, y0, min(largura, x0 + lado), min(altura, y0 + lado) SEMELHANCA_COR = 30 # soma R+G+B: até isso é "a mesma cor" (antisserrilhado à parte) def _mesma_cor(a, b): return abs(a[0] - b[0]) + abs(a[1] - b[1]) + abs(a[2] - b[2]) <= SEMELHANCA_COR def encaixar(imagem, x, y, raio=20): """Se o ponto caiu numa área lisa (errou o botão por pouco), vai pro centro do elemento INTEIRO mais perto (uma mancha de cor própria que cabe na região). Caiu em cima de algo: fica.""" try: img = imagem.convert("RGB") if not (0 <= x < img.width and 0 <= y < img.height): return x, y x0, y0, x1, y1 = recorte_em_volta(x, y, 3, img.width, img.height) cor = img.getpixel((x, y)) if not all(_mesma_cor(cor, img.getpixel((i, j))) for i in range(x0, x1) for j in range(y0, y1)): return x, y # já está em cima de alguma coisa x0, y0, x1, y1 = recorte_em_volta(x, y, raio, img.width, img.height) regiao = img.crop((x0, y0, x1, y1)) largura, altura = regiao.size px = regiao.load() visto = [[False] * largura for _ in range(altura)] lx, ly = x - x0, y - y0 melhor = None for j in range(altura): for i in range(largura): if visto[j][i]: continue semente = px[i, j] pilha, pontos = [(i, j)], [] visto[j][i] = True while pilha: a, b = pilha.pop() pontos.append((a, b)) for na, nb in ((a + 1, b), (a - 1, b), (a, b + 1), (a, b - 1)): if 0 <= na < largura and 0 <= nb < altura and not visto[nb][na] \ and _mesma_cor(px[na, nb], semente): visto[nb][na] = True pilha.append((na, nb)) xs = [a for a, _ in pontos] ys = [b for _, b in pontos] bx0, bx1, by0, by1 = min(xs), max(xs), min(ys), max(ys) # o fundo onde o ponto caiu, pedaços de coisa maior (encostam # na borda) e ruído não são "o botão" if (bx0 <= lx <= bx1 and by0 <= ly <= by1 and (lx, ly) in set(pontos)) \ or bx0 == 0 or by0 == 0 or bx1 == largura - 1 or by1 == altura - 1 \ or len(pontos) < 12: continue dx = max(bx0 - lx, 0, lx - bx1) dy = max(by0 - ly, 0, ly - by1) d = dx * dx + dy * dy if melhor is None or d < melhor[0]: melhor = (d, (bx0 + bx1) / 2, (by0 + by1) / 2) if melhor is None: return x, y return round(x0 + melhor[1]), round(y0 + melhor[2]) except Exception: return x, y # ------------------------------------------------------------ # A tela mudou? # ------------------------------------------------------------ LARGURA_COMPARACAO = 320 LIMIAR_PIXEL = 40 # diferença R+G+B pra contar como pixel mudado FRACAO_MUDOU = 0.002 # 0,2% dos pixels: uma aba fechando já passa disso def mudanca(antes, depois): """Fração (0–1) dos pixels que mudaram entre duas capturas do mesmo lugar. Compara em tamanho pequeno: rápido e ignora ruído de 1 px.""" if antes is None or depois is None or antes.size != depois.size: return None largura = min(LARGURA_COMPARACAO, antes.width) altura = max(1, round(antes.height * largura / antes.width)) a = antes.convert("RGB").resize((largura, altura)) b = depois.convert("RGB").resize((largura, altura)) pa, pb = a.load(), b.load() mudados = 0 for j in range(altura): for i in range(largura): ca, cb = pa[i, j], pb[i, j] if abs(ca[0] - cb[0]) + abs(ca[1] - cb[1]) + abs(ca[2] - cb[2]) > LIMIAR_PIXEL: mudados += 1 return mudados / (largura * altura) def mudou(fracao): return fracao is not None and fracao >= FRACAO_MUDOU # ------------------------------------------------------------ # 3.0 Miller: mover JANELA (não arrastar o que está escrito nela) # ------------------------------------------------------------ # "arraste a janela do openTARS pro topo esquerdo": antes o arrasto ia # até o TEXTO "openTARS" dentro da janela. Mover janela é com o # gerenciador de janelas (EWMH), que sabe onde fica a borda e a barra. _PALAVRAS_JANELA = {"janela", "window", "ventana", "fenetre", "fenster", "titlebar", "barra de titulo", "title bar", "barre de titre", "titelleiste", "barra de titulo"} _PALAVRAS_ABA = {"aba", "tab", "guia", "pestana", "onglet", "reiter", "tabs", "abas"} _RE_MAXIMIZAR = {"maximize", "maximizar", "maximizada", "maximiser", "maximieren", "tela cheia", "full screen", "fullscreen", "pantalla completa", "plein ecran", "vollbild", "inteira"} _RE_RESTAURAR = {"restore", "restaurar", "restaure", "wiederherstellen", "normal", "unmaximize"} def fala_de_janela(texto): """'janela do openTARS', 'barra de título', 'the firefox window' (mas não 'aba do YouTube').""" n = " ".join(_normalizar(texto)) palavras = set(n.split()) if palavras & _PALAVRAS_ABA: return False return bool(palavras & _PALAVRAS_JANELA) or any(p in n for p in _PALAVRAS_JANELA if " " in p) def sem_palavras_de_janela(texto): """'a janela do openTARS' -> 'openTARS'.""" fora = {"a", "o", "the", "la", "le", "die", "der", "da", "do", "de", "of", "del", "du", "des"} fora |= {p for p in _PALAVRAS_JANELA if " " not in p} fora |= {"barra", "titulo", "title", "bar", "barre", "titre", "titelleiste", "sua", "your", "minha", "my"} return " ".join(p for p in str(texto or "").split() if " ".join(_normalizar(p)) not in fora).strip() def lugar_da_janela(lugar, area, tamanho=None): """Onde pôr a janela: (x, y, largura, altura) do retângulo EXTERNO (com a borda), largura/altura None = não muda o tamanho; ou 'maximizar' / 'restaurar'; ou None se 'lugar' não é um lugar conhecido. area = (x, y, largura, altura) da área útil (sem painéis, _NET_WORKAREA); tamanho = (largura, altura) da janela por fora, pra encostar nos cantos da direita/de baixo e centralizar.""" palavras = set(_normalizar(lugar)) n = " ".join(_normalizar(lugar)) if palavras & _RE_MAXIMIZAR or any(p in n for p in _RE_MAXIMIZAR if " " in p): return "maximizar" if palavras & _RE_RESTAURAR: return "restaurar" ax, ay, al, aa = area vertical = next((k for k, v in _VERTICAL.items() if palavras & set(v)), None) horizontal = next((k for k, v in _HORIZONTAL.items() if palavras & set(v)), None) centro = bool(palavras & set(_CENTRO)) metade = bool(palavras & {"half", "metade", "mitad", "moitie", "halfte", "haelfte"}) if not (vertical or horizontal or centro): return None if metade: if horizontal and not vertical: return (ax if horizontal == "esquerda" else ax + al // 2, ay, al // 2, aa) if vertical and not horizontal: return (ax, ay if vertical == "topo" else ay + aa // 2, al, aa // 2) largura, altura = tamanho or (al // 2, aa // 2) largura, altura = min(largura, al), min(altura, aa) if horizontal == "esquerda": x = ax elif horizontal == "direita": x = ax + al - largura else: x = ax + (al - largura) // 2 if vertical == "topo": y = ay elif vertical == "base": y = ay + aa - altura else: y = ay + (aa - altura) // 2 return (x, y, None, None)