shortcutxl 0.2.12 → 0.2.13
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 +26 -26
- package/agent-docs/README.md +397 -397
- package/agent-docs/docs/compaction.md +390 -390
- package/agent-docs/docs/custom-provider.md +580 -580
- package/agent-docs/docs/extensions.md +1971 -1971
- package/agent-docs/docs/packages.md +209 -209
- package/agent-docs/docs/rpc.md +1317 -1317
- package/agent-docs/docs/sdk.md +962 -962
- package/agent-docs/docs/session.md +412 -412
- package/agent-docs/docs/termux.md +127 -127
- package/agent-docs/docs/tui.md +887 -887
- package/agent-docs/examples/README.md +25 -25
- package/agent-docs/examples/extensions/README.md +205 -205
- package/agent-docs/examples/extensions/antigravity-image-gen.ts +447 -447
- package/agent-docs/examples/extensions/auto-commit-on-exit.ts +49 -49
- package/agent-docs/examples/extensions/bash-spawn-hook.ts +30 -30
- package/agent-docs/examples/extensions/bookmark.ts +50 -50
- package/agent-docs/examples/extensions/built-in-tool-renderer.ts +256 -256
- package/agent-docs/examples/extensions/claude-rules.ts +86 -86
- package/agent-docs/examples/extensions/commands.ts +75 -75
- package/agent-docs/examples/extensions/confirm-destructive.ts +59 -59
- package/agent-docs/examples/extensions/custom-compaction.ts +126 -126
- package/agent-docs/examples/extensions/custom-footer.ts +63 -63
- package/agent-docs/examples/extensions/custom-header.ts +73 -73
- package/agent-docs/examples/extensions/custom-provider-anthropic/index.ts +660 -660
- package/agent-docs/examples/extensions/custom-provider-gitlab-duo/index.ts +362 -362
- package/agent-docs/examples/extensions/custom-provider-gitlab-duo/test.ts +88 -88
- package/agent-docs/examples/extensions/custom-provider-qwen-cli/index.ts +349 -349
- package/agent-docs/examples/extensions/dirty-repo-guard.ts +56 -56
- package/agent-docs/examples/extensions/doom-overlay/doom-component.ts +133 -133
- package/agent-docs/examples/extensions/doom-overlay/doom-keys.ts +108 -108
- package/agent-docs/examples/extensions/doom-overlay/index.ts +74 -74
- package/agent-docs/examples/extensions/dynamic-resources/index.ts +15 -15
- package/agent-docs/examples/extensions/dynamic-tools.ts +77 -77
- package/agent-docs/examples/extensions/event-bus.ts +43 -43
- package/agent-docs/examples/extensions/file-trigger.ts +41 -41
- package/agent-docs/examples/extensions/git-checkpoint.ts +53 -53
- package/agent-docs/examples/extensions/handoff.ts +155 -155
- package/agent-docs/examples/extensions/hello.ts +25 -25
- package/agent-docs/examples/extensions/inline-bash.ts +94 -94
- package/agent-docs/examples/extensions/input-transform.ts +43 -43
- package/agent-docs/examples/extensions/interactive-shell.ts +209 -209
- package/agent-docs/examples/extensions/mac-system-theme.ts +47 -47
- package/agent-docs/examples/extensions/message-renderer.ts +59 -59
- package/agent-docs/examples/extensions/minimal-mode.ts +430 -430
- package/agent-docs/examples/extensions/modal-editor.ts +90 -90
- package/agent-docs/examples/extensions/model-status.ts +31 -31
- package/agent-docs/examples/extensions/notify.ts +55 -55
- package/agent-docs/examples/extensions/overlay-qa-tests.ts +936 -936
- package/agent-docs/examples/extensions/overlay-test.ts +159 -159
- package/agent-docs/examples/extensions/permission-gate.ts +37 -37
- package/agent-docs/examples/extensions/pirate.ts +47 -47
- package/agent-docs/examples/extensions/plan-mode/index.ts +363 -363
- package/agent-docs/examples/extensions/preset.ts +418 -418
- package/agent-docs/examples/extensions/protected-paths.ts +30 -30
- package/agent-docs/examples/extensions/qna.ts +122 -122
- package/agent-docs/examples/extensions/question.ts +278 -278
- package/agent-docs/examples/extensions/questionnaire.ts +440 -440
- package/agent-docs/examples/extensions/rainbow-editor.ts +90 -90
- package/agent-docs/examples/extensions/reload-runtime.ts +37 -37
- package/agent-docs/examples/extensions/rpc-demo.ts +124 -124
- package/agent-docs/examples/extensions/sandbox/index.ts +324 -324
- package/agent-docs/examples/extensions/send-user-message.ts +97 -97
- package/agent-docs/examples/extensions/session-name.ts +27 -27
- package/agent-docs/examples/extensions/shutdown-command.ts +69 -69
- package/agent-docs/examples/extensions/snake.ts +343 -343
- package/agent-docs/examples/extensions/space-invaders.ts +566 -566
- package/agent-docs/examples/extensions/ssh.ts +233 -233
- package/agent-docs/examples/extensions/status-line.ts +40 -40
- package/agent-docs/examples/extensions/subagent/agents.ts +130 -130
- package/agent-docs/examples/extensions/subagent/index.ts +1068 -1068
- package/agent-docs/examples/extensions/summarize.ts +206 -206
- package/agent-docs/examples/extensions/system-prompt-header.ts +17 -17
- package/agent-docs/examples/extensions/timed-confirm.ts +72 -72
- package/agent-docs/examples/extensions/titlebar-spinner.ts +58 -58
- package/agent-docs/examples/extensions/todo.ts +314 -314
- package/agent-docs/examples/extensions/tool-override.ts +146 -146
- package/agent-docs/examples/extensions/tools.ts +145 -145
- package/agent-docs/examples/extensions/trigger-compact.ts +40 -40
- package/agent-docs/examples/extensions/truncated-tool.ts +194 -194
- package/agent-docs/examples/extensions/widget-placement.ts +17 -17
- package/agent-docs/examples/extensions/with-deps/index.ts +37 -37
- package/agent-docs/examples/rpc-extension-ui.ts +654 -654
- package/agent-docs/examples/sdk/01-minimal.ts +22 -22
- package/agent-docs/examples/sdk/02-custom-model.ts +48 -48
- package/agent-docs/examples/sdk/03-custom-prompt.ts +55 -55
- package/agent-docs/examples/sdk/04-skills.ts +53 -53
- package/agent-docs/examples/sdk/05-tools.ts +56 -56
- package/agent-docs/examples/sdk/06-extensions.ts +88 -88
- package/agent-docs/examples/sdk/07-context-files.ts +40 -40
- package/agent-docs/examples/sdk/08-prompt-templates.ts +47 -47
- package/agent-docs/examples/sdk/09-api-keys-and-oauth.ts +48 -48
- package/agent-docs/examples/sdk/10-settings.ts +54 -54
- package/agent-docs/examples/sdk/11-sessions.ts +48 -48
- package/agent-docs/examples/sdk/12-full-control.ts +82 -82
- package/agent-docs/examples/sdk/README.md +144 -144
- package/agent-docs/xll-spec.md +110 -110
- package/dist/core/auth-storage.js +21 -2
- package/package.json +1 -1
- package/xll/ShortcutXL.xll +0 -0
- package/xll/modules/debug_render.py +272 -272
- package/xll/modules/gameboy.py +241 -241
- package/xll/modules/pong.py +188 -188
- package/xll/modules/shortcut_xl/_diff_highlight.py +176 -0
- package/xll/modules/shortcut_xl/_log.py +12 -12
- package/xll/modules/shortcut_xl/_registry.py +44 -44
- package/xll/modules/stocks.py +100 -100
- /package/skills/{com-advanced-api → COM-advanced-api}/SKILL.md +0 -0
- /package/skills/{com-advanced-api → COM-advanced-api}/excel-type-library.py +0 -0
- /package/skills/{com-advanced-api → COM-advanced-api}/office-type-library.py +0 -0
package/xll/modules/pong.py
CHANGED
|
@@ -1,188 +1,188 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Pong in Excel — type =pong_start() to play!
|
|
3
|
-
|
|
4
|
-
Two AI paddles rally a ball across a 30x20 cell grid.
|
|
5
|
-
Type =pong_stop() to end the game.
|
|
6
|
-
"""
|
|
7
|
-
import shortcut_xl
|
|
8
|
-
from shortcut_xl import xl_func
|
|
9
|
-
import random
|
|
10
|
-
|
|
11
|
-
WIDTH = 30
|
|
12
|
-
HEIGHT = 20
|
|
13
|
-
PADDLE_H = 4
|
|
14
|
-
FPS = 30
|
|
15
|
-
|
|
16
|
-
# Grid occupies A3:AD22 (20 rows x 30 cols)
|
|
17
|
-
_GRID_RANGE = "A3:AD22"
|
|
18
|
-
|
|
19
|
-
_state = {
|
|
20
|
-
'ball_x': 15.0, 'ball_y': 10.0,
|
|
21
|
-
'ball_dx': 1.0, 'ball_dy': 0.7,
|
|
22
|
-
'paddle_l': 8, 'paddle_r': 8,
|
|
23
|
-
'score_l': 0, 'score_r': 0,
|
|
24
|
-
'running': False,
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
def _reset_ball():
|
|
29
|
-
_state['ball_x'] = WIDTH / 2.0
|
|
30
|
-
_state['ball_y'] = HEIGHT / 2.0
|
|
31
|
-
_state['ball_dx'] = random.choice([-1.0, 1.0])
|
|
32
|
-
_state['ball_dy'] = random.uniform(-1.0, 1.0)
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
def _render():
|
|
36
|
-
# Build the grid
|
|
37
|
-
grid = []
|
|
38
|
-
for r in range(HEIGHT):
|
|
39
|
-
row = []
|
|
40
|
-
for c in range(WIDTH):
|
|
41
|
-
ch = ""
|
|
42
|
-
# Left paddle
|
|
43
|
-
if c == 0 and _state['paddle_l'] <= r < _state['paddle_l'] + PADDLE_H:
|
|
44
|
-
ch = chr(9608) # full block
|
|
45
|
-
# Right paddle
|
|
46
|
-
elif c == WIDTH - 1 and _state['paddle_r'] <= r < _state['paddle_r'] + PADDLE_H:
|
|
47
|
-
ch = chr(9608)
|
|
48
|
-
# Ball
|
|
49
|
-
elif int(round(_state['ball_y'])) == r and int(round(_state['ball_x'])) == c:
|
|
50
|
-
ch = chr(9679) # black circle
|
|
51
|
-
# Center line
|
|
52
|
-
elif c == WIDTH // 2 and r % 2 == 0:
|
|
53
|
-
ch = chr(9474) # light vertical
|
|
54
|
-
row.append(ch)
|
|
55
|
-
grid.append(row)
|
|
56
|
-
|
|
57
|
-
# Write entire frame in one batch — one repaint, one retry point
|
|
58
|
-
score = f"PONG {_state['score_l']} : {_state['score_r']}"
|
|
59
|
-
|
|
60
|
-
def _paint(app):
|
|
61
|
-
# Suppress auto-recalculation — without this every 600-cell write
|
|
62
|
-
# triggers a full recalc cycle that fights our rendering and causes
|
|
63
|
-
# the visible "flashing" effect.
|
|
64
|
-
app.Calculation = -4135 # xlCalculationManual
|
|
65
|
-
|
|
66
|
-
sheet = app.ActiveSheet
|
|
67
|
-
|
|
68
|
-
# One-time setup: make cells square-ish and use a monospace font
|
|
69
|
-
# so the Unicode glyphs (█ ● │) are actually visible.
|
|
70
|
-
if not _state.get('_formatted'):
|
|
71
|
-
_state['_formatted'] = True
|
|
72
|
-
area = sheet.Range(_GRID_RANGE)
|
|
73
|
-
area.ColumnWidth = 2.14
|
|
74
|
-
area.Font.Name = "Consolas"
|
|
75
|
-
area.Font.Size = 11
|
|
76
|
-
area.HorizontalAlignment = -4108 # xlCenter
|
|
77
|
-
|
|
78
|
-
# Grid (20x30) — use direct range address, NOT Resize()
|
|
79
|
-
# win32com Resize() is broken: returns only the bottom-right cell
|
|
80
|
-
grid_com = tuple(tuple(row) for row in grid)
|
|
81
|
-
sheet.Range(_GRID_RANGE).Value = grid_com
|
|
82
|
-
# Scoreboard + instructions
|
|
83
|
-
sheet.Range("A1").Value = score
|
|
84
|
-
sheet.Range("A2").Value = "pong_start() to play pong_stop() to end"
|
|
85
|
-
|
|
86
|
-
shortcut_xl.xl_batch(_paint)
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
def _tick():
|
|
90
|
-
shortcut_xl.xl_log(f"_tick: running={_state['running']}")
|
|
91
|
-
if not _state['running']:
|
|
92
|
-
return
|
|
93
|
-
|
|
94
|
-
try:
|
|
95
|
-
_tick_inner()
|
|
96
|
-
shortcut_xl.xl_log("_tick: frame OK")
|
|
97
|
-
except Exception as e:
|
|
98
|
-
shortcut_xl.xl_log(f"pong tick error: {e}")
|
|
99
|
-
import traceback
|
|
100
|
-
shortcut_xl.xl_log(traceback.format_exc())
|
|
101
|
-
_state['running'] = False
|
|
102
|
-
shortcut_xl.schedule_call(_restore_calc, 0.3)
|
|
103
|
-
return
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
def _tick_inner():
|
|
107
|
-
s = _state
|
|
108
|
-
|
|
109
|
-
# Move ball
|
|
110
|
-
s['ball_x'] += s['ball_dx']
|
|
111
|
-
s['ball_y'] += s['ball_dy']
|
|
112
|
-
|
|
113
|
-
# Bounce top/bottom
|
|
114
|
-
if s['ball_y'] <= 0:
|
|
115
|
-
s['ball_y'] = 0
|
|
116
|
-
s['ball_dy'] = abs(s['ball_dy'])
|
|
117
|
-
elif s['ball_y'] >= HEIGHT - 1:
|
|
118
|
-
s['ball_y'] = HEIGHT - 1
|
|
119
|
-
s['ball_dy'] = -abs(s['ball_dy'])
|
|
120
|
-
|
|
121
|
-
# Left paddle check
|
|
122
|
-
if s['ball_x'] <= 1:
|
|
123
|
-
if s['paddle_l'] - 0.5 <= s['ball_y'] < s['paddle_l'] + PADDLE_H + 0.5:
|
|
124
|
-
s['ball_dx'] = abs(s['ball_dx'])
|
|
125
|
-
s['ball_x'] = 1
|
|
126
|
-
# Add spin based on where ball hit paddle
|
|
127
|
-
hit_pos = (s['ball_y'] - s['paddle_l']) / PADDLE_H
|
|
128
|
-
s['ball_dy'] = (hit_pos - 0.5) * 2.0
|
|
129
|
-
else:
|
|
130
|
-
s['score_r'] += 1
|
|
131
|
-
_reset_ball()
|
|
132
|
-
|
|
133
|
-
# Right paddle check
|
|
134
|
-
if s['ball_x'] >= WIDTH - 2:
|
|
135
|
-
if s['paddle_r'] - 0.5 <= s['ball_y'] < s['paddle_r'] + PADDLE_H + 0.5:
|
|
136
|
-
s['ball_dx'] = -abs(s['ball_dx'])
|
|
137
|
-
s['ball_x'] = WIDTH - 2
|
|
138
|
-
hit_pos = (s['ball_y'] - s['paddle_r']) / PADDLE_H
|
|
139
|
-
s['ball_dy'] = (hit_pos - 0.5) * 2.0
|
|
140
|
-
else:
|
|
141
|
-
s['score_l'] += 1
|
|
142
|
-
_reset_ball()
|
|
143
|
-
|
|
144
|
-
# AI: move paddles toward ball with slight delay
|
|
145
|
-
ball_y = s['ball_y']
|
|
146
|
-
for paddle in ('paddle_l', 'paddle_r'):
|
|
147
|
-
target = int(ball_y) - PADDLE_H // 2
|
|
148
|
-
diff = target - s[paddle]
|
|
149
|
-
# Add some imperfection
|
|
150
|
-
speed = 1 if abs(diff) < 3 else 2
|
|
151
|
-
if diff > 0:
|
|
152
|
-
s[paddle] = min(s[paddle] + speed, HEIGHT - PADDLE_H)
|
|
153
|
-
elif diff < 0:
|
|
154
|
-
s[paddle] = max(s[paddle] - speed, 0)
|
|
155
|
-
|
|
156
|
-
# Speed up slightly over time
|
|
157
|
-
if abs(s['ball_dx']) < 2.5:
|
|
158
|
-
s['ball_dx'] *= 1.005
|
|
159
|
-
|
|
160
|
-
_render()
|
|
161
|
-
shortcut_xl.schedule_call(_tick, 1.0 / FPS)
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
def _restore_calc():
|
|
165
|
-
"""Restore automatic calculation after the game ends."""
|
|
166
|
-
def _do(app):
|
|
167
|
-
app.Calculation = -4105 # xlCalculationAutomatic
|
|
168
|
-
shortcut_xl.xl_batch(_do)
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
@xl_func
|
|
172
|
-
def pong_start():
|
|
173
|
-
if _state['running']:
|
|
174
|
-
return "Already running!"
|
|
175
|
-
_state['running'] = True
|
|
176
|
-
_state['score_l'] = 0
|
|
177
|
-
_state['score_r'] = 0
|
|
178
|
-
_state['_formatted'] = False
|
|
179
|
-
_reset_ball()
|
|
180
|
-
shortcut_xl.schedule_call(_tick, 0.1)
|
|
181
|
-
return "PONG!"
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
@xl_func
|
|
185
|
-
def pong_stop():
|
|
186
|
-
_state['running'] = False
|
|
187
|
-
shortcut_xl.schedule_call(_restore_calc, 0.3)
|
|
188
|
-
return "Stopped"
|
|
1
|
+
"""
|
|
2
|
+
Pong in Excel — type =pong_start() to play!
|
|
3
|
+
|
|
4
|
+
Two AI paddles rally a ball across a 30x20 cell grid.
|
|
5
|
+
Type =pong_stop() to end the game.
|
|
6
|
+
"""
|
|
7
|
+
import shortcut_xl
|
|
8
|
+
from shortcut_xl import xl_func
|
|
9
|
+
import random
|
|
10
|
+
|
|
11
|
+
WIDTH = 30
|
|
12
|
+
HEIGHT = 20
|
|
13
|
+
PADDLE_H = 4
|
|
14
|
+
FPS = 30
|
|
15
|
+
|
|
16
|
+
# Grid occupies A3:AD22 (20 rows x 30 cols)
|
|
17
|
+
_GRID_RANGE = "A3:AD22"
|
|
18
|
+
|
|
19
|
+
_state = {
|
|
20
|
+
'ball_x': 15.0, 'ball_y': 10.0,
|
|
21
|
+
'ball_dx': 1.0, 'ball_dy': 0.7,
|
|
22
|
+
'paddle_l': 8, 'paddle_r': 8,
|
|
23
|
+
'score_l': 0, 'score_r': 0,
|
|
24
|
+
'running': False,
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _reset_ball():
|
|
29
|
+
_state['ball_x'] = WIDTH / 2.0
|
|
30
|
+
_state['ball_y'] = HEIGHT / 2.0
|
|
31
|
+
_state['ball_dx'] = random.choice([-1.0, 1.0])
|
|
32
|
+
_state['ball_dy'] = random.uniform(-1.0, 1.0)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _render():
|
|
36
|
+
# Build the grid
|
|
37
|
+
grid = []
|
|
38
|
+
for r in range(HEIGHT):
|
|
39
|
+
row = []
|
|
40
|
+
for c in range(WIDTH):
|
|
41
|
+
ch = ""
|
|
42
|
+
# Left paddle
|
|
43
|
+
if c == 0 and _state['paddle_l'] <= r < _state['paddle_l'] + PADDLE_H:
|
|
44
|
+
ch = chr(9608) # full block
|
|
45
|
+
# Right paddle
|
|
46
|
+
elif c == WIDTH - 1 and _state['paddle_r'] <= r < _state['paddle_r'] + PADDLE_H:
|
|
47
|
+
ch = chr(9608)
|
|
48
|
+
# Ball
|
|
49
|
+
elif int(round(_state['ball_y'])) == r and int(round(_state['ball_x'])) == c:
|
|
50
|
+
ch = chr(9679) # black circle
|
|
51
|
+
# Center line
|
|
52
|
+
elif c == WIDTH // 2 and r % 2 == 0:
|
|
53
|
+
ch = chr(9474) # light vertical
|
|
54
|
+
row.append(ch)
|
|
55
|
+
grid.append(row)
|
|
56
|
+
|
|
57
|
+
# Write entire frame in one batch — one repaint, one retry point
|
|
58
|
+
score = f"PONG {_state['score_l']} : {_state['score_r']}"
|
|
59
|
+
|
|
60
|
+
def _paint(app):
|
|
61
|
+
# Suppress auto-recalculation — without this every 600-cell write
|
|
62
|
+
# triggers a full recalc cycle that fights our rendering and causes
|
|
63
|
+
# the visible "flashing" effect.
|
|
64
|
+
app.Calculation = -4135 # xlCalculationManual
|
|
65
|
+
|
|
66
|
+
sheet = app.ActiveSheet
|
|
67
|
+
|
|
68
|
+
# One-time setup: make cells square-ish and use a monospace font
|
|
69
|
+
# so the Unicode glyphs (█ ● │) are actually visible.
|
|
70
|
+
if not _state.get('_formatted'):
|
|
71
|
+
_state['_formatted'] = True
|
|
72
|
+
area = sheet.Range(_GRID_RANGE)
|
|
73
|
+
area.ColumnWidth = 2.14
|
|
74
|
+
area.Font.Name = "Consolas"
|
|
75
|
+
area.Font.Size = 11
|
|
76
|
+
area.HorizontalAlignment = -4108 # xlCenter
|
|
77
|
+
|
|
78
|
+
# Grid (20x30) — use direct range address, NOT Resize()
|
|
79
|
+
# win32com Resize() is broken: returns only the bottom-right cell
|
|
80
|
+
grid_com = tuple(tuple(row) for row in grid)
|
|
81
|
+
sheet.Range(_GRID_RANGE).Value = grid_com
|
|
82
|
+
# Scoreboard + instructions
|
|
83
|
+
sheet.Range("A1").Value = score
|
|
84
|
+
sheet.Range("A2").Value = "pong_start() to play pong_stop() to end"
|
|
85
|
+
|
|
86
|
+
shortcut_xl.xl_batch(_paint)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _tick():
|
|
90
|
+
shortcut_xl.xl_log(f"_tick: running={_state['running']}")
|
|
91
|
+
if not _state['running']:
|
|
92
|
+
return
|
|
93
|
+
|
|
94
|
+
try:
|
|
95
|
+
_tick_inner()
|
|
96
|
+
shortcut_xl.xl_log("_tick: frame OK")
|
|
97
|
+
except Exception as e:
|
|
98
|
+
shortcut_xl.xl_log(f"pong tick error: {e}")
|
|
99
|
+
import traceback
|
|
100
|
+
shortcut_xl.xl_log(traceback.format_exc())
|
|
101
|
+
_state['running'] = False
|
|
102
|
+
shortcut_xl.schedule_call(_restore_calc, 0.3)
|
|
103
|
+
return
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _tick_inner():
|
|
107
|
+
s = _state
|
|
108
|
+
|
|
109
|
+
# Move ball
|
|
110
|
+
s['ball_x'] += s['ball_dx']
|
|
111
|
+
s['ball_y'] += s['ball_dy']
|
|
112
|
+
|
|
113
|
+
# Bounce top/bottom
|
|
114
|
+
if s['ball_y'] <= 0:
|
|
115
|
+
s['ball_y'] = 0
|
|
116
|
+
s['ball_dy'] = abs(s['ball_dy'])
|
|
117
|
+
elif s['ball_y'] >= HEIGHT - 1:
|
|
118
|
+
s['ball_y'] = HEIGHT - 1
|
|
119
|
+
s['ball_dy'] = -abs(s['ball_dy'])
|
|
120
|
+
|
|
121
|
+
# Left paddle check
|
|
122
|
+
if s['ball_x'] <= 1:
|
|
123
|
+
if s['paddle_l'] - 0.5 <= s['ball_y'] < s['paddle_l'] + PADDLE_H + 0.5:
|
|
124
|
+
s['ball_dx'] = abs(s['ball_dx'])
|
|
125
|
+
s['ball_x'] = 1
|
|
126
|
+
# Add spin based on where ball hit paddle
|
|
127
|
+
hit_pos = (s['ball_y'] - s['paddle_l']) / PADDLE_H
|
|
128
|
+
s['ball_dy'] = (hit_pos - 0.5) * 2.0
|
|
129
|
+
else:
|
|
130
|
+
s['score_r'] += 1
|
|
131
|
+
_reset_ball()
|
|
132
|
+
|
|
133
|
+
# Right paddle check
|
|
134
|
+
if s['ball_x'] >= WIDTH - 2:
|
|
135
|
+
if s['paddle_r'] - 0.5 <= s['ball_y'] < s['paddle_r'] + PADDLE_H + 0.5:
|
|
136
|
+
s['ball_dx'] = -abs(s['ball_dx'])
|
|
137
|
+
s['ball_x'] = WIDTH - 2
|
|
138
|
+
hit_pos = (s['ball_y'] - s['paddle_r']) / PADDLE_H
|
|
139
|
+
s['ball_dy'] = (hit_pos - 0.5) * 2.0
|
|
140
|
+
else:
|
|
141
|
+
s['score_l'] += 1
|
|
142
|
+
_reset_ball()
|
|
143
|
+
|
|
144
|
+
# AI: move paddles toward ball with slight delay
|
|
145
|
+
ball_y = s['ball_y']
|
|
146
|
+
for paddle in ('paddle_l', 'paddle_r'):
|
|
147
|
+
target = int(ball_y) - PADDLE_H // 2
|
|
148
|
+
diff = target - s[paddle]
|
|
149
|
+
# Add some imperfection
|
|
150
|
+
speed = 1 if abs(diff) < 3 else 2
|
|
151
|
+
if diff > 0:
|
|
152
|
+
s[paddle] = min(s[paddle] + speed, HEIGHT - PADDLE_H)
|
|
153
|
+
elif diff < 0:
|
|
154
|
+
s[paddle] = max(s[paddle] - speed, 0)
|
|
155
|
+
|
|
156
|
+
# Speed up slightly over time
|
|
157
|
+
if abs(s['ball_dx']) < 2.5:
|
|
158
|
+
s['ball_dx'] *= 1.005
|
|
159
|
+
|
|
160
|
+
_render()
|
|
161
|
+
shortcut_xl.schedule_call(_tick, 1.0 / FPS)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _restore_calc():
|
|
165
|
+
"""Restore automatic calculation after the game ends."""
|
|
166
|
+
def _do(app):
|
|
167
|
+
app.Calculation = -4105 # xlCalculationAutomatic
|
|
168
|
+
shortcut_xl.xl_batch(_do)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
@xl_func
|
|
172
|
+
def pong_start():
|
|
173
|
+
if _state['running']:
|
|
174
|
+
return "Already running!"
|
|
175
|
+
_state['running'] = True
|
|
176
|
+
_state['score_l'] = 0
|
|
177
|
+
_state['score_r'] = 0
|
|
178
|
+
_state['_formatted'] = False
|
|
179
|
+
_reset_ball()
|
|
180
|
+
shortcut_xl.schedule_call(_tick, 0.1)
|
|
181
|
+
return "PONG!"
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
@xl_func
|
|
185
|
+
def pong_stop():
|
|
186
|
+
_state['running'] = False
|
|
187
|
+
shortcut_xl.schedule_call(_restore_calc, 0.3)
|
|
188
|
+
return "Stopped"
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
"""Diff highlighting — CF overlay + MessageBox approval for cell changes.
|
|
2
|
+
|
|
3
|
+
After user code executes and dirty cells are collected, this module:
|
|
4
|
+
1. Applies Conditional Formatting to highlight changed cells (yellow)
|
|
5
|
+
2. Shows a Win32 MessageBox asking the user to keep or reject changes
|
|
6
|
+
3. If rejected, reverts cells using oldValue/oldFormula from the diff
|
|
7
|
+
4. Always cleans up CF rules before returning
|
|
8
|
+
|
|
9
|
+
Performance: CF rules are applied via batched Union ranges (not per-cell),
|
|
10
|
+
so highlighting cost is O(n / batch_size) COM calls, not O(n).
|
|
11
|
+
Revert writes are per-cell but in-process COM (~microseconds each).
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import ctypes
|
|
15
|
+
from shortcut_xl._log import xl_log
|
|
16
|
+
|
|
17
|
+
# Win32 MessageBox constants
|
|
18
|
+
_MB_YESNO = 0x04
|
|
19
|
+
_MB_ICONQUESTION = 0x20
|
|
20
|
+
_IDYES = 6
|
|
21
|
+
|
|
22
|
+
# Excel constants
|
|
23
|
+
_XL_EXPRESSION = 2 # xlExpression CF type
|
|
24
|
+
_XL_CALC_MANUAL = -4135 # xlCalculationManual
|
|
25
|
+
_HIGHLIGHT_COLOR = 0x00FFFF # yellow (BGR for #FFFF00) — Track Changes color
|
|
26
|
+
|
|
27
|
+
# Max length for comma-separated address string (Excel limit)
|
|
28
|
+
_ADDR_BATCH_LIMIT = 255
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _group_by_sheet(dirty_cells):
|
|
32
|
+
by_sheet = {}
|
|
33
|
+
for cell in dirty_cells:
|
|
34
|
+
by_sheet.setdefault(cell['sheet'], []).append(cell)
|
|
35
|
+
return by_sheet
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _build_union(app, ws, addresses):
|
|
39
|
+
"""Build a multi-area Range from address list, batching to stay under limits."""
|
|
40
|
+
if not addresses:
|
|
41
|
+
return None
|
|
42
|
+
batches = []
|
|
43
|
+
current = ""
|
|
44
|
+
for addr in addresses:
|
|
45
|
+
candidate = f"{current},{addr}" if current else addr
|
|
46
|
+
if len(candidate) > _ADDR_BATCH_LIMIT and current:
|
|
47
|
+
batches.append(current)
|
|
48
|
+
current = addr
|
|
49
|
+
else:
|
|
50
|
+
current = candidate
|
|
51
|
+
if current:
|
|
52
|
+
batches.append(current)
|
|
53
|
+
|
|
54
|
+
rng = ws.Range(batches[0])
|
|
55
|
+
for b in batches[1:]:
|
|
56
|
+
rng = app.Union(rng, ws.Range(b))
|
|
57
|
+
return rng
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _apply_cf_highlights(app, by_sheet):
|
|
61
|
+
"""Apply CF rules to highlight changed cells. Returns cleanup state."""
|
|
62
|
+
cf_state = [] # (ws, count_before) for cleanup
|
|
63
|
+
for sheet_name, cells in by_sheet.items():
|
|
64
|
+
try:
|
|
65
|
+
ws = app.Worksheets(sheet_name)
|
|
66
|
+
count_before = ws.Cells.FormatConditions.Count
|
|
67
|
+
rng = _build_union(app, ws, [c['address'] for c in cells])
|
|
68
|
+
if rng is None:
|
|
69
|
+
continue
|
|
70
|
+
rng.FormatConditions.Add(Type=_XL_EXPRESSION, Formula1="=TRUE")
|
|
71
|
+
rule = rng.FormatConditions(rng.FormatConditions.Count)
|
|
72
|
+
rule.Interior.Color = _HIGHLIGHT_COLOR
|
|
73
|
+
cf_state.append((ws, count_before))
|
|
74
|
+
except Exception as e:
|
|
75
|
+
xl_log(f"_apply_cf_highlights({sheet_name}): {e}")
|
|
76
|
+
return cf_state
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _cleanup_cf(cf_state):
|
|
80
|
+
"""Remove CF rules added during highlighting."""
|
|
81
|
+
for ws, count_before in cf_state:
|
|
82
|
+
try:
|
|
83
|
+
while ws.Cells.FormatConditions.Count > count_before:
|
|
84
|
+
ws.Cells.FormatConditions(
|
|
85
|
+
ws.Cells.FormatConditions.Count).Delete()
|
|
86
|
+
except Exception as e:
|
|
87
|
+
xl_log(f"_cleanup_cf: {e}")
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _revert_cells(app, dirty_cells):
|
|
91
|
+
"""Restore old values/formulas for rejected changes.
|
|
92
|
+
|
|
93
|
+
Sets calc to manual during revert to avoid expensive recalcs
|
|
94
|
+
on each write, then restores original calc mode.
|
|
95
|
+
"""
|
|
96
|
+
orig_calc = None
|
|
97
|
+
try:
|
|
98
|
+
orig_calc = app.Calculation
|
|
99
|
+
app.Calculation = _XL_CALC_MANUAL
|
|
100
|
+
except Exception:
|
|
101
|
+
pass
|
|
102
|
+
|
|
103
|
+
for sheet_name, cells in _group_by_sheet(dirty_cells).items():
|
|
104
|
+
try:
|
|
105
|
+
ws = app.Worksheets(sheet_name)
|
|
106
|
+
for cell in cells:
|
|
107
|
+
addr = cell['address']
|
|
108
|
+
try:
|
|
109
|
+
if cell.get('oldFormula'):
|
|
110
|
+
ws.Range(addr).Formula = cell['oldFormula']
|
|
111
|
+
else:
|
|
112
|
+
ws.Range(addr).Value = cell['oldValue']
|
|
113
|
+
except Exception as e:
|
|
114
|
+
xl_log(f"_revert_cells({sheet_name}!{addr}): {e}")
|
|
115
|
+
except Exception as e:
|
|
116
|
+
xl_log(f"_revert_cells({sheet_name}): {e}")
|
|
117
|
+
|
|
118
|
+
if orig_calc is not None:
|
|
119
|
+
try:
|
|
120
|
+
app.Calculation = orig_calc
|
|
121
|
+
except Exception:
|
|
122
|
+
pass
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def highlight_and_approve(app, dirty_cells):
|
|
126
|
+
"""Highlight changed cells with CF, show MessageBox for user approval.
|
|
127
|
+
|
|
128
|
+
Returns True if approved, False if rejected.
|
|
129
|
+
On rejection, all changes are reverted from dirty_cells' oldValue/oldFormula.
|
|
130
|
+
CF highlighting is always cleaned up before returning.
|
|
131
|
+
"""
|
|
132
|
+
if not dirty_cells:
|
|
133
|
+
return True
|
|
134
|
+
|
|
135
|
+
by_sheet = _group_by_sheet(dirty_cells)
|
|
136
|
+
cf_state = []
|
|
137
|
+
|
|
138
|
+
try:
|
|
139
|
+
# Phase 1: Apply CF highlights
|
|
140
|
+
try:
|
|
141
|
+
app.ScreenUpdating = False
|
|
142
|
+
except Exception:
|
|
143
|
+
pass
|
|
144
|
+
cf_state = _apply_cf_highlights(app, by_sheet)
|
|
145
|
+
try:
|
|
146
|
+
app.ScreenUpdating = True
|
|
147
|
+
except Exception:
|
|
148
|
+
pass
|
|
149
|
+
|
|
150
|
+
# Phase 2: MessageBox — modal to Excel window
|
|
151
|
+
n = len(dirty_cells)
|
|
152
|
+
sheet_summary = ", ".join(
|
|
153
|
+
f"{name} ({len(cells)})" for name, cells in by_sheet.items()
|
|
154
|
+
)
|
|
155
|
+
msg = (
|
|
156
|
+
f"ShortcutXL modified {n} cell{'s' if n != 1 else ''} "
|
|
157
|
+
f"in: {sheet_summary}\n\n"
|
|
158
|
+
"Keep these changes?"
|
|
159
|
+
)
|
|
160
|
+
try:
|
|
161
|
+
hwnd = app.Hwnd
|
|
162
|
+
except Exception:
|
|
163
|
+
hwnd = 0
|
|
164
|
+
result = ctypes.windll.user32.MessageBoxW(
|
|
165
|
+
hwnd, msg, "ShortcutXL", _MB_YESNO | _MB_ICONQUESTION
|
|
166
|
+
)
|
|
167
|
+
approved = (result == _IDYES)
|
|
168
|
+
|
|
169
|
+
# Phase 3: Revert if rejected
|
|
170
|
+
if not approved:
|
|
171
|
+
_revert_cells(app, dirty_cells)
|
|
172
|
+
|
|
173
|
+
return approved
|
|
174
|
+
|
|
175
|
+
finally:
|
|
176
|
+
_cleanup_cf(cf_state)
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
"""Logging utility for ShortcutXL."""
|
|
2
|
-
|
|
3
|
-
import os
|
|
4
|
-
import datetime
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
def xl_log(msg):
|
|
8
|
-
"""Append a timestamped message to %TEMP%\\shortcutxl.log."""
|
|
9
|
-
log_path = os.path.join(os.environ.get("TEMP", "."), "shortcutxl.log")
|
|
10
|
-
timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")[:-3]
|
|
11
|
-
with open(log_path, "a", encoding="utf-8") as f:
|
|
12
|
-
f.write(f"[{timestamp}] {msg}\n")
|
|
1
|
+
"""Logging utility for ShortcutXL."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import datetime
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def xl_log(msg):
|
|
8
|
+
"""Append a timestamped message to %TEMP%\\shortcutxl.log."""
|
|
9
|
+
log_path = os.path.join(os.environ.get("TEMP", "."), "shortcutxl.log")
|
|
10
|
+
timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")[:-3]
|
|
11
|
+
with open(log_path, "a", encoding="utf-8") as f:
|
|
12
|
+
f.write(f"[{timestamp}] {msg}\n")
|
|
@@ -1,44 +1,44 @@
|
|
|
1
|
-
"""@xl_func decorator — marks functions for registration as Excel UDFs.
|
|
2
|
-
|
|
3
|
-
The C runtime scans _registry at xlAutoOpen time to register functions with Excel.
|
|
4
|
-
"""
|
|
5
|
-
|
|
6
|
-
import inspect
|
|
7
|
-
|
|
8
|
-
# Internal registry: list of (name, callable, n_args)
|
|
9
|
-
_registry = []
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
def xl_func(fn=None, *, name=None):
|
|
13
|
-
"""Decorator — mark a function as an Excel UDF.
|
|
14
|
-
|
|
15
|
-
Usage:
|
|
16
|
-
@xl_func
|
|
17
|
-
def my_func(a, b):
|
|
18
|
-
return a + b
|
|
19
|
-
|
|
20
|
-
@xl_func(name="myCustomName")
|
|
21
|
-
def my_func(a, b):
|
|
22
|
-
return a + b
|
|
23
|
-
"""
|
|
24
|
-
def _register(f):
|
|
25
|
-
excel_name = name if name else f.__name__
|
|
26
|
-
sig = inspect.signature(f)
|
|
27
|
-
n_args = len([
|
|
28
|
-
p for p in sig.parameters.values()
|
|
29
|
-
if p.kind in (
|
|
30
|
-
inspect.Parameter.POSITIONAL_OR_KEYWORD,
|
|
31
|
-
inspect.Parameter.POSITIONAL_ONLY,
|
|
32
|
-
)
|
|
33
|
-
])
|
|
34
|
-
# Check if already registered (e.g. after a hot-reload)
|
|
35
|
-
for i, (n, _, _) in enumerate(_registry):
|
|
36
|
-
if n == excel_name:
|
|
37
|
-
_registry[i] = (excel_name, f, n_args)
|
|
38
|
-
return f
|
|
39
|
-
_registry.append((excel_name, f, n_args))
|
|
40
|
-
return f
|
|
41
|
-
|
|
42
|
-
if fn is not None:
|
|
43
|
-
return _register(fn)
|
|
44
|
-
return _register
|
|
1
|
+
"""@xl_func decorator — marks functions for registration as Excel UDFs.
|
|
2
|
+
|
|
3
|
+
The C runtime scans _registry at xlAutoOpen time to register functions with Excel.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import inspect
|
|
7
|
+
|
|
8
|
+
# Internal registry: list of (name, callable, n_args)
|
|
9
|
+
_registry = []
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def xl_func(fn=None, *, name=None):
|
|
13
|
+
"""Decorator — mark a function as an Excel UDF.
|
|
14
|
+
|
|
15
|
+
Usage:
|
|
16
|
+
@xl_func
|
|
17
|
+
def my_func(a, b):
|
|
18
|
+
return a + b
|
|
19
|
+
|
|
20
|
+
@xl_func(name="myCustomName")
|
|
21
|
+
def my_func(a, b):
|
|
22
|
+
return a + b
|
|
23
|
+
"""
|
|
24
|
+
def _register(f):
|
|
25
|
+
excel_name = name if name else f.__name__
|
|
26
|
+
sig = inspect.signature(f)
|
|
27
|
+
n_args = len([
|
|
28
|
+
p for p in sig.parameters.values()
|
|
29
|
+
if p.kind in (
|
|
30
|
+
inspect.Parameter.POSITIONAL_OR_KEYWORD,
|
|
31
|
+
inspect.Parameter.POSITIONAL_ONLY,
|
|
32
|
+
)
|
|
33
|
+
])
|
|
34
|
+
# Check if already registered (e.g. after a hot-reload)
|
|
35
|
+
for i, (n, _, _) in enumerate(_registry):
|
|
36
|
+
if n == excel_name:
|
|
37
|
+
_registry[i] = (excel_name, f, n_args)
|
|
38
|
+
return f
|
|
39
|
+
_registry.append((excel_name, f, n_args))
|
|
40
|
+
return f
|
|
41
|
+
|
|
42
|
+
if fn is not None:
|
|
43
|
+
return _register(fn)
|
|
44
|
+
return _register
|