primocode 9.5.0 → 9.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,525 @@
1
+ """cursor.py — o cursor do Primo: o mouse de verdade, com a cara dele.
2
+
3
+ "Quando ele tá no modo, eu consigo ver o cursor dele, o mouse, ver ele
4
+ digitando, abrindo as coisas, ver ele fazer as coisas em tempo real."
5
+ (23/09/2026)
6
+
7
+ ── O QUE É ─────────────────────────────────────────────────────────────────
8
+ Um processo pequeno, só do macOS, que o Node sobe na primeira ação no
9
+ computador. Ele faz duas coisas ao mesmo tempo, e é a soma das duas que dá a
10
+ sensação de "ver o Primo trabalhando":
11
+
12
+ 1. MEXE NO MOUSE E NO TECLADO DE VERDADE, pelo Quartz (`CGEventPost`). Não é
13
+ um cursor de enfeite correndo por cima: o clique acontece no app que está
14
+ embaixo. Isso também aposenta o `cliclick`, que precisava de Homebrew e,
15
+ sem ele, fazia TODO clique falhar — foi por isso que "mostra o teu mouse"
16
+ virou uma página HTML com coordenadas.
17
+
18
+ 2. DESENHA O CURSOR DELE por cima do de verdade: uma seta escura com contorno
19
+ claro e brilho na cor da marca, um anel em volta, uma onda a cada clique e
20
+ uma etiqueta curta do que está fazendo ("digitando", "abrindo Safari").
21
+ Numa janela transparente, acima de tudo, que NÃO recebe clique (o clique
22
+ atravessa para o app) e que não aparece em captura de tela (o print que o
23
+ agente tira mostra a tela, não o cursor dele).
24
+
25
+ O desenho é o do Jarvis (~/Jarvis/src/jarvis/ui/web/hud.html e
26
+ motor/pointer.py): o mesmo trajeto em curva com aceleração suave — reta
27
+ parece robô — e o mesmo sumiço 1,6 s depois da última ação, devolvendo a tela
28
+ ao cursor da pessoa.
29
+
30
+ ── O PROTOCOLO ─────────────────────────────────────────────────────────────
31
+ Uma linha de JSON por comando no stdin, uma de resposta no stdout:
32
+
33
+ {"id": 1, "cmd": "clicar", "x": 400, "y": 300, "botao": "esquerdo", "duplo": false}
34
+ {"id": 1, "ok": true}
35
+
36
+ Comandos: mover, clicar, arrastar, rolar, digitar, tecla, rotulo, ocupado,
37
+ esconder, permissao, tamanho. Coordenadas em PONTOS da tela principal, com a
38
+ origem no canto de cima à esquerda — as mesmas do System Events, que é de onde
39
+ vêm as posições dos botões.
40
+ """
41
+ from __future__ import annotations
42
+
43
+ import json
44
+ import math
45
+ import random
46
+ import sys
47
+ import threading
48
+ import time
49
+
50
+ import objc
51
+ import Quartz
52
+ from AppKit import (NSApplication, NSBezierPath, NSColor, NSFont, NSMakeRect,
53
+ NSObject, NSScreen, NSShadow, NSTimer, NSView, NSWindow,
54
+ NSBackingStoreBuffered, NSCompositingOperationClear,
55
+ NSRectFillUsingOperation, NSFontAttributeName,
56
+ NSForegroundColorAttributeName)
57
+ from Foundation import NSString
58
+ from PyObjCTools import AppHelper
59
+
60
+ # ── A cara ──────────────────────────────────────────────────────────────────
61
+ CORES = {
62
+ "normal": (0.37, 0.91, 1.0), # ciano da marca
63
+ "agindo": (1.0, 0.71, 0.32), # âmbar: está mexendo em alguma coisa
64
+ }
65
+ PASSOS_HZ = 120 # quantas vezes por segundo o mouse anda no trajeto
66
+ SOME_DEPOIS = 1.6 # segundos parado até o cursor dele sumir
67
+ NIVEL = 1000 # NSScreenSaverWindowLevel: acima de qualquer app
68
+
69
+
70
+ def _cor(rgb, alfa=1.0):
71
+ return NSColor.colorWithSRGBRed_green_blue_alpha_(rgb[0], rgb[1], rgb[2], alfa)
72
+
73
+
74
+ # ── O mouse e o teclado de verdade ──────────────────────────────────────────
75
+
76
+ def _post(evento) -> None:
77
+ Quartz.CGEventPost(Quartz.kCGHIDEventTap, evento)
78
+
79
+
80
+ def posicao():
81
+ p = Quartz.CGEventGetLocation(Quartz.CGEventCreate(None))
82
+ return float(p.x), float(p.y)
83
+
84
+
85
+ def pode_agir() -> bool:
86
+ """O sistema deixa este processo mexer no mouse e no teclado?
87
+
88
+ `CGPreflightPostEventAccess` e não `AXIsProcessTrusted`: este último fica
89
+ em cache pela vida do processo, e continuava dizendo "não" depois de a
90
+ pessoa conceder a permissão (medido no Jarvis, motor/permissions.py).
91
+ """
92
+ try:
93
+ return bool(Quartz.CGPreflightPostEventAccess())
94
+ except Exception:
95
+ return True # macOS antigo sem a função: tenta e vê
96
+
97
+
98
+ def pedir_permissao() -> None:
99
+ try:
100
+ Quartz.CGRequestPostEventAccess()
101
+ except Exception:
102
+ pass
103
+
104
+
105
+ # Códigos de tecla do layout americano: o atalho é pelo CÓDIGO, não pela
106
+ # letra, então vale em qualquer layout de teclado.
107
+ TECLAS = {
108
+ "a": 0, "s": 1, "d": 2, "f": 3, "h": 4, "g": 5, "z": 6, "x": 7, "c": 8, "v": 9,
109
+ "b": 11, "q": 12, "w": 13, "e": 14, "r": 15, "y": 16, "t": 17, "1": 18, "2": 19,
110
+ "3": 20, "4": 21, "6": 22, "5": 23, "=": 24, "9": 25, "7": 26, "-": 27, "8": 28,
111
+ "0": 29, "]": 30, "o": 31, "u": 32, "[": 33, "i": 34, "p": 35, "l": 37, "j": 38,
112
+ "'": 39, "k": 40, ";": 41, "\\": 42, ",": 43, "/": 44, "n": 45, "m": 46, ".": 47,
113
+ "`": 50,
114
+ "enter": 36, "return": 36, "tab": 48, "space": 49, "espaco": 49, "espaço": 49,
115
+ "delete": 51, "backspace": 51, "apagar": 51, "esc": 53, "escape": 53,
116
+ "forwarddelete": 117, "del": 117, "home": 115, "end": 119, "pageup": 116,
117
+ "pagedown": 121, "left": 123, "esquerda": 123, "right": 124, "direita": 124,
118
+ "down": 125, "baixo": 125, "up": 126, "cima": 126,
119
+ "f1": 122, "f2": 120, "f3": 99, "f4": 118, "f5": 96, "f6": 97, "f7": 98,
120
+ "f8": 100, "f9": 101, "f10": 109, "f11": 103, "f12": 111,
121
+ }
122
+ MODIFICADORES = {
123
+ "cmd": Quartz.kCGEventFlagMaskCommand, "command": Quartz.kCGEventFlagMaskCommand,
124
+ "super": Quartz.kCGEventFlagMaskCommand, "win": Quartz.kCGEventFlagMaskCommand,
125
+ "meta": Quartz.kCGEventFlagMaskCommand,
126
+ "ctrl": Quartz.kCGEventFlagMaskControl, "control": Quartz.kCGEventFlagMaskControl,
127
+ "alt": Quartz.kCGEventFlagMaskAlternate, "option": Quartz.kCGEventFlagMaskAlternate,
128
+ "opt": Quartz.kCGEventFlagMaskAlternate,
129
+ "shift": Quartz.kCGEventFlagMaskShift,
130
+ }
131
+
132
+
133
+ def combo(nome: str):
134
+ """"cmd+shift+t" → (código, flags). None se a tecla não for conhecida."""
135
+ partes = [p.strip().lower() for p in str(nome).replace(" ", "").split("+") if p.strip()]
136
+ if not partes:
137
+ return None
138
+ flags = 0
139
+ for p in partes[:-1]:
140
+ if p not in MODIFICADORES:
141
+ return None
142
+ flags |= MODIFICADORES[p]
143
+ tecla = partes[-1]
144
+ if tecla not in TECLAS:
145
+ return None
146
+ return TECLAS[tecla], flags
147
+
148
+
149
+ def apertar(codigo: int, flags: int = 0) -> None:
150
+ for desce in (True, False):
151
+ ev = Quartz.CGEventCreateKeyboardEvent(None, codigo, desce)
152
+ if flags:
153
+ Quartz.CGEventSetFlags(ev, flags)
154
+ _post(ev)
155
+ time.sleep(0.012)
156
+
157
+
158
+ def digitar(texto: str) -> None:
159
+ """Letra por letra, pelo caractere — acento e emoji saem certos em
160
+ qualquer layout de teclado. Quebra de linha é Return de verdade."""
161
+ for ch in str(texto):
162
+ if ch == "\n":
163
+ apertar(36)
164
+ elif ch == "\t":
165
+ apertar(48)
166
+ else:
167
+ for desce in (True, False):
168
+ ev = Quartz.CGEventCreateKeyboardEvent(None, 0, desce)
169
+ Quartz.CGEventKeyboardSetUnicodeString(ev, len(ch), ch)
170
+ _post(ev)
171
+ time.sleep(0.008)
172
+
173
+
174
+ # ── A janela do cursor ──────────────────────────────────────────────────────
175
+
176
+ class VistaCursor(NSView):
177
+ """Pinta o cursor do Primo. Coordenadas viradas: y cresce para baixo,
178
+ como as do Quartz — o mesmo número serve para os dois."""
179
+
180
+ def initWithFrame_(self, quadro):
181
+ self = objc.super(VistaCursor, self).initWithFrame_(quadro)
182
+ if self is None:
183
+ return None
184
+ self.x, self.y = posicao()
185
+ self.alfa = 0.0 # 0..1, para aparecer e sumir sem piscar
186
+ self.visivel = False
187
+ self.estado = "normal"
188
+ self.rotulo = ""
189
+ self.ocupado = False
190
+ self.ondas = [] # [(x, y, começo)] de cada clique
191
+ self.fase = 0.0
192
+ self.ultima = 0.0 # hora da última ação
193
+ return self
194
+
195
+ def isFlipped(self):
196
+ return True
197
+
198
+ def isOpaque(self):
199
+ return False
200
+
201
+ def drawRect_(self, rect):
202
+ NSColor.clearColor().set()
203
+ NSRectFillUsingOperation(self.bounds(), NSCompositingOperationClear)
204
+ if self.alfa <= 0.01 and not self.ondas:
205
+ return
206
+ a = self.alfa
207
+ cor = CORES.get(self.estado, CORES["normal"])
208
+ x, y = self.x, self.y
209
+ agora = time.time()
210
+
211
+ # A ONDA de cada clique: um anel que abre e some em meio segundo.
212
+ vivas = []
213
+ for (ox, oy, t0) in self.ondas:
214
+ t = (agora - t0) / 0.5
215
+ if t >= 1:
216
+ continue
217
+ vivas.append((ox, oy, t0))
218
+ r = 6 + 30 * t
219
+ _cor(cor, (1 - t) * 0.9).set()
220
+ anel = NSBezierPath.bezierPathWithOvalInRect_(NSMakeRect(ox - r, oy - r, 2 * r, 2 * r))
221
+ anel.setLineWidth_(2.5 * (1 - t) + 0.5)
222
+ anel.stroke()
223
+ self.ondas = vivas
224
+
225
+ if a <= 0.01:
226
+ return
227
+
228
+ # HALO: um anel cheio e um tracejado girando, em volta da ponta.
229
+ _cor(cor, 0.35 * a).set()
230
+ halo = NSBezierPath.bezierPathWithOvalInRect_(NSMakeRect(x - 20, y - 20, 40, 40))
231
+ halo.setLineWidth_(1.5)
232
+ halo.stroke()
233
+ _cor(cor, 0.55 * a).set()
234
+ giro = NSBezierPath.bezierPathWithOvalInRect_(NSMakeRect(x - 14, y - 14, 28, 28))
235
+ giro.setLineWidth_(1.2)
236
+ giro.setLineDash_count_phase_([4.0, 5.0], 2, self.fase * 18)
237
+ giro.stroke()
238
+
239
+ # A SETA: escura e cheia (para cobrir a seta do sistema embaixo), com
240
+ # contorno claro e brilho na cor do estado. A ponta é o ponto do mouse.
241
+ pontos = [(0, 0), (0, 25), (6.2, 19.4), (10.6, 29.2), (14.6, 27.4), (10.3, 17.9), (18.5, 17.9)]
242
+ seta = NSBezierPath.bezierPath()
243
+ seta.moveToPoint_((x + pontos[0][0], y + pontos[0][1]))
244
+ for (px, py) in pontos[1:]:
245
+ seta.lineToPoint_((x + px, y + py))
246
+ seta.closePath()
247
+ sombra = NSShadow.alloc().init()
248
+ sombra.setShadowColor_(_cor(cor, 0.9 * a))
249
+ sombra.setShadowBlurRadius_(10)
250
+ sombra.setShadowOffset_((0, 0))
251
+ from AppKit import NSGraphicsContext
252
+ NSGraphicsContext.saveGraphicsState()
253
+ sombra.set()
254
+ NSColor.colorWithSRGBRed_green_blue_alpha_(0.02, 0.043, 0.075, 0.96 * a).set()
255
+ seta.fill()
256
+ NSGraphicsContext.restoreGraphicsState()
257
+ NSColor.colorWithSRGBRed_green_blue_alpha_(0.92, 0.98, 1.0, a).set()
258
+ seta.setLineWidth_(1.6)
259
+ seta.setLineJoinStyle_(1)
260
+ seta.stroke()
261
+ # o núcleo de energia dentro da seta
262
+ _cor(cor, a).set()
263
+ NSBezierPath.bezierPathWithOvalInRect_(NSMakeRect(x + 3.2, y + 10.5, 5, 5)).fill()
264
+
265
+ # OCUPADO: um arco girando embaixo à direita da seta.
266
+ if self.ocupado:
267
+ _cor(cor, 0.9 * a).set()
268
+ arco = NSBezierPath.bezierPath()
269
+ arco.appendBezierPathWithArcWithCenter_radius_startAngle_endAngle_(
270
+ (x + 24, y + 30), 6, self.fase * 360 % 360, (self.fase * 360 + 250) % 360)
271
+ arco.setLineWidth_(2)
272
+ arco.stroke()
273
+
274
+ # A ETIQUETA do que ele está fazendo.
275
+ if self.rotulo:
276
+ atributos = {
277
+ NSFontAttributeName: NSFont.systemFontOfSize_weight_(12, 0.3),
278
+ NSForegroundColorAttributeName: NSColor.colorWithSRGBRed_green_blue_alpha_(0.95, 0.98, 1.0, a),
279
+ }
280
+ texto = NSString.stringWithString_(self.rotulo)
281
+ tam = texto.sizeWithAttributes_(atributos)
282
+ bx, by = x + 22, y + 34
283
+ caixa = NSBezierPath.bezierPathWithRoundedRect_xRadius_yRadius_(
284
+ NSMakeRect(bx, by, tam.width + 18, tam.height + 8), 9, 9)
285
+ NSColor.colorWithSRGBRed_green_blue_alpha_(0.03, 0.06, 0.1, 0.86 * a).set()
286
+ caixa.fill()
287
+ _cor(cor, 0.6 * a).set()
288
+ caixa.setLineWidth_(1)
289
+ caixa.stroke()
290
+ texto.drawAtPoint_withAttributes_((bx + 9, by + 4), atributos)
291
+
292
+
293
+ class Relogio(NSObject):
294
+ """Anima o cursor a 60 quadros por segundo, na thread da interface."""
295
+
296
+ def initComVista_(self, vista):
297
+ self = objc.super(Relogio, self).init()
298
+ if self is None:
299
+ return None
300
+ self.vista = vista
301
+ return self
302
+
303
+ def tique_(self, _timer):
304
+ v = self.vista
305
+ v.fase += 1 / 60
306
+ parado = time.time() - v.ultima
307
+ alvo = 1.0 if (v.visivel and (v.ocupado or parado < SOME_DEPOIS)) else 0.0
308
+ v.alfa += (alvo - v.alfa) * 0.25
309
+ if alvo == 0.0 and v.alfa < 0.02:
310
+ v.alfa = 0.0
311
+ if not v.ocupado:
312
+ v.rotulo = ""
313
+ v.setNeedsDisplay_(True)
314
+
315
+
316
+ class Cursor:
317
+ def __init__(self):
318
+ self.app = NSApplication.sharedApplication()
319
+ self.app.setActivationPolicy_(1) # acessório: sem ícone no Dock
320
+ tela = NSScreen.screens()[0].frame() # a tela principal (origem 0,0)
321
+ self.largura, self.altura = tela.size.width, tela.size.height
322
+ self.janela = NSWindow.alloc().initWithContentRect_styleMask_backing_defer_(
323
+ tela, 0, NSBackingStoreBuffered, False)
324
+ j = self.janela
325
+ j.setOpaque_(False)
326
+ j.setBackgroundColor_(NSColor.clearColor())
327
+ j.setHasShadow_(False)
328
+ j.setLevel_(NIVEL)
329
+ j.setIgnoresMouseEvents_(True) # o clique atravessa para o app
330
+ # Fora das capturas de tela (o print do agente mostra a tela, não o
331
+ # cursor dele). PRIMO_CURSOR_CAPTURAVEL=1 libera, para o teste fotografar.
332
+ import os
333
+ j.setSharingType_(1 if os.environ.get("PRIMO_CURSOR_CAPTURAVEL") == "1" else 0)
334
+ j.setCollectionBehavior_(1 | 16 | 64 | 256)
335
+ self.vista = VistaCursor.alloc().initWithFrame_(NSMakeRect(0, 0, self.largura, self.altura))
336
+ j.setContentView_(self.vista)
337
+ j.orderFrontRegardless()
338
+ self.relogio = Relogio.alloc().initComVista_(self.vista)
339
+ NSTimer.scheduledTimerWithTimeInterval_target_selector_userInfo_repeats_(
340
+ 1 / 60, self.relogio, "tique:", None, True)
341
+
342
+ # ── o que a interface mostra (sempre na thread dela) ────────────────────
343
+ def _ui(self, **campos):
344
+ def aplicar():
345
+ for k, v in campos.items():
346
+ setattr(self.vista, k, v)
347
+ self.vista.ultima = time.time()
348
+ AppHelper.callAfter(aplicar)
349
+
350
+ def _onda(self, x, y):
351
+ AppHelper.callAfter(lambda: self.vista.ondas.append((x, y, time.time())))
352
+
353
+ # ── o trajeto ───────────────────────────────────────────────────────────
354
+ def mover(self, x: float, y: float, arrastando: bool = False) -> None:
355
+ """Vai até (x, y) numa curva leve, acelerando e freando.
356
+
357
+ Reta com velocidade constante é o que denuncia robô. O ponto de
358
+ controle da curva sai para o lado, proporcional à distância, com teto.
359
+ """
360
+ x = max(0.0, min(self.largura - 1, float(x)))
361
+ y = max(0.0, min(self.altura - 1, float(y)))
362
+ x0, y0 = posicao()
363
+ dist = math.hypot(x - x0, y - y0)
364
+ self._ui(visivel=True, estado="agindo")
365
+ if dist < 1:
366
+ self._ui(x=x, y=y)
367
+ return
368
+ dur = max(0.14, min(1.2, 0.22 + dist / 1700))
369
+ passos = max(2, int(dur * PASSOS_HZ))
370
+ desvio = min(dist * 0.12, 90) * random.choice((-1, 1))
371
+ mx, my = (x0 + x) / 2, (y0 + y) / 2
372
+ nx, ny = -(y - y0) / dist, (x - x0) / dist
373
+ cx, cy = mx + nx * desvio, my + ny * desvio
374
+ tipo = Quartz.kCGEventLeftMouseDragged if arrastando else Quartz.kCGEventMouseMoved
375
+ for i in range(1, passos + 1):
376
+ t = i / passos
377
+ e = 0.5 - 0.5 * math.cos(math.pi * t)
378
+ px = (1 - e) ** 2 * x0 + 2 * (1 - e) * e * cx + e ** 2 * x
379
+ py = (1 - e) ** 2 * y0 + 2 * (1 - e) * e * cy + e ** 2 * y
380
+ _post(Quartz.CGEventCreateMouseEvent(None, tipo, (px, py), Quartz.kCGMouseButtonLeft))
381
+ if i % 2 == 0 or i == passos:
382
+ self._ui(x=px, y=py)
383
+ time.sleep(dur / passos)
384
+
385
+ def clicar(self, x, y, botao="esquerdo", duplo=False) -> None:
386
+ self.mover(x, y)
387
+ time.sleep(0.04) # o app vê o mouse chegar antes do clique
388
+ direito = str(botao).lower() in ("direito", "right")
389
+ desce = Quartz.kCGEventRightMouseDown if direito else Quartz.kCGEventLeftMouseDown
390
+ sobe = Quartz.kCGEventRightMouseUp if direito else Quartz.kCGEventLeftMouseUp
391
+ qual = Quartz.kCGMouseButtonRight if direito else Quartz.kCGMouseButtonLeft
392
+ for n in ((1, 2) if duplo else (1,)):
393
+ for tipo in (desce, sobe):
394
+ ev = Quartz.CGEventCreateMouseEvent(None, tipo, (x, y), qual)
395
+ Quartz.CGEventSetIntegerValueField(ev, Quartz.kCGMouseEventClickState, n)
396
+ _post(ev)
397
+ self._onda(x, y)
398
+ time.sleep(0.09)
399
+
400
+ def arrastar(self, x1, y1, x2, y2) -> None:
401
+ self.mover(x1, y1)
402
+ _post(Quartz.CGEventCreateMouseEvent(None, Quartz.kCGEventLeftMouseDown, (x1, y1), Quartz.kCGMouseButtonLeft))
403
+ time.sleep(0.12)
404
+ self.mover(x2, y2, arrastando=True)
405
+ _post(Quartz.CGEventCreateMouseEvent(None, Quartz.kCGEventLeftMouseUp, (x2, y2), Quartz.kCGMouseButtonLeft))
406
+ self._onda(x2, y2)
407
+
408
+ def rolar(self, dy) -> None:
409
+ linhas = int(round(float(dy) / 50)) or (1 if float(dy) > 0 else -1)
410
+ x, y = posicao()
411
+ self._ui(visivel=True, x=x, y=y)
412
+ passo = -1 if linhas > 0 else 1 # dy > 0 rola para BAIXO
413
+ for _ in range(abs(linhas)):
414
+ _post(Quartz.CGEventCreateScrollWheelEvent(None, Quartz.kCGScrollEventUnitLine, 1, passo * 3))
415
+ time.sleep(0.03)
416
+
417
+
418
+ # ── O laço de comandos ──────────────────────────────────────────────────────
419
+
420
+ def responder(obj) -> None:
421
+ sys.stdout.write(json.dumps(obj, ensure_ascii=False) + "\n")
422
+ sys.stdout.flush()
423
+
424
+
425
+ def executar(cur: Cursor, msg: dict) -> dict:
426
+ cmd = msg.get("cmd")
427
+ if cmd == "permissao":
428
+ ok = pode_agir()
429
+ if not ok and msg.get("pedir"):
430
+ pedir_permissao()
431
+ return {"ok": ok}
432
+ if cmd == "tamanho":
433
+ return {"ok": True, "largura": int(cur.largura), "altura": int(cur.altura)}
434
+ if cmd == "rotulo":
435
+ cur._ui(rotulo=str(msg.get("texto") or "")[:48], visivel=True, x=posicao()[0], y=posicao()[1])
436
+ return {"ok": True}
437
+ if cmd == "ocupado":
438
+ cur._ui(ocupado=bool(msg.get("sim")), visivel=True)
439
+ return {"ok": True}
440
+ if cmd == "esconder":
441
+ cur._ui(visivel=False, ocupado=False, rotulo="")
442
+ return {"ok": True}
443
+ if cmd == "foto":
444
+ # Só para o teste: renderiza a vista do cursor numa imagem, em volta
445
+ # dele. Não é captura de tela (a janela fica fora delas de propósito).
446
+ import os
447
+ if os.environ.get("PRIMO_CURSOR_CAPTURAVEL") != "1":
448
+ return {"ok": False, "error": "foto só no modo de teste"}
449
+ feito = threading.Event()
450
+ saida = {}
451
+ def render():
452
+ v = cur.vista
453
+ area = NSMakeRect(max(0, v.x - 60), max(0, v.y - 60), 240, 160)
454
+ rep = v.bitmapImageRepForCachingDisplayInRect_(area)
455
+ v.cacheDisplayInRect_toBitmapImageRep_(area, rep)
456
+ rep.representationUsingType_properties_(4, None).writeToFile_atomically_(msg["caminho"], True)
457
+ saida.update(alfa=round(v.alfa, 2), x=v.x, y=v.y)
458
+ feito.set()
459
+ AppHelper.callAfter(render)
460
+ feito.wait(3)
461
+ return {"ok": bool(saida), **saida}
462
+
463
+ # Daqui para baixo, mexe no computador: sem a permissão, o sistema engole
464
+ # os eventos calado. Melhor dizer do que fingir que clicou.
465
+ if not pode_agir():
466
+ pedir_permissao()
467
+ return {"ok": False, "permissao": False,
468
+ "error": "o macOS não deixou mexer no mouse e no teclado. Libere o Terminal (ou o app "
469
+ "de onde você abriu o PrimoCode) em Ajustes do Sistema > Privacidade e "
470
+ "Segurança > Acessibilidade, e peça de novo."}
471
+ if "rotulo" in msg:
472
+ cur._ui(rotulo=str(msg.get("rotulo") or "")[:48])
473
+ if cmd == "mover":
474
+ cur.mover(msg["x"], msg["y"])
475
+ elif cmd == "clicar":
476
+ cur.clicar(msg["x"], msg["y"], msg.get("botao", "esquerdo"), bool(msg.get("duplo")))
477
+ elif cmd == "arrastar":
478
+ cur.arrastar(msg["x1"], msg["y1"], msg["x2"], msg["y2"])
479
+ elif cmd == "rolar":
480
+ cur.rolar(msg.get("dy", 300))
481
+ elif cmd == "digitar":
482
+ cur._ui(visivel=True, x=posicao()[0], y=posicao()[1], ocupado=True)
483
+ digitar(msg.get("texto", ""))
484
+ cur._ui(ocupado=False)
485
+ elif cmd == "tecla":
486
+ c = combo(msg.get("combo", ""))
487
+ if not c:
488
+ return {"ok": False, "error": f'tecla desconhecida: "{msg.get("combo")}". Use nomes como '
489
+ '"enter", "esc", "tab", "cmd+t", "cmd+shift+4", "left".'}
490
+ cur._ui(visivel=True, x=posicao()[0], y=posicao()[1])
491
+ apertar(*c)
492
+ else:
493
+ return {"ok": False, "error": f"comando desconhecido: {cmd}"}
494
+ return {"ok": True}
495
+
496
+
497
+ def ler_comandos(cur: Cursor) -> None:
498
+ for linha in sys.stdin:
499
+ linha = linha.strip()
500
+ if not linha:
501
+ continue
502
+ try:
503
+ msg = json.loads(linha)
504
+ except ValueError:
505
+ continue
506
+ try:
507
+ r = executar(cur, msg)
508
+ except Exception as e: # um comando ruim não derruba o cursor
509
+ r = {"ok": False, "error": str(e)}
510
+ r["id"] = msg.get("id")
511
+ responder(r)
512
+ # stdin fechou: o PrimoCode saiu. O cursor sai junto.
513
+ AppHelper.callAfter(AppHelper.stopEventLoop)
514
+
515
+
516
+ def main() -> int:
517
+ cur = Cursor()
518
+ threading.Thread(target=ler_comandos, args=(cur,), daemon=True).start()
519
+ responder({"pronto": True, "permissao": pode_agir()})
520
+ AppHelper.runEventLoop(installInterrupt=True)
521
+ return 0
522
+
523
+
524
+ if __name__ == "__main__":
525
+ sys.exit(main())
@@ -174,7 +174,10 @@ def _criar_nativa(orbe: "Orbe"):
174
174
  janela.setBackgroundColor_(NSColor.clearColor())
175
175
  janela.setHasShadow_(False)
176
176
  janela.setLevel_(25) # NSStatusWindowLevel: por cima de tudo
177
- janela.setMovableByWindowBackground_(True) # arrasta pela orb
177
+ # O clique ATRAVESSA a orb: ela mora no pé da tela, em cima do Dock e
178
+ # do que o cursor do Primo precisa clicar. Uma orb que segura o clique
179
+ # faria o agente clicar nela em vez de no app.
180
+ janela.setIgnoresMouseEvents_(True)
178
181
  janela.setCollectionBehavior_(1 | 16) # todos os espaços; fora do Exposé
179
182
  janela.setReleasedWhenClosed_(False)
180
183
  vista = Vista.alloc().initWithFrame_(NSMakeRect(0, 0, LARGURA, ALTURA))