@jian-ai/remote-control 0.8.1 → 0.8.3
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.
- package/README.md +22 -4
- package/bin/computer-linux.mjs +537 -0
- package/bin/computer-macos.mjs +205 -62
- package/bin/computer-shared.mjs +61 -0
- package/bin/computer-windows.mjs +516 -0
- package/bin/computer.mjs +63 -0
- package/bin/jian-remote.mjs +819 -105
- package/linux/computer-helper.py +695 -0
- package/macos/computer-helper +0 -0
- package/macos/computer-helper.swift +416 -141
- package/macos/jian-pointer.png +0 -0
- package/package.json +3 -1
- package/windows/computer-helper.ps1 +501 -0
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
/*
|
|
2
2
|
Jian Remote Control — macOS computer-use helper
|
|
3
3
|
|
|
4
|
-
Long-lived AppKit process: overlay pointer + synthetic mouse/keyboard via CoreGraphics.
|
|
4
|
+
Long-lived AppKit process: Jian overlay pointer + synthetic mouse/keyboard via CoreGraphics.
|
|
5
5
|
Protocol: newline-delimited JSON on stdin → newline-delimited JSON on stdout (flush each).
|
|
6
6
|
Errors go to stderr in English only.
|
|
7
7
|
|
|
8
|
+
Pointer UX (Lucas / v0.8.3):
|
|
9
|
+
- Bitmap sprite: white fill + thick #2880F8 stroke + soft blue glow (embedded PNG).
|
|
10
|
+
Prefer sprite over live CGPath so AA/glow match the approved preview.
|
|
11
|
+
- Agent actions save the user’s cursor, hide it briefly, drive HID at the target, then
|
|
12
|
+
restore — so clicks do not leave the physical mouse stranded at the Jian position.
|
|
13
|
+
- Overlay tip animates (ease-out) between positions; system cursor is not the visible UX.
|
|
14
|
+
|
|
8
15
|
Published npm packages ship a prebuilt universal binary (Publish CI). From source on a Mac:
|
|
9
16
|
./build-helper.sh
|
|
10
17
|
# or: swiftc -O -framework AppKit -framework Carbon -o computer-helper computer-helper.swift
|
|
@@ -73,16 +80,151 @@ private func intValue(_ object: [String: Any], _ key: String) -> Int? {
|
|
|
73
80
|
return nil
|
|
74
81
|
}
|
|
75
82
|
|
|
76
|
-
// MARK: -
|
|
83
|
+
// MARK: - Pointer sprite (embedded PNG)
|
|
77
84
|
|
|
78
|
-
|
|
79
|
-
private let
|
|
85
|
+
/// Soft click-ripple blue (matches ship stroke #2880F8).
|
|
86
|
+
private let jianGlow = NSColor(srgbRed: 0x28 / 255, green: 0x80 / 255, blue: 0xF8 / 255, alpha: 1)
|
|
80
87
|
|
|
81
|
-
private let sessionAlpha: CGFloat = 0.
|
|
88
|
+
private let sessionAlpha: CGFloat = 0.94
|
|
82
89
|
private let idleAlpha: CGFloat = 0.18
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
90
|
+
/// Padded sprite canvas (40 pt blade + 10 pt glow pad each side).
|
|
91
|
+
private let pointerSize: CGFloat = 60
|
|
92
|
+
/// Tip hotspot in the padded 60 pt view (content tip was (2,2) in the 40 pt box).
|
|
93
|
+
private let hotspot = CGPoint(x: 12, y: 12)
|
|
94
|
+
|
|
95
|
+
/// Ship asset `macos/jian-pointer.png` (@3x master) as base64 — single-file helper stays
|
|
96
|
+
/// reliable when only the binary is copied into Application Support (no sidecar PNG).
|
|
97
|
+
private let jianPointerPngBase64 =
|
|
98
|
+
"iVBORw0KGgoAAAANSUhEUgAAALQAAAC0CAYAAAA9zQYyAAAaf0lEQVR4nO1dW6wkV3Vd+1T38EcQ"
|
|
99
|
+
"Fi+bGc+MZ+w4g4AoJIoQkSIh8oNEflAUPhARQrZxHIcoxglfiD+IQTaDI5IoEZHgI5FCflBeH0mk"
|
|
100
|
+
"RCbGCEuQsQdzPXc8Y5vwCijhJ3du19n5OK99TlXfe7v7VN+err3s7nr0o2qq1121zt7n7AMoFAqF"
|
|
101
|
+
"QqFQKBQKhUKhUCgUCoVCoVAoFAqFQqFQKBQKhUKhUCgUCoVCoTh20HGfwHyQODfm4zsPxc2EYyQ0"
|
|
102
|
+
"VTq2kl2RsCZC95B31SPPpbESfMxYA6EFmXuPRnM35oB7V3s2ldzjw4CELlS5y9tDjl2+fCA3ufOe"
|
|
103
|
+
"7O1K7LFgIELPU+U+IpNcULG3A04rXRJnO5TcY8QAhO4jc0lkAigxueckDjqvDlPZPfURnPuJraTe"
|
|
104
|
+
"VlQmNM3T2STDFNgtX6O+M+nZyfFJrqLLaZ5LbCX1VqMioXvJfACRKawWxD2kEemoW8ouC4KnHUrs"
|
|
105
|
+
"0aESoQ8nc5fIgbgkxbrfX2eQqizJGYjJGbl7iK2k3mJM6n/lXDKT42sgvyB011NT8X0Ckb2UFJn9"
|
|
106
|
+
"+8Sqf4UAcNpmt0VuiwK3iZTU24EKCi3VeR6ZpSqLdU9wimTPuEy9ycRcjd0Tc3xy+5j9WxhJoVWt"
|
|
107
|
+
"R4A6Ct3hXYfMXoUDodOOqNpOsenOj/3wp+FbvvPJ170yHYDlsTjxjhlgJqfDgbhecYMgu/eSV29h"
|
|
108
|
+
"sv0fEHM6hKr1zYwVFbpPnecocyKzYzZRRvI7P/aDn/YdYedTt77KfS6KqfDK0ic7YoulV225H/F9"
|
|
109
|
+
"c9VaY9Y3NWoSOnjjaJZz9XVLgnHbhxB5HnYeOXkLgEBMwKk1g9mz2HaJHdaL/eg2GtWC3OSoROh5"
|
|
110
|
+
"6izJbIIqE8gQiMydf/j9/13mqM9/5sxrI6EjkQNRreW0fhDZD/DWQc2Rbys2Hit46DJUB3S8MoQy"
|
|
111
|
+
"JzIbENGyZAYAMpOGBaHJk9XtM0Rk3TbIghypyVkOL82e1MJbewcewiRui4Jah9apEnvTsYJCp2gc"
|
|
112
|
+
"kIjsNNoAgAEZgMgkm2GMU2pjQGTOP/zyj1c5+SuP3XVbICdHNbaWnRJbp9CWmdM6/I6uNQF61Fot"
|
|
113
|
+
"yE2GGoSOcWXKIxnk1JKMsxhOmYkaAzLm/MMvrUTmgN2LF05xICuz9es2EdtaWGbAitekorNFEm+1"
|
|
114
|
+
"IDc5KhE6euegxADIkPfKgdBOmRs6//BLP6l0/gCA3cfffMYR1BHZq7FlG4md9ol1562TzxYNR09p"
|
|
115
|
+
"ZsliVevNR8VMobfMyYL4DZ88caR2Sl0ZZx/41lUA2H38LWeTOreGYFtma4hbC6aWmYiYCEyWmQC2"
|
|
116
|
+
"TL616FhqQ6aRiV2LoIhZc0o/Ahqz3jwsQa6gwkAQeOrkqlOgw9kMQwCZqNID4ewD39ylZjqFmU7J"
|
|
117
|
+
"TCdophNqJg2ZSQMzmRA1DfzDrRsDagyRMd4auQecPUL4h6ZsPIk7Uroeio3BquSSzE6ZvxiLjiE6"
|
|
118
|
+
"H7YbRqElztz/9A410wk10wkZ94CZTsg4YrtHY2Aak0jtrFC6i1D2iFYqkTeRWqb0FceOupbDL72j"
|
|
119
|
+
"TmQIihcVcFic+fDXnwOAq5//xbvAxoJMCzbEZCzZ1lkP27ZsYIkJYAKzJTAsOQthmRmgaEFC2tx1"
|
|
120
|
+
"itK0+caiErlILoV0CZWLUQ6zNiU78+GvP0cmqPVkQmYygVs2aRktiIGPwKBrQWiOBYFakM1CDUKn"
|
|
121
|
+
"BItMrGRZwmg5DMzwCi1x+r6vXSZzYormxDSzIs5bT4gmDZmmgWkaIiMtiFELcvNhBXJ1MoVhW0hX"
|
|
122
|
+
"SLYEhVvMQ1/++CuWPz2B0/d+9Zkz9z31bfKkRhO89aShZuKW1BiYiSHTGOexTWzEUqHSgdRRtPPI"
|
|
123
|
+
"jqr1MWI1tSwyheG/dBuWqmaWinLUIjUAnL73yWc9qaNaIzUWJ06pJ30WxFsmtSCbjsoeOhkP8YNL"
|
|
124
|
+
"H+1u4QuiKqnv8WrtQnvT4LHRJFJHC2KahowRvlotyKajrp+NiRXpnfMedstGOS5//BV1iX3f1y5T"
|
|
125
|
+
"c2JCzYkpUniv8Q1HR2oXszaO4H0WxKgF2TCsSGgSi/jjdW65FNXM37pXQF0L8h/PevvhH5NIah+v"
|
|
126
|
+
"dkkZaky/BSG1IBuGlRMrc7YzqwGYQrFXQ2ULcslZkBOTaEOaaRbWS8tgQRq1IBuKumE7v0KxsZga"
|
|
127
|
+
"hxSiHagTtqtvQZ70FmQaiB0yjBMkpfYWxKgF2VBUjgnnCRb/HHxJahhWRH0LcmKKEK9uJhMy08al"
|
|
128
|
+
"0Bu1IDcBKhA6KHG+wzcQw3r+qIy6FuSJSz5mPaHmxATJW6sFuQkwQNaOslXZ7S766AFQ3YLc++Tl"
|
|
129
|
+
"GNaL1iMsJ2pBNhQVCU0kFMc/idtt+jEH/dEqW5BnnAXx3rpJ5A72Qy3IZqF2KTAqVsm7j8EVWiKQ"
|
|
130
|
+
"+u5P7K38XafveeISALzwp7/8c6AZsW/fsiUitK3rfkfE3PrBA3CDB6xlAiyHgQMCYlAufO88AkgH"
|
|
131
|
+
"5VbAMI1CKrY9qZP9WA+qp807FmSS+oRIC0JqQY4LQ/R88w3BuOmNiBDrNWJ4CzKZpERMGB1T24Io"
|
|
132
|
+
"sY+KgaqPzldqrJvRWKcFIWdBLBGbxSwIqGf8IslBuURqQQ7HkH2T0+0zhaWl9Kwdm2xBetRaLcgS"
|
|
133
|
+
"GJDQKdxx8L71YqMtSB6z9kcRFkRj1odiAMshIZMr2AQ+A9hgC6IlFFbGmoZDldG8zcCwFmQ6z4I0"
|
|
134
|
+
"cyyIK8hzaAkFqAU5AGsc30dRozcJx2BBjLAgjbAgvr+4iNerBVkYA1uOEuuNQx8Vw1kQ6rMgRJZa"
|
|
135
|
+
"b0GsK6FAWsWpEtY6AnvTMYgFkQVvfO+92NdaqzhVhxK6QHULIksohH4gzVGrOFHnoT33DsaaLcfN"
|
|
136
|
+
"gcEtiCViBAtCfVWcAD/zka9njSNUcVILAlXoAzFICQVTWpBJgxSv7liQWMXpaIkYd7ARWxAl9CGo"
|
|
137
|
+
"XULBR0FyCxKiIKmKkyFRlqxIxJRRkH5SR4yL1Go5joDBLAjKKAiCBQEb+CgIwBaAi4VYlkkYIFgQ"
|
|
138
|
+
"ZmIXDwlFJElOUzce+6EKvQDqW5BOPZBJqrPn+oH4uiByFoR8mBd67Qcg+9K4lVEotRJ6QdQelHvm"
|
|
139
|
+
"w19/jprp1A/GFX56YsgkP51XcCIqrUc/qUtPvf1QQi+B2uMX3aDciR9lHhqJvnKTSRnFVDA+xqqj"
|
|
140
|
+
"Qh/aUAQwBpVWQq+AunVBnvo2ykZiHFneITX5GcaKcmtIpC5leiTWQwm9Iuoq9dcun7n/6Z1QkN1X"
|
|
141
|
+
"QhU+OpZL8DMixJnGQtmIRN+s68xWcziDEroCqluQ+7+xE5Q5VkPt+GjfjzqyWSp19M55NnEEUEJX"
|
|
142
|
+
"RF1SP71DSaVD1KMR8WlKE5qaLOKB6EOyDo7lOM+thBK6MmqT+uwD37qa+nu4KT3IhBAeBesh/bT7"
|
|
143
|
+
"cNLmPOqx5aRWQg+A2hbk7O/85wtuygxP6uijTayFAGmkc7Uei9sAoIQeFFVJ/eClayT6d6RZebNE"
|
|
144
|
+
"C7KeeOn5gNT4dkFT3wOjZtr87IPPXgeAK4+ev5XJMoGYXZiDmVMcml1HPM9gHkfO20MVek2oqdZ3"
|
|
145
|
+
"/N7Od8/9/tXvd3rgSZWOXjqEp3NXHV/cMiih14iapAaAcw+98MNYVSyPcABxRIsc97b9jUMl9JpR"
|
|
146
|
+
"u8F47qHrPxIKDUjPXCQMO8zdQlIroY8JNUl9/uGXfhzZm4IbiaS0hcydAyX0MaIWqXceOXVLsh0A"
|
|
147
|
+
"ksXI+bvtbIYS+thRw4Kc/+j1/z7/8Ms/EYNk0ekXnfcl3drwnRJ6Q1BHrbeTpItACb1BWFmp/+C/"
|
|
148
|
+
"/kdsFoo8DiihNwyrWxA6YHP7ea2ZwgVRI+M3LMaUF+xCFXoBbD6ZM7B4znZtM5TQiq2CEnrrwfFp"
|
|
149
|
+
"DFBCbxG+88nXvdIVlGEWHC7IvN3cPgZCb/cFPWZw4Zk5e20EWDOhmTGSC7tu7Hzy9T/jLy+H/+OL"
|
|
150
|
+
"gddiz7b+DGsitL/QYVVRFTufesOrOLcaoq7dfMneRgwch45EdnoxrvbJ4Nh55OSrwS2DOXsIcos7"
|
|
151
|
+
"4jiu+4CE7mOvMroGnv/06dcwtxZsGeyeMgJLHRb3Re79OYBtEu6BCC2Imyvz1nq3oXHl0fO3Mrct"
|
|
152
|
+
"rLXM1pPZsuOztVGd2RObpQXJPHXxG2wPmYFBCC1uc3nkKPi6tV/Ao2b4DutDcfnjr1gqW/i9r/z2"
|
|
153
|
+
"e7nd2+d2f5/bG/vc3phxu78Puz/jdn/Gdtaync0Ql61l27bMsxbWWnDbMrfWrUcye2JnVgMZX3mO"
|
|
154
|
+
"gmyhMgdUbhR2bnVIBOdCKIbH3Z/YW4iAR3nvUh2HbGth25btrIWdtbBtG7bZti3btvVk9tszT+a2"
|
|
155
|
+
"ZW5bto7MzK11VqO1zOzJ7EjNsMlHI1dnPw8LMIff24TaUY4+ZUZgMstox4BYlMjlZ2vj9b/+J3/L"
|
|
156
|
+
"bB0prQ3qa2HbFuzJzG0guoVfd6ocCNxGdRaWwwKCzNJLs2yvbDeJJSoSurx2SZkdk6W3Gw7r6EC0"
|
|
157
|
+
"lEqzZXh1RSA3W8u2tcy2hW2jAnOwFs5uBM8syMyJzFKVmaXJ8Jd9nnfeTtRUaJ6nzEh85qEUehVV"
|
|
158
|
+
"Xgdufe8X/4EjQkMuLFsWpHUkto74XTKnRqD/nrwR6MUjkXn7IxsSFQhd2ojcM6fXh1Po2kQezEuD"
|
|
159
|
+
"GbAMWO/AJEETeTkncKHM3LUZmTJzce3dgXOB3k4yA8M2CoMyF4F/W+1ibroql3jj+/7mX8HhP5EA"
|
|
160
|
+
"kZEKjkQNcWbOGoDI3iPJHBuC3EfmkaAGoYuGYNhI8hxVmq37cSpgaCIPp9KAvCzcR+zgjSN5rVf2"
|
|
161
|
+
"XjJnNmMumUegzsAgUY4Q0QAn9xEi/uGWuxpuJlUucfL9X3kije3rOLWCqIVVm9sA7PXMoyMzsDKh"
|
|
162
|
+
"WahzvHipEdj7oyxP6E20GMuo9Knf+qenitoZBeL1y3ZySVCOF/UIZB4HKkc5sh8hWg0ORGZrl7Uc"
|
|
163
|
+
"x0HkQY+ZzWAFzCG3VGaU4hBuhEXiZA6Zt1+dgWqEliSGuB32NXjsQoTeRFUusYxK3/7Bf3k6q4Hb"
|
|
164
|
+
"U0tRIrJU3g3dqlBmQeTMZoyDzEC1sF1YC1cyqIlv8yCLoR754g5B5B/840PvX+T961PpUPX2ABvi"
|
|
165
|
+
"ltHLdVrifS5lZFiB0CwvYBKQtJ03YHwMlRdU6Jr43lfuf6+d7e19/+8/8r7a372USn/o376ZzQRL"
|
|
166
|
+
"afLMo9eEKdTZrY9KlSVq9LZjwM2D4H4EZjATyEkGeQNNsMalc9u1E/q7X/7Au90EO36OEtM06z6H"
|
|
167
|
+
"uaBUhT9MAMREAOczabplxxv3sHmcRA6o66FZ3BZjiMnHUJ06L+yhV8XLf/Ub7+TZ3h7P/u+Gne3d"
|
|
168
|
+
"4HbvBs/2bizyHbW6n/bh9D1PXJKkzubsTlNJ9Ot1T1Bj7KjfOUkEnyWxWfbhXQNe/NJ73vHil97z"
|
|
169
|
+
"DtsmEvvlvm339r/75Q+8e4jjLkNqIhKT0RvqeOswJb1/+3w7Mm51BpYidAqHAvB5XL8a8lQpqpR6"
|
|
170
|
+
"g4l+C88/eu4NdU6/H9f/8td+yRN4n2eOxNze2PfLG/6xv8h3Dtk4PH3vk8+KqdmKGWHD5Gxid8D2"
|
|
171
|
+
"115cGEsqdCB1bAIKaYgKjdQ48X0S4Elth1Ppa3/xq291hN3b55kjriBx2N7n9sb+y3/9m+8a4hyW"
|
|
172
|
+
"SonHOQezWWEFwaX1SGERv13pzG9+VByC5TlMFNZTYgXMBMtgYoa1CzThj4wX/uztFzIfGarZE4k5"
|
|
173
|
+
"sRsDtpZ44u8cm+M8z9z31Lev/vHPnwMMsfDRDL/q2tm+5d1z2gTfkBy37RggsRIag9J/2KDOvntk"
|
|
174
|
+
"a5//9OnX1Dk2cPXzb7uTZ0l5ex6z4rHvxvLtzxY5ziK2YzmVDopc+migUGqoKvejZm87kcnyKylV"
|
|
175
|
+
"KzukR1KveuDdx996dvfxt5x15Lyxnz08Ybndn7nBqX5Qqt8XBqi++KX3vGPV86iFM/c/vRMnoxeV"
|
|
176
|
+
"9+NkQMW0KVTuUNRIrIieMrGJWHSDFA1DSeqdR07esuzRdy9eOMX2hiesJ3DxQCCuTQRGO5uxnaV9"
|
|
177
|
+
"drZZKi0noCefPRQWmsJchH1cjpMGjRe1PLRLrqR0oZ9yGgC5FiPBsvv7sZYBQ4AFmHf+6I2vdkkP"
|
|
178
|
+
"onMPXfvRvANceezO24ojyiyDCMSmmc2iFzXGMAzDiM49/r1ERC9+8d1vP/n+v/tqnUuxIrIJNP06"
|
|
179
|
+
"IzE6XecDLfVYUYnQ/vo65sKx2KuI28cMMLFFIjUT2BgitgATM9HOIydvIRCd++j1SOznP3PmtZnq"
|
|
180
|
+
"ZD8gJ1KHn1gQ2iubARsLMobADUcOSI9qBlO1ZWp5EIg42gpQh6+uAQjPZ3L/em0QAit7L+nt0vx3"
|
|
181
|
+
"KQ8gwk4iUeCSCATAmJgZS0kEkTFLp1j22uEyPRZT7yK05b7KuDtAY0CmITNpYCYNmUlDzXQKM52Y"
|
|
182
|
+
"5sT01Af/+RuL/MsXsROLEnr34oXbud3fZ7vfws5m7OtzpJIGceR3snUAOuHUEWKITGHoCoasYSgH"
|
|
183
|
+
"eGZFUnwj0VpZhyIWY4Gdtb4uRRsfRcEW956ZhfW1LmIhF1eByFUimvnPz0RRl1kLdtWKrn/hXW+r"
|
|
184
|
+
"dy1yLOqlzz74zLX5s7GN2iIfihUtB7tbXbzdR+/hXyQQOHVc4uRL3GvEIv1FADHH7xM2g/PIdSdV"
|
|
185
|
+
"WQagKUi9u0swmIk5/vGyIRATwZqWyRAwc3HqBXD3J/aqztctsXvxwu0H+2I1zvNQoz+0CNdlRBNP"
|
|
186
|
+
"PePkfGclzofri0IsbZvVqRCP2GvPVR6yKD8f6sH5SkWpwKG/XYfacUHpffWi61945y+sfj36sQj5"
|
|
187
|
+
"zz74zDW3No+0SuZ5GKDguUh9+4R3TINn9iMUSAlVNCPBU0FC6y2IJ6V7BFviKw+xFWRPxPWVijhV"
|
|
188
|
+
"LArkdvbFVzEKteQSwRfAsCNp8r4FMjyqmI9aUY7Cevh4nW+KMxPge/lK8yFSMeTb6t6OsLinytvr"
|
|
189
|
+
"gT9mOB7YH5tcdJBY+BdmIrIEhm0BS0StddZjRrCL2Y5FsAT5g5Uqb3uKA1CzL8ccUntPzcRMHANO"
|
|
190
|
+
"ediY2XdoR/oRQ7Ks50ec35L3n3PcZrLefxv4/iQW7EJixC2BybKlFmSIrCHQzFz781958+0f+vdv"
|
|
191
|
+
"1boqS6t46GvCJakVB6FyfejDSQ0ATIK9IdgWgtgkvuuwn7F8vfvHEXI4Bm6kgSEm686RLHNriQ3B"
|
|
192
|
+
"tpZhWsQ+yUdHX+NwVSty5bG7bktp2BAzSt4tb58oJAaIARVJkHyjzNPGJC51P7D0CYhIhw92GEOu"
|
|
193
|
+
"w4+BaQz5YVhEjYtJN5OGzGRCZjpBM52QmU5O3/PEpRonswyuPHrnraHBGsOUoTELa2GZAR+LhghD"
|
|
194
|
+
"axx6sAr+iEoNlGrt98Qkn/fN4T04tNcNx88VkH8u3rcjdPkjC4KxYOsP78OF3BIsESNYj7IX/Xpx"
|
|
195
|
+
"5bN3n4xlv1LPRaSl2+g2K8ZLYokhJw3iSIxI43T1kXG2+C0EWRf6lTgchInJPTlSc/wbcBuWicky"
|
|
196
|
+
"iIiJmC2RI3bLnWEha4YrxsPo1LBDMNTZJdmgLt0bgYHnKSxUg8sN4ROlRzzyg7n7+RgiRHjqvq8Y"
|
|
197
|
+
"42hbl6m0rYWfFuLq599216CXZh5i2NJlVUUX3HhLE2/urIwdA89TCPSTeo4CdrvhzPnKOftJREjc"
|
|
198
|
+
"cWPsMPkUy+6WQOyGhoVGYgswWuaQRTzaqdTElcfuui1UG+U4bM03Cv3foXsns3K4H2sgdB8kyWWK"
|
|
199
|
+
"e9WvRYp0OA/P3kcnjhN8nxLrXT3BWQ+ALRGhdX56jdi9eOF2DnOqhPp/he1I3WNEC7D0GyNvEALH"
|
|
200
|
+
"RmiJmj9A1hANwWif1+GYo2Hi0KOVmS0TyLIFyLTENqVydi++6XaYpjn7wDd3651jFzFzWU7bltRZ"
|
|
201
|
+
"WCf0XDIV7IANIHRNiIZolOvCZ7oOUiHRwgBs7FkVOM8AWYAN8x0PXLo25Blf+ezPvjGl4+WIntA7"
|
|
202
|
+
"0Xp/ESxHCErH9qGg8rjVGdjavoiyNlzqZ+2i0yZVJyJj0ohwQ2SMATVNKBlGxvWhvuMjz71U+wyv"
|
|
203
|
+
"XLxwSvRvjrNgxXhzNr+KIzZEwROECq9ZAkoJvaWEBhKpxUABigMJ4kiVWLHIkdoQGQPTEJFp3H5P"
|
|
204
|
+
"cNOYO3738ournNHu5958Jp+mLU3hJuYhlFMf2zgFhZ9fpYjeQAmdY7sJDSCROg6mkYNPZRaRIqE9"
|
|
205
|
+
"wR2RfT2PWOzR/RGcffDZ60c9k93Pvem0S+iV0xpHAnP/umwYhqgHhDrHvh5QMjtsMaEBodL+qUNq"
|
|
206
|
+
"EyyI2yfsh1DunOSyboaZq9pXLl44FX1vTJAUE2fKuQdhLdtgLcQ8hLG7bWodZuE7VecM209oABmp"
|
|
207
|
+
"Y80L8rWYU1GXkqxSud3rYQyk+Ez2XbGTqkjhhTF/Yi5ulvMUynolro9GLP2Qz0VYWI3QUAz/ViU0"
|
|
208
|
+
"sHVRjhI9UQ9m5lAcwMXzYvyWYYhgjSemG5kOQwRmMBl2I8SIYOOAX1cqQZA5D6yI2PFBxE4eOdoM"
|
|
209
|
+
"mSVMmXvBYCVzH7ac0B4x4eLXGMxpwIEgoQWDbBgY4LMxxEwEMuzGR/p6H+zFmYmEUU+x7kg3EW6L"
|
|
210
|
+
"EbcsUtElt4xixMhzDNNBrihybLnlCOgvtxArLkjLIKIgVGzn+0GxsRmOIa9mUNW44Q1DQeyk3N39"
|
|
211
|
+
"6c4RLIxajcMwEkIDh5MaKfohQ34lsd1u0SMvkNt/BVIGxy8KIkbljS1GkSsRoTjP5j4yi69XQucY"
|
|
212
|
+
"EaGBIuHinxIhuw28EA2RjUmkfdmXUX41O4SLigt4z5HHkXNVdu9UMi+K8REaQC+pg4noJW2RpBGv"
|
|
213
|
+
"iabgPKTYWt6ok4rsXy1VGVAyL4aRERqYT2q/oELGY2dAaTf6PnsgpOntiSMn0oa4SN4AVDIfFSMk"
|
|
214
|
+
"NHAwqf0qlaY7cxg9163cJxuFxT7OX+9XcBklyb5DyXwARkpoICPgwcQWb6biE0cxHNmK2OLOSv6p"
|
|
215
|
+
"3t1K5kMwYkID/aSOG8W1Kcnd85a54Hytn8R+ByN/M6BEPjpGTmjgcH4e5JMXuXy9nOS+hXhZibwg"
|
|
216
|
+
"lNAdFF64q9y9G4egT3YP2KVEXhZK6LnoafjVulr9Yq0krgAl9JFRu1aHElihUCgUCoVCoVAoFAqF"
|
|
217
|
+
"QqFQKBQKhUKhUCgUCoVCoVAoFAqFQqFQKBQKhUKhUCgUa8X/A+26fqLnlvojAAAAAElFTkSuQmCC"
|
|
218
|
+
|
|
219
|
+
private let jianPointerImage: NSImage? = {
|
|
220
|
+
guard let data = Data(base64Encoded: jianPointerPngBase64, options: .ignoreUnknownCharacters)
|
|
221
|
+
else {
|
|
222
|
+
fputs("computer-helper: embedded pointer PNG base64 decode failed\n", stderr)
|
|
223
|
+
fflush(stderr)
|
|
224
|
+
return nil
|
|
225
|
+
}
|
|
226
|
+
return NSImage(data: data)
|
|
227
|
+
}()
|
|
86
228
|
|
|
87
229
|
// MARK: - Overlay view
|
|
88
230
|
|
|
@@ -98,11 +240,11 @@ private final class JianPointerView: NSView {
|
|
|
98
240
|
guard let ctx = NSGraphicsContext.current?.cgContext else { return }
|
|
99
241
|
|
|
100
242
|
if rippleVisible {
|
|
101
|
-
let maxRadius = pointerSize * 0.
|
|
102
|
-
let radius =
|
|
243
|
+
let maxRadius = pointerSize * 0.55
|
|
244
|
+
let radius = 3 + maxRadius * rippleProgress
|
|
103
245
|
let alpha = max(0, 1 - rippleProgress)
|
|
104
246
|
ctx.saveGState()
|
|
105
|
-
ctx.setStrokeColor(
|
|
247
|
+
ctx.setStrokeColor(jianGlow.withAlphaComponent(0.45 * alpha).cgColor)
|
|
106
248
|
ctx.setLineWidth(1.5)
|
|
107
249
|
ctx.strokeEllipse(in: CGRect(
|
|
108
250
|
x: hotspot.x - radius,
|
|
@@ -113,57 +255,28 @@ private final class JianPointerView: NSView {
|
|
|
113
255
|
ctx.restoreGState()
|
|
114
256
|
}
|
|
115
257
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
// Tip near top-left (hotspot), body sweeping down-right then curling back — one ascending wing.
|
|
136
|
-
path.move(to: CGPoint(x: s * 0.12, y: s * 0.10))
|
|
137
|
-
path.curve(
|
|
138
|
-
to: CGPoint(x: s * 0.72, y: s * 0.18),
|
|
139
|
-
controlPoint1: CGPoint(x: s * 0.28, y: s * 0.02),
|
|
140
|
-
controlPoint2: CGPoint(x: s * 0.52, y: s * 0.06)
|
|
141
|
-
)
|
|
142
|
-
path.curve(
|
|
143
|
-
to: CGPoint(x: s * 0.88, y: s * 0.55),
|
|
144
|
-
controlPoint1: CGPoint(x: s * 0.88, y: s * 0.28),
|
|
145
|
-
controlPoint2: CGPoint(x: s * 0.94, y: s * 0.42)
|
|
146
|
-
)
|
|
147
|
-
path.curve(
|
|
148
|
-
to: CGPoint(x: s * 0.42, y: s * 0.88),
|
|
149
|
-
controlPoint1: CGPoint(x: s * 0.80, y: s * 0.72),
|
|
150
|
-
controlPoint2: CGPoint(x: s * 0.58, y: s * 0.90)
|
|
151
|
-
)
|
|
152
|
-
path.curve(
|
|
153
|
-
to: CGPoint(x: s * 0.18, y: s * 0.48),
|
|
154
|
-
controlPoint1: CGPoint(x: s * 0.26, y: s * 0.86),
|
|
155
|
-
controlPoint2: CGPoint(x: s * 0.12, y: s * 0.68)
|
|
156
|
-
)
|
|
157
|
-
path.curve(
|
|
158
|
-
to: CGPoint(x: s * 0.12, y: s * 0.10),
|
|
159
|
-
controlPoint1: CGPoint(x: s * 0.22, y: s * 0.32),
|
|
160
|
-
controlPoint2: CGPoint(x: s * 0.08, y: s * 0.20)
|
|
161
|
-
)
|
|
162
|
-
path.close()
|
|
163
|
-
return path
|
|
258
|
+
// Bitmap blade (white + #2880F8 stroke + baked glow). Pulse alpha on click flash.
|
|
259
|
+
let alpha = 1 - fillPulse * 0.28
|
|
260
|
+
if let image = jianPointerImage {
|
|
261
|
+
image.draw(
|
|
262
|
+
in: bounds,
|
|
263
|
+
from: .zero,
|
|
264
|
+
operation: .sourceOver,
|
|
265
|
+
fraction: alpha,
|
|
266
|
+
respectFlipped: true,
|
|
267
|
+
hints: nil
|
|
268
|
+
)
|
|
269
|
+
} else {
|
|
270
|
+
// Last-resort fallback if decode failed — still a visible tip, not ink/cream geometry.
|
|
271
|
+
jianGlow.withAlphaComponent(0.9 * alpha).setFill()
|
|
272
|
+
let tip = NSBezierPath(
|
|
273
|
+
ovalIn: CGRect(x: hotspot.x - 3, y: hotspot.y - 3, width: 6, height: 6)
|
|
274
|
+
)
|
|
275
|
+
tip.fill()
|
|
276
|
+
}
|
|
164
277
|
}
|
|
165
278
|
|
|
166
|
-
func flashClick(duration: TimeInterval = 0.
|
|
279
|
+
func flashClick(duration: TimeInterval = 0.16) {
|
|
167
280
|
rippleTimer?.invalidate()
|
|
168
281
|
rippleVisible = true
|
|
169
282
|
rippleProgress = 0
|
|
@@ -207,6 +320,7 @@ private final class OverlayController {
|
|
|
207
320
|
private var pointerView: JianPointerView?
|
|
208
321
|
/// Current tip/hotspot in global top-left Quartz coordinates.
|
|
209
322
|
private var tipPoint = CGPoint.zero
|
|
323
|
+
private var hasTip = false
|
|
210
324
|
|
|
211
325
|
private init() {}
|
|
212
326
|
|
|
@@ -241,6 +355,10 @@ private final class OverlayController {
|
|
|
241
355
|
ensureWindow()
|
|
242
356
|
guard let window else { return }
|
|
243
357
|
window.alphaValue = sessionAlpha
|
|
358
|
+
if !hasTip {
|
|
359
|
+
tipPoint = quartzMouseLocation()
|
|
360
|
+
hasTip = true
|
|
361
|
+
}
|
|
244
362
|
positionWindow()
|
|
245
363
|
window.orderFrontRegardless()
|
|
246
364
|
}
|
|
@@ -257,10 +375,12 @@ private final class OverlayController {
|
|
|
257
375
|
guard let window else { return }
|
|
258
376
|
window.orderOut(nil)
|
|
259
377
|
window.alphaValue = 0
|
|
378
|
+
hasTip = false
|
|
260
379
|
}
|
|
261
380
|
|
|
262
381
|
func moveTip(to globalTopLeft: CGPoint) {
|
|
263
382
|
tipPoint = globalTopLeft
|
|
383
|
+
hasTip = true
|
|
264
384
|
ensureWindow()
|
|
265
385
|
positionWindow()
|
|
266
386
|
}
|
|
@@ -277,7 +397,6 @@ private final class OverlayController {
|
|
|
277
397
|
// Quartz global: origin top-left of primary, Y down. Cocoa: origin bottom-left, Y up.
|
|
278
398
|
let mainHeight = NSScreen.screens.first?.frame.height ?? NSScreen.main?.frame.height ?? 0
|
|
279
399
|
let tipCocoaY = mainHeight - tipPoint.y
|
|
280
|
-
// Window origin is bottom-left; hotspot is hotspot.y down from the top of the view.
|
|
281
400
|
let origin = NSPoint(
|
|
282
401
|
x: tipPoint.x - hotspot.x,
|
|
283
402
|
y: tipCocoaY - (pointerSize - hotspot.y)
|
|
@@ -286,105 +405,175 @@ private final class OverlayController {
|
|
|
286
405
|
}
|
|
287
406
|
}
|
|
288
407
|
|
|
289
|
-
// MARK: -
|
|
408
|
+
// MARK: - Cursor location / preserve
|
|
290
409
|
|
|
291
|
-
|
|
292
|
-
|
|
410
|
+
/// Quartz global top-left coordinates for the live system cursor.
|
|
411
|
+
private func quartzMouseLocation() -> CGPoint {
|
|
412
|
+
let cocoa = NSEvent.mouseLocation
|
|
413
|
+
let mainHeight = NSScreen.screens.first?.frame.height ?? NSScreen.main?.frame.height ?? 0
|
|
414
|
+
return CGPoint(x: cocoa.x, y: mainHeight - cocoa.y)
|
|
293
415
|
}
|
|
294
416
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
private func postMouseMove(to point: CGPoint) {
|
|
298
|
-
OverlayController.shared.moveTip(to: point)
|
|
417
|
+
/// Warp HID cursor without moving the Jian overlay tip.
|
|
418
|
+
private func warpSystemCursor(to point: CGPoint) {
|
|
299
419
|
guard let event = CGEvent(
|
|
300
420
|
mouseEventSource: nil,
|
|
301
421
|
mouseType: .mouseMoved,
|
|
302
422
|
mouseCursorPosition: point,
|
|
303
423
|
mouseButton: .left
|
|
304
424
|
) else {
|
|
305
|
-
logError("could not create mouseMoved event")
|
|
425
|
+
logError("could not create mouseMoved event for warp")
|
|
306
426
|
return
|
|
307
427
|
}
|
|
308
428
|
event.post(tap: .cghidEventTap)
|
|
309
429
|
}
|
|
310
430
|
|
|
431
|
+
/**
|
|
432
|
+
Save → hide → act → restore → show.
|
|
433
|
+
|
|
434
|
+
HID clicks must be posted at the target (macOS delivers them at the event location),
|
|
435
|
+
but we restore immediately so Lucas’s physical mouse is not left at the Jian point.
|
|
436
|
+
*/
|
|
437
|
+
private func withPreservedUserCursor(_ body: () -> Void) {
|
|
438
|
+
let saved = quartzMouseLocation()
|
|
439
|
+
CGDisplayHideCursor(CGMainDisplayID())
|
|
440
|
+
body()
|
|
441
|
+
warpSystemCursor(to: saved)
|
|
442
|
+
// Tiny yield so the restore event lands before unhide.
|
|
443
|
+
Thread.sleep(forTimeInterval: 0.008)
|
|
444
|
+
CGDisplayShowCursor(CGMainDisplayID())
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// MARK: - Smooth overlay motion
|
|
448
|
+
|
|
449
|
+
private func easeOutCubic(_ t: Double) -> Double {
|
|
450
|
+
let x = min(1, max(0, t))
|
|
451
|
+
return 1 - pow(1 - x, 3)
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
private func animateOverlay(to target: CGPoint) {
|
|
455
|
+
OverlayController.shared.ensureSession()
|
|
456
|
+
let start = OverlayController.shared.currentTip()
|
|
457
|
+
let distance = hypot(target.x - start.x, target.y - start.y)
|
|
458
|
+
if distance < 1.5 {
|
|
459
|
+
OverlayController.shared.moveTip(to: target)
|
|
460
|
+
return
|
|
461
|
+
}
|
|
462
|
+
// ~0.25 ms/px, clamped — snappy for agent loops.
|
|
463
|
+
let duration = min(0.11, max(0.028, distance * 0.00025))
|
|
464
|
+
let steps = max(1, Int(ceil(duration * 60)))
|
|
465
|
+
let stepSleep = duration / Double(steps)
|
|
466
|
+
for i in 1...steps {
|
|
467
|
+
let t = easeOutCubic(Double(i) / Double(steps))
|
|
468
|
+
let p = CGPoint(
|
|
469
|
+
x: start.x + (target.x - start.x) * t,
|
|
470
|
+
y: start.y + (target.y - start.y) * t
|
|
471
|
+
)
|
|
472
|
+
OverlayController.shared.moveTip(to: p)
|
|
473
|
+
if i < steps {
|
|
474
|
+
RunLoop.current.run(until: Date(timeIntervalSinceNow: stepSleep))
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
OverlayController.shared.moveTip(to: target)
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// MARK: - Mouse / keyboard
|
|
481
|
+
|
|
482
|
+
private func postOverlayMove(to point: CGPoint) {
|
|
483
|
+
animateOverlay(to: point)
|
|
484
|
+
}
|
|
485
|
+
|
|
311
486
|
private func postMouseClick(button: CGMouseButton, count: Int64) {
|
|
312
487
|
OverlayController.shared.flashClick()
|
|
313
488
|
let point = OverlayController.shared.currentTip()
|
|
314
489
|
let downType: CGEventType = button == .right ? .rightMouseDown : .leftMouseDown
|
|
315
490
|
let upType: CGEventType = button == .right ? .rightMouseUp : .leftMouseUp
|
|
316
491
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
492
|
+
withPreservedUserCursor {
|
|
493
|
+
warpSystemCursor(to: point)
|
|
494
|
+
for clickIndex in 1...max(1, count) {
|
|
495
|
+
if let down = CGEvent(
|
|
496
|
+
mouseEventSource: nil,
|
|
497
|
+
mouseType: downType,
|
|
498
|
+
mouseCursorPosition: point,
|
|
499
|
+
mouseButton: button
|
|
500
|
+
) {
|
|
501
|
+
down.setIntegerValueField(.mouseEventClickState, value: clickIndex)
|
|
502
|
+
down.post(tap: .cghidEventTap)
|
|
503
|
+
}
|
|
504
|
+
if let up = CGEvent(
|
|
505
|
+
mouseEventSource: nil,
|
|
506
|
+
mouseType: upType,
|
|
507
|
+
mouseCursorPosition: point,
|
|
508
|
+
mouseButton: button
|
|
509
|
+
) {
|
|
510
|
+
up.setIntegerValueField(.mouseEventClickState, value: clickIndex)
|
|
511
|
+
up.post(tap: .cghidEventTap)
|
|
512
|
+
}
|
|
513
|
+
if clickIndex < count {
|
|
514
|
+
Thread.sleep(forTimeInterval: 0.04)
|
|
515
|
+
}
|
|
338
516
|
}
|
|
339
517
|
}
|
|
340
518
|
}
|
|
341
519
|
|
|
520
|
+
private func postClickAt(point: CGPoint, button: CGMouseButton, count: Int64) {
|
|
521
|
+
animateOverlay(to: point)
|
|
522
|
+
postMouseClick(button: button, count: count)
|
|
523
|
+
}
|
|
524
|
+
|
|
342
525
|
private func postDrag(from: CGPoint, to: CGPoint) {
|
|
343
|
-
|
|
526
|
+
OverlayController.shared.ensureSession()
|
|
527
|
+
animateOverlay(to: from)
|
|
344
528
|
OverlayController.shared.flashClick()
|
|
345
|
-
Thread.sleep(forTimeInterval: 0.02)
|
|
346
529
|
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
mouseCursorPosition: from,
|
|
351
|
-
mouseButton: .left
|
|
352
|
-
) {
|
|
353
|
-
down.post(tap: .cghidEventTap)
|
|
354
|
-
}
|
|
530
|
+
withPreservedUserCursor {
|
|
531
|
+
warpSystemCursor(to: from)
|
|
532
|
+
Thread.sleep(forTimeInterval: 0.012)
|
|
355
533
|
|
|
356
|
-
|
|
357
|
-
for i in 1...steps {
|
|
358
|
-
let t = CGFloat(i) / CGFloat(steps)
|
|
359
|
-
let p = CGPoint(
|
|
360
|
-
x: from.x + (to.x - from.x) * t,
|
|
361
|
-
y: from.y + (to.y - from.y) * t
|
|
362
|
-
)
|
|
363
|
-
OverlayController.shared.moveTip(to: p)
|
|
364
|
-
if let drag = CGEvent(
|
|
534
|
+
if let down = CGEvent(
|
|
365
535
|
mouseEventSource: nil,
|
|
366
|
-
mouseType: .
|
|
367
|
-
mouseCursorPosition:
|
|
536
|
+
mouseType: .leftMouseDown,
|
|
537
|
+
mouseCursorPosition: from,
|
|
368
538
|
mouseButton: .left
|
|
369
539
|
) {
|
|
370
|
-
|
|
540
|
+
down.post(tap: .cghidEventTap)
|
|
371
541
|
}
|
|
372
|
-
Thread.sleep(forTimeInterval: 0.008)
|
|
373
|
-
}
|
|
374
542
|
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
543
|
+
let distance = hypot(to.x - from.x, to.y - from.y)
|
|
544
|
+
let steps = max(6, min(14, Int(distance / 40) + 6))
|
|
545
|
+
for i in 1...steps {
|
|
546
|
+
let t = easeOutCubic(Double(i) / Double(steps))
|
|
547
|
+
let p = CGPoint(
|
|
548
|
+
x: from.x + (to.x - from.x) * t,
|
|
549
|
+
y: from.y + (to.y - from.y) * t
|
|
550
|
+
)
|
|
551
|
+
OverlayController.shared.moveTip(to: p)
|
|
552
|
+
if let drag = CGEvent(
|
|
553
|
+
mouseEventSource: nil,
|
|
554
|
+
mouseType: .leftMouseDragged,
|
|
555
|
+
mouseCursorPosition: p,
|
|
556
|
+
mouseButton: .left
|
|
557
|
+
) {
|
|
558
|
+
drag.post(tap: .cghidEventTap)
|
|
559
|
+
}
|
|
560
|
+
Thread.sleep(forTimeInterval: 0.006)
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
if let up = CGEvent(
|
|
564
|
+
mouseEventSource: nil,
|
|
565
|
+
mouseType: .leftMouseUp,
|
|
566
|
+
mouseCursorPosition: to,
|
|
567
|
+
mouseButton: .left
|
|
568
|
+
) {
|
|
569
|
+
up.post(tap: .cghidEventTap)
|
|
570
|
+
}
|
|
382
571
|
}
|
|
383
572
|
OverlayController.shared.moveTip(to: to)
|
|
384
573
|
}
|
|
385
574
|
|
|
386
575
|
private func postScroll(at point: CGPoint, direction: String, amount: Int) {
|
|
387
|
-
|
|
576
|
+
animateOverlay(to: point)
|
|
388
577
|
let n = max(1, amount)
|
|
389
578
|
var dx = 0
|
|
390
579
|
var dy = 0
|
|
@@ -397,18 +586,21 @@ private func postScroll(at point: CGPoint, direction: String, amount: Int) {
|
|
|
397
586
|
logError("unknown scroll direction: \(direction)")
|
|
398
587
|
return
|
|
399
588
|
}
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
589
|
+
withPreservedUserCursor {
|
|
590
|
+
warpSystemCursor(to: point)
|
|
591
|
+
for _ in 0..<n {
|
|
592
|
+
if let event = CGEvent(
|
|
593
|
+
scrollWheelEvent2Source: nil,
|
|
594
|
+
units: .line,
|
|
595
|
+
wheelCount: 2,
|
|
596
|
+
wheel1: Int32(dy),
|
|
597
|
+
wheel2: Int32(dx),
|
|
598
|
+
wheel3: 0
|
|
599
|
+
) {
|
|
600
|
+
event.post(tap: .cghidEventTap)
|
|
601
|
+
}
|
|
602
|
+
Thread.sleep(forTimeInterval: 0.012)
|
|
410
603
|
}
|
|
411
|
-
Thread.sleep(forTimeInterval: 0.02)
|
|
412
604
|
}
|
|
413
605
|
}
|
|
414
606
|
|
|
@@ -480,7 +672,6 @@ private func postUnicodeChar(_ scalar: UniChar) {
|
|
|
480
672
|
}
|
|
481
673
|
|
|
482
674
|
private func typeText(_ text: String) {
|
|
483
|
-
// Iterate UTF-16 code units; map Return/Tab to virtual keys when present as characters.
|
|
484
675
|
let utf16 = Array(text.utf16)
|
|
485
676
|
var i = 0
|
|
486
677
|
while i < utf16.count {
|
|
@@ -496,7 +687,7 @@ private func typeText(_ text: String) {
|
|
|
496
687
|
postUnicodeChar(unit)
|
|
497
688
|
}
|
|
498
689
|
i += 1
|
|
499
|
-
Thread.sleep(forTimeInterval: 0.
|
|
690
|
+
Thread.sleep(forTimeInterval: 0.002)
|
|
500
691
|
}
|
|
501
692
|
}
|
|
502
693
|
|
|
@@ -547,7 +738,6 @@ private func parseAndPostKey(_ keySpec: String) -> Bool {
|
|
|
547
738
|
}
|
|
548
739
|
}
|
|
549
740
|
|
|
550
|
-
// Function keys F1–F12
|
|
551
741
|
if token.hasPrefix("f"), let n = Int(token.dropFirst()), (1...12).contains(n) {
|
|
552
742
|
let base = kVK_F1
|
|
553
743
|
postKeyCode(CGKeyCode(base + (n - 1)), flags: flags)
|
|
@@ -567,7 +757,6 @@ private func geometryResponse() -> [String: Any] {
|
|
|
567
757
|
}
|
|
568
758
|
let frame = screen.frame
|
|
569
759
|
let scale = screen.backingScaleFactor
|
|
570
|
-
// Primary display: Cocoa origin is bottom-left of main screen; Quartz global top-left origin is (0,0).
|
|
571
760
|
return [
|
|
572
761
|
"ok": true,
|
|
573
762
|
"originX": 0,
|
|
@@ -592,6 +781,33 @@ private func handleCommand(_ object: [String: Any]) {
|
|
|
592
781
|
case "ping":
|
|
593
782
|
writeOk()
|
|
594
783
|
|
|
784
|
+
case "sessionStatus":
|
|
785
|
+
var locked = false
|
|
786
|
+
if let raw = CGSessionCopyCurrentDictionary() {
|
|
787
|
+
let dict = raw as NSDictionary
|
|
788
|
+
if let number = dict["CGSSessionScreenIsLocked"] as? NSNumber {
|
|
789
|
+
locked = number.boolValue
|
|
790
|
+
} else if let flag = dict["CGSSessionScreenIsLocked"] as? Bool {
|
|
791
|
+
locked = flag
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
// CGDisplayIsAsleep returns boolean_t (Int32); Swift will not coerce it in `if`.
|
|
795
|
+
let displayAsleep = CGDisplayIsAsleep(CGMainDisplayID()) != 0
|
|
796
|
+
let state: String
|
|
797
|
+
if locked {
|
|
798
|
+
state = "locked"
|
|
799
|
+
} else if displayAsleep {
|
|
800
|
+
state = "display_asleep"
|
|
801
|
+
} else {
|
|
802
|
+
state = "unlocked"
|
|
803
|
+
}
|
|
804
|
+
writeResponse([
|
|
805
|
+
"ok": true,
|
|
806
|
+
"state": state,
|
|
807
|
+
"locked": locked,
|
|
808
|
+
"displayAsleep": displayAsleep,
|
|
809
|
+
])
|
|
810
|
+
|
|
595
811
|
case "geometry":
|
|
596
812
|
writeResponse(geometryResponse())
|
|
597
813
|
|
|
@@ -612,7 +828,8 @@ private func handleCommand(_ object: [String: Any]) {
|
|
|
612
828
|
writeError("move requires pointX and pointY")
|
|
613
829
|
return
|
|
614
830
|
}
|
|
615
|
-
|
|
831
|
+
// Overlay-only: do not warp the user’s system cursor.
|
|
832
|
+
postOverlayMove(to: CGPoint(x: x, y: y))
|
|
616
833
|
writeOk()
|
|
617
834
|
|
|
618
835
|
case "click":
|
|
@@ -633,6 +850,28 @@ private func handleCommand(_ object: [String: Any]) {
|
|
|
633
850
|
postMouseClick(button: button, count: count)
|
|
634
851
|
writeOk()
|
|
635
852
|
|
|
853
|
+
case "clickAt":
|
|
854
|
+
guard let x = doubleValue(object, "pointX"), let y = doubleValue(object, "pointY") else {
|
|
855
|
+
writeError("clickAt requires pointX and pointY")
|
|
856
|
+
return
|
|
857
|
+
}
|
|
858
|
+
let buttonName = (object["button"] as? String ?? "left").lowercased()
|
|
859
|
+
let count = Int64(intValue(object, "count") ?? 1)
|
|
860
|
+
let button: CGMouseButton
|
|
861
|
+
switch buttonName {
|
|
862
|
+
case "left": button = .left
|
|
863
|
+
case "right": button = .right
|
|
864
|
+
default:
|
|
865
|
+
writeError("button must be left or right")
|
|
866
|
+
return
|
|
867
|
+
}
|
|
868
|
+
if count != 1 && count != 2 {
|
|
869
|
+
writeError("count must be 1 or 2")
|
|
870
|
+
return
|
|
871
|
+
}
|
|
872
|
+
postClickAt(point: CGPoint(x: x, y: y), button: button, count: count)
|
|
873
|
+
writeOk()
|
|
874
|
+
|
|
636
875
|
case "drag":
|
|
637
876
|
guard let fromX = doubleValue(object, "fromX"),
|
|
638
877
|
let fromY = doubleValue(object, "fromY"),
|
|
@@ -642,7 +881,7 @@ private func handleCommand(_ object: [String: Any]) {
|
|
|
642
881
|
writeError("drag requires fromX, fromY, toX, toY")
|
|
643
882
|
return
|
|
644
883
|
}
|
|
645
|
-
postDrag(from:
|
|
884
|
+
postDrag(from: CGPoint(x: fromX, y: fromY), to: CGPoint(x: toX, y: toY))
|
|
646
885
|
writeOk()
|
|
647
886
|
|
|
648
887
|
case "type":
|
|
@@ -678,7 +917,7 @@ private func handleCommand(_ object: [String: Any]) {
|
|
|
678
917
|
writeError("direction must be up, down, left, or right")
|
|
679
918
|
return
|
|
680
919
|
}
|
|
681
|
-
postScroll(at:
|
|
920
|
+
postScroll(at: CGPoint(x: x, y: y), direction: dir, amount: amount)
|
|
682
921
|
writeOk()
|
|
683
922
|
|
|
684
923
|
default:
|
|
@@ -698,7 +937,6 @@ private final class StdinReader {
|
|
|
698
937
|
while true {
|
|
699
938
|
let chunk = handle.availableData
|
|
700
939
|
if chunk.isEmpty {
|
|
701
|
-
// EOF — exit the process cleanly.
|
|
702
940
|
DispatchQueue.main.async {
|
|
703
941
|
NSApp.terminate(nil)
|
|
704
942
|
}
|
|
@@ -726,8 +964,45 @@ private final class StdinReader {
|
|
|
726
964
|
}
|
|
727
965
|
}
|
|
728
966
|
|
|
967
|
+
// MARK: - Session status (one-shot, no AppKit run loop)
|
|
968
|
+
|
|
969
|
+
/// Reports lock / display-asleep without starting the overlay. Used by the Node worker poll
|
|
970
|
+
/// so a locked Mac stays "online" with an explicit uiState instead of looking dead.
|
|
971
|
+
private func printSessionStatusAndExit() -> Never {
|
|
972
|
+
var locked = false
|
|
973
|
+
if let raw = CGSessionCopyCurrentDictionary() {
|
|
974
|
+
let dict = raw as NSDictionary
|
|
975
|
+
if let number = dict["CGSSessionScreenIsLocked"] as? NSNumber {
|
|
976
|
+
locked = number.boolValue
|
|
977
|
+
} else if let flag = dict["CGSSessionScreenIsLocked"] as? Bool {
|
|
978
|
+
locked = flag
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
// CGDisplayIsAsleep returns boolean_t (Int32); Swift will not coerce it in `if`.
|
|
982
|
+
let displayAsleep = CGDisplayIsAsleep(CGMainDisplayID()) != 0
|
|
983
|
+
let state: String
|
|
984
|
+
if locked {
|
|
985
|
+
state = "locked"
|
|
986
|
+
} else if displayAsleep {
|
|
987
|
+
state = "display_asleep"
|
|
988
|
+
} else {
|
|
989
|
+
state = "unlocked"
|
|
990
|
+
}
|
|
991
|
+
writeResponse([
|
|
992
|
+
"ok": true,
|
|
993
|
+
"state": state,
|
|
994
|
+
"locked": locked,
|
|
995
|
+
"displayAsleep": displayAsleep,
|
|
996
|
+
])
|
|
997
|
+
exit(0)
|
|
998
|
+
}
|
|
999
|
+
|
|
729
1000
|
private let stdinReader = StdinReader()
|
|
730
1001
|
|
|
1002
|
+
if CommandLine.arguments.contains("--session-status") {
|
|
1003
|
+
printSessionStatusAndExit()
|
|
1004
|
+
}
|
|
1005
|
+
|
|
731
1006
|
autoreleasepool {
|
|
732
1007
|
let app = NSApplication.shared
|
|
733
1008
|
app.setActivationPolicy(.accessory)
|