@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.
@@ -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: - Colors (Jian UI)
83
+ // MARK: - Pointer sprite (embedded PNG)
77
84
 
78
- private let jianCream = NSColor(srgbRed: 0xF5 / 255, green: 0xF5 / 255, blue: 0xF3 / 255, alpha: 1)
79
- private let jianInk = NSColor(srgbRed: 0x17 / 255, green: 0x17 / 255, blue: 0x16 / 255, alpha: 1)
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.92
88
+ private let sessionAlpha: CGFloat = 0.94
82
89
  private let idleAlpha: CGFloat = 0.18
83
- private let pointerSize: CGFloat = 32
84
- /// Tip of the wing relative to the view's top-left (hotspot for cursor placement).
85
- private let hotspot = CGPoint(x: 4, y: 4)
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.95
102
- let radius = 4 + maxRadius * rippleProgress
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(jianInk.withAlphaComponent(0.55 * alpha).cgColor)
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
- let path = Self.wingPath(in: bounds)
117
- ctx.saveGState()
118
- ctx.setShadow(offset: CGSize(width: 0, height: 1.5), blur: 4, color: NSColor.black.withAlphaComponent(0.28).cgColor)
119
- let fillAlpha = 1 - fillPulse * 0.35
120
- jianCream.withAlphaComponent(fillAlpha).setFill()
121
- path.fill()
122
- ctx.restoreGState()
123
-
124
- jianInk.setStroke()
125
- path.lineWidth = 2
126
- path.lineJoinStyle = .round
127
- path.lineCapStyle = .round
128
- path.stroke()
129
- }
130
-
131
- /// Ascending wing mark inspired by the Jian brand path (favicon / mark.tsx).
132
- private static func wingPath(in bounds: NSRect) -> NSBezierPath {
133
- let s = min(bounds.width, bounds.height)
134
- let path = NSBezierPath()
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.2) {
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: - Coordinate helpers
408
+ // MARK: - Cursor location / preserve
290
409
 
291
- private func cgPointFromTopLeft(_ x: Double, _ y: Double) -> CGPoint {
292
- CGPoint(x: x, y: y)
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
- // MARK: - Mouse / keyboard
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
- for clickIndex in 1...max(1, count) {
318
- if let down = CGEvent(
319
- mouseEventSource: nil,
320
- mouseType: downType,
321
- mouseCursorPosition: point,
322
- mouseButton: button
323
- ) {
324
- down.setIntegerValueField(.mouseEventClickState, value: clickIndex)
325
- down.post(tap: .cghidEventTap)
326
- }
327
- if let up = CGEvent(
328
- mouseEventSource: nil,
329
- mouseType: upType,
330
- mouseCursorPosition: point,
331
- mouseButton: button
332
- ) {
333
- up.setIntegerValueField(.mouseEventClickState, value: clickIndex)
334
- up.post(tap: .cghidEventTap)
335
- }
336
- if clickIndex < count {
337
- Thread.sleep(forTimeInterval: 0.05)
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
- postMouseMove(to: from)
526
+ OverlayController.shared.ensureSession()
527
+ animateOverlay(to: from)
344
528
  OverlayController.shared.flashClick()
345
- Thread.sleep(forTimeInterval: 0.02)
346
529
 
347
- if let down = CGEvent(
348
- mouseEventSource: nil,
349
- mouseType: .leftMouseDown,
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
- let steps = 12
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: .leftMouseDragged,
367
- mouseCursorPosition: p,
536
+ mouseType: .leftMouseDown,
537
+ mouseCursorPosition: from,
368
538
  mouseButton: .left
369
539
  ) {
370
- drag.post(tap: .cghidEventTap)
540
+ down.post(tap: .cghidEventTap)
371
541
  }
372
- Thread.sleep(forTimeInterval: 0.008)
373
- }
374
542
 
375
- if let up = CGEvent(
376
- mouseEventSource: nil,
377
- mouseType: .leftMouseUp,
378
- mouseCursorPosition: to,
379
- mouseButton: .left
380
- ) {
381
- up.post(tap: .cghidEventTap)
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
- postMouseMove(to: point)
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
- for _ in 0..<n {
401
- if let event = CGEvent(
402
- scrollWheelEvent2Source: nil,
403
- units: .line,
404
- wheelCount: 2,
405
- wheel1: Int32(dy),
406
- wheel2: Int32(dx),
407
- wheel3: 0
408
- ) {
409
- event.post(tap: .cghidEventTap)
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.005)
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
- postMouseMove(to: cgPointFromTopLeft(x, y))
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: cgPointFromTopLeft(fromX, fromY), to: cgPointFromTopLeft(toX, toY))
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: cgPointFromTopLeft(x, y), direction: dir, amount: amount)
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)