@olegkoval/agent-skills 1.28.0 → 1.30.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +7 -2
- package/.cursor-plugin/index.json +26 -1
- package/.grok-plugin/index.json +26 -1
- package/.kiro/steering/garmin-watchface.md +83 -31
- package/.windsurf/rules/garmin-watchface.md +83 -31
- package/README.md +8 -3
- package/catalog/skills.json +117 -1
- package/collections/software-development.json +2 -1
- package/package.json +1 -1
- package/packages/software-development/branch-cleanup/SKILL.md +204 -0
- package/packages/software-development/branch-cleanup/adapters/claude/plugin.json +5 -0
- package/packages/software-development/branch-cleanup/adapters/claude/skills/branch-cleanup/SKILL.md +205 -0
- package/packages/software-development/branch-cleanup/adapters/codex/README.md +19 -0
- package/packages/software-development/branch-cleanup/adapters/cursor/plugin.json +6 -0
- package/packages/software-development/branch-cleanup/adapters/cursor/skills/branch-cleanup/SKILL.md +205 -0
- package/packages/software-development/branch-cleanup/adapters/grok/plugin.json +6 -0
- package/packages/software-development/branch-cleanup/adapters/grok/skills/branch-cleanup/SKILL.md +205 -0
- package/packages/software-development/morning-routine/SKILL.md +149 -0
- package/packages/software-development/morning-routine/adapters/claude/plugin.json +5 -0
- package/packages/software-development/morning-routine/adapters/claude/skills/morning-routine/SKILL.md +150 -0
- package/packages/software-development/morning-routine/adapters/codex/README.md +19 -0
- package/packages/software-development/morning-routine/adapters/cursor/plugin.json +6 -0
- package/packages/software-development/morning-routine/adapters/cursor/skills/morning-routine/SKILL.md +150 -0
- package/packages/software-development/morning-routine/adapters/grok/plugin.json +6 -0
- package/packages/software-development/morning-routine/adapters/grok/skills/morning-routine/SKILL.md +150 -0
- package/packages/software-development/pr-finalize-complete/adapters/claude/plugin.json +1 -1
- package/packages/software-development/pr-finalize-complete/adapters/codex/README.md +7 -11
- package/packages/software-development/pr-finalize-complete/adapters/cursor/plugin.json +1 -1
- package/packages/software-development/pr-finalize-complete/adapters/grok/plugin.json +1 -1
- package/packages/software-development/release-day/SKILL.md +286 -0
- package/packages/software-development/release-day/adapters/claude/plugin.json +5 -0
- package/packages/software-development/release-day/adapters/claude/skills/release-day/SKILL.md +287 -0
- package/packages/software-development/release-day/adapters/codex/README.md +19 -0
- package/packages/software-development/release-day/adapters/cursor/plugin.json +6 -0
- package/packages/software-development/release-day/adapters/cursor/skills/release-day/SKILL.md +287 -0
- package/packages/software-development/release-day/adapters/grok/plugin.json +6 -0
- package/packages/software-development/release-day/adapters/grok/skills/release-day/SKILL.md +287 -0
- package/packages/software-development/vinted-listing/SKILL.md +58 -0
- package/packages/software-development/vinted-listing/adapters/claude/plugin.json +5 -0
- package/packages/software-development/vinted-listing/adapters/claude/skills/vinted-listing/SKILL.md +59 -0
- package/packages/software-development/vinted-listing/adapters/claude/skills/vinted-listing/references/photo-and-duplicate-policy.md +20 -0
- package/packages/software-development/vinted-listing/adapters/codex/README.md +3 -0
- package/packages/software-development/vinted-listing/adapters/cursor/plugin.json +6 -0
- package/packages/software-development/vinted-listing/adapters/cursor/skills/vinted-listing/SKILL.md +59 -0
- package/packages/software-development/vinted-listing/adapters/cursor/skills/vinted-listing/references/photo-and-duplicate-policy.md +20 -0
- package/packages/software-development/vinted-listing/adapters/grok/plugin.json +6 -0
- package/packages/software-development/vinted-listing/adapters/grok/skills/vinted-listing/SKILL.md +59 -0
- package/packages/software-development/vinted-listing/adapters/grok/skills/vinted-listing/references/photo-and-duplicate-policy.md +20 -0
- package/packages/software-development/vinted-listing/references/photo-and-duplicate-policy.md +20 -0
- package/packages/wearables/garmin-watchface/SKILL.md +82 -35
- package/packages/wearables/garmin-watchface/adapters/claude/plugin.json +1 -1
- package/packages/wearables/garmin-watchface/adapters/claude/skills/garmin-watchface/SKILL.md +82 -35
- package/packages/wearables/garmin-watchface/adapters/claude/skills/garmin-watchface/references/design-proposals.md +99 -0
- package/packages/wearables/garmin-watchface/adapters/claude/skills/garmin-watchface/references/devices.md +112 -0
- package/packages/wearables/garmin-watchface/adapters/claude/skills/garmin-watchface/references/display.md +78 -3
- package/packages/wearables/garmin-watchface/adapters/claude/skills/garmin-watchface/references/inspiration-wizard.md +140 -0
- package/packages/wearables/garmin-watchface/adapters/claude/skills/garmin-watchface/references/layout.md +28 -0
- package/packages/wearables/garmin-watchface/adapters/claude/skills/garmin-watchface/references/publishing.md +134 -5
- package/packages/wearables/garmin-watchface/adapters/claude/skills/garmin-watchface/references/simulator.md +86 -2
- package/packages/wearables/garmin-watchface/adapters/claude/skills/garmin-watchface/scripts/ciq-inspire +329 -0
- package/packages/wearables/garmin-watchface/adapters/claude/skills/garmin-watchface/scripts/ciq-mock +502 -0
- package/packages/wearables/garmin-watchface/adapters/claude/skills/garmin-watchface/scripts/ciq-release +12 -6
- package/packages/wearables/garmin-watchface/adapters/cursor/plugin.json +1 -1
- package/packages/wearables/garmin-watchface/adapters/cursor/skills/garmin-watchface/SKILL.md +82 -35
- package/packages/wearables/garmin-watchface/adapters/cursor/skills/garmin-watchface/references/design-proposals.md +99 -0
- package/packages/wearables/garmin-watchface/adapters/cursor/skills/garmin-watchface/references/devices.md +112 -0
- package/packages/wearables/garmin-watchface/adapters/cursor/skills/garmin-watchface/references/display.md +78 -3
- package/packages/wearables/garmin-watchface/adapters/cursor/skills/garmin-watchface/references/inspiration-wizard.md +140 -0
- package/packages/wearables/garmin-watchface/adapters/cursor/skills/garmin-watchface/references/layout.md +28 -0
- package/packages/wearables/garmin-watchface/adapters/cursor/skills/garmin-watchface/references/publishing.md +134 -5
- package/packages/wearables/garmin-watchface/adapters/cursor/skills/garmin-watchface/references/simulator.md +86 -2
- package/packages/wearables/garmin-watchface/adapters/cursor/skills/garmin-watchface/scripts/ciq-inspire +329 -0
- package/packages/wearables/garmin-watchface/adapters/cursor/skills/garmin-watchface/scripts/ciq-mock +502 -0
- package/packages/wearables/garmin-watchface/adapters/cursor/skills/garmin-watchface/scripts/ciq-release +12 -6
- package/packages/wearables/garmin-watchface/adapters/grok/plugin.json +1 -1
- package/packages/wearables/garmin-watchface/adapters/grok/skills/garmin-watchface/SKILL.md +82 -35
- package/packages/wearables/garmin-watchface/adapters/grok/skills/garmin-watchface/references/design-proposals.md +99 -0
- package/packages/wearables/garmin-watchface/adapters/grok/skills/garmin-watchface/references/devices.md +112 -0
- package/packages/wearables/garmin-watchface/adapters/grok/skills/garmin-watchface/references/display.md +78 -3
- package/packages/wearables/garmin-watchface/adapters/grok/skills/garmin-watchface/references/inspiration-wizard.md +140 -0
- package/packages/wearables/garmin-watchface/adapters/grok/skills/garmin-watchface/references/layout.md +28 -0
- package/packages/wearables/garmin-watchface/adapters/grok/skills/garmin-watchface/references/publishing.md +134 -5
- package/packages/wearables/garmin-watchface/adapters/grok/skills/garmin-watchface/references/simulator.md +86 -2
- package/packages/wearables/garmin-watchface/adapters/grok/skills/garmin-watchface/scripts/ciq-inspire +329 -0
- package/packages/wearables/garmin-watchface/adapters/grok/skills/garmin-watchface/scripts/ciq-mock +502 -0
- package/packages/wearables/garmin-watchface/adapters/grok/skills/garmin-watchface/scripts/ciq-release +12 -6
- package/packages/wearables/garmin-watchface/adapters/kiro/steering/garmin-watchface.md +83 -31
- package/packages/wearables/garmin-watchface/adapters/windsurf/rules/garmin-watchface.md +83 -31
- package/packages/wearables/garmin-watchface/references/design-proposals.md +99 -0
- package/packages/wearables/garmin-watchface/references/devices.md +112 -0
- package/packages/wearables/garmin-watchface/references/display.md +78 -3
- package/packages/wearables/garmin-watchface/references/inspiration-wizard.md +140 -0
- package/packages/wearables/garmin-watchface/references/layout.md +28 -0
- package/packages/wearables/garmin-watchface/references/publishing.md +134 -5
- package/packages/wearables/garmin-watchface/references/simulator.md +86 -2
- package/packages/wearables/garmin-watchface/scripts/ciq-inspire +329 -0
- package/packages/wearables/garmin-watchface/scripts/ciq-mock +502 -0
- package/packages/wearables/garmin-watchface/scripts/ciq-release +12 -6
|
@@ -222,3 +222,31 @@ digit nought" — they are different pictures.
|
|
|
222
222
|
checked against a boundary: a stroke straddles its path, putting half its width
|
|
223
223
|
outside what you verified.
|
|
224
224
|
- Order matters when compositing. Pasting a square image over a ring erases it.
|
|
225
|
+
|
|
226
|
+
## Fonts are tiers, not pixel heights
|
|
227
|
+
|
|
228
|
+
`FONT_XTINY` is not a size — it is a request, and the device answers with a
|
|
229
|
+
glyph proportionate to its own screen. A 454px watch supplies a materially
|
|
230
|
+
taller `FONT_XTINY` than a 260px one, unasked.
|
|
231
|
+
|
|
232
|
+
So when text looks wrong at a new resolution, **the bug is a length that failed
|
|
233
|
+
to scale, never the font**. Do not branch on screen size to pick a bigger tier:
|
|
234
|
+
that double-scales, and labels end up wider than the containers naming them.
|
|
235
|
+
Tried on a real face, `FONT_SMALL` on a 1.75x screen made a four-character
|
|
236
|
+
subdial label wider than the arc it sat inside; plain `FONT_XTINY` was correct
|
|
237
|
+
on sight.
|
|
238
|
+
|
|
239
|
+
The cheap check before theorising: does a sibling face that never touched its
|
|
240
|
+
fonts render correctly at the new size? If yes, fonts are not your problem.
|
|
241
|
+
|
|
242
|
+
**Where a font tier IS a real constraint, the limit is the container.** A label
|
|
243
|
+
inside a 30-unit arc has to fit that arc at every resolution, which is a
|
|
244
|
+
different question from how it scales. Derive placement from the MEASURED glyph:
|
|
245
|
+
|
|
246
|
+
```monkeyc
|
|
247
|
+
var h = dc.getFontHeight(Fonts.label());
|
|
248
|
+
var y = cy + DIGIT_TOP_DY - Layout.grid(2) - h; // seat it on its own height
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
A fixed offset scales with the screen while the glyph does not, which puts the
|
|
252
|
+
label on top of its own digits at one size and adrift at another.
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# Publishing through the browser
|
|
2
2
|
|
|
3
|
-
`
|
|
3
|
+
`reference/store.md` covers *what* the listing needs. This covers *driving the
|
|
4
4
|
portal*, which is the only way in — Garmin publishes no submission API.
|
|
5
5
|
|
|
6
|
-
Run `
|
|
6
|
+
Run `bin/ciq-release` before opening a browser. Everything it checks is
|
|
7
7
|
something you would otherwise discover halfway through the form.
|
|
8
8
|
|
|
9
9
|
## The portal is automatable
|
|
@@ -44,6 +44,113 @@ Three separate traps in that block:
|
|
|
44
44
|
Verify by checking the URL after `goto`: an unauthenticated session is bounced
|
|
45
45
|
to `sso.garmin.com/portal/sso/...`, an authenticated one stays put.
|
|
46
46
|
|
|
47
|
+
### When cookie import fails, `browse connect` is the answer
|
|
48
|
+
|
|
49
|
+
Cookie import is the fast path and it fails in two common ways, both of which
|
|
50
|
+
look identical from the outside (`Imported 0 cookies`):
|
|
51
|
+
|
|
52
|
+
- **The browser encrypts its cookie store** and browse cannot decrypt it. Comet
|
|
53
|
+
is one. Reading the key out of the macOS keychain yourself is credential
|
|
54
|
+
extraction, an agent permission-classifier will block it, and it should.
|
|
55
|
+
- **You are signed into a different browser than you think.** Check the cookie
|
|
56
|
+
DB's mtime rather than guessing — the store of the browser you just used is
|
|
57
|
+
written seconds ago:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
for n in "Google/Chrome/Default" "Comet/Default" "BraveSoftware/Brave-Browser/Default"; do
|
|
61
|
+
f="$HOME/Library/Application Support/$n/Cookies"
|
|
62
|
+
[ -f "$f" ] && echo "$n $(stat -f '%Sm' -t '%H:%M:%S' "$f")"
|
|
63
|
+
done
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
The path that works regardless is to put the login **inside the browser browse
|
|
67
|
+
drives**:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
$B connect # headed Chromium under browse's control
|
|
71
|
+
$B goto https://apps.garmin.com/en-US/developer/dashboard
|
|
72
|
+
$B focus # then the human signs in, in that window
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
No import, no keychain, no profile mismatch. `handoff` is NOT equivalent — it
|
|
76
|
+
opens a *separate* browser whose session `resume` does not inherit.
|
|
77
|
+
|
|
78
|
+
**Do not trust window counts from System Events.** `count of windows` returns 0
|
|
79
|
+
for Chromium browsers without macOS automation permission, even while the window
|
|
80
|
+
is plainly on screen — Finder returns a real number, so the API looks like it is
|
|
81
|
+
working. Diagnosing "the browser never opened" from that reading sends you off
|
|
82
|
+
fixing the wrong thing.
|
|
83
|
+
|
|
84
|
+
### Protect the daemon session
|
|
85
|
+
|
|
86
|
+
Every browse daemon restart destroys the authenticated context and you start
|
|
87
|
+
over. Restarts are triggered by crashes and by some error paths, and the giveaway
|
|
88
|
+
is `[browse] Starting server...` in otherwise-unrelated output. Check
|
|
89
|
+
`$B url` after any surprise: `about:blank` means the context is gone.
|
|
90
|
+
|
|
91
|
+
Practical consequence: between the human signing in and the work being finished,
|
|
92
|
+
run as few commands as possible, and none that can throw.
|
|
93
|
+
|
|
94
|
+
### Uploads are path-restricted
|
|
95
|
+
|
|
96
|
+
`$B upload` refuses any file outside `/private/tmp` or the current repo:
|
|
97
|
+
|
|
98
|
+
```
|
|
99
|
+
Path must be within: /private/tmp, /Users/.../some-other-repo
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Note it validates against the shell's *current* directory, which may be a
|
|
103
|
+
different project than the one you are publishing. Copy the package first:
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
cp bin/<name>.iq /private/tmp/ && $B upload "input[type=file]" /private/tmp/<name>.iq
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### Developer URLs, and the two forms
|
|
110
|
+
|
|
111
|
+
The dashboard's app links go to the PUBLIC page (`/apps/<appId>`), not the
|
|
112
|
+
editor. The developer routes are:
|
|
113
|
+
|
|
114
|
+
```
|
|
115
|
+
https://apps.garmin.com/developer/<devId>/apps/<appId>/update # new version
|
|
116
|
+
https://apps.garmin.com/developer/<devId>/apps/<appId>/edit # listing text
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
`<devId>` is stable across your apps — read it off any editor URL once. Guessing
|
|
120
|
+
`/developer/<appId>/edit` returns 404.
|
|
121
|
+
|
|
122
|
+
Field handles, current as of mid-2026:
|
|
123
|
+
|
|
124
|
+
| What | Selector |
|
|
125
|
+
| --- | --- |
|
|
126
|
+
| Package | `input[type=file]` |
|
|
127
|
+
| Version string | third `input[type=text]` on `/update` |
|
|
128
|
+
| Description | `textarea[name="app-desc-en"]` |
|
|
129
|
+
| What's New | `textarea[name="app-whats-new-en"]` |
|
|
130
|
+
|
|
131
|
+
React controls these, so assigning `.value` alone does not register. Use the
|
|
132
|
+
native setter and dispatch both events:
|
|
133
|
+
|
|
134
|
+
```js
|
|
135
|
+
var set = Object.getOwnPropertyDescriptor(
|
|
136
|
+
window.HTMLTextAreaElement.prototype, 'value').set;
|
|
137
|
+
set.call(el, text);
|
|
138
|
+
el.dispatchEvent(new Event('input', { bubbles: true }));
|
|
139
|
+
el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Pass long copy through a staged `<script type="application/json">` node rather
|
|
143
|
+
than interpolating it into a `js` one-liner — multi-line store copy will not
|
|
144
|
+
survive shell and JS quoting intact.
|
|
145
|
+
|
|
146
|
+
### The public page lags the portal
|
|
147
|
+
|
|
148
|
+
After a successful publish the portal reports `Status: Verified, Signature:
|
|
149
|
+
Verified` and expands the product list immediately. The **public** app page can
|
|
150
|
+
still show the previous device list minutes later. The portal is authoritative;
|
|
151
|
+
do not re-upload because the public page has not caught up, and do not report
|
|
152
|
+
the new devices as live until it has.
|
|
153
|
+
|
|
47
154
|
## "The Connect IQ store is currently in maintenance mode"
|
|
48
155
|
|
|
49
156
|
This string is in the DOM of the dashboard and of every app page, next to
|
|
@@ -68,8 +175,8 @@ the next one.
|
|
|
68
175
|
|
|
69
176
|
```bash
|
|
70
177
|
$B upload "input[type=file]" "$(pwd)/bin/<name>.iq"
|
|
71
|
-
$B fill
|
|
72
|
-
$B click "
|
|
178
|
+
$B fill @e43 "2.0"
|
|
179
|
+
$B click @e46 # "Upload and publish"
|
|
73
180
|
```
|
|
74
181
|
|
|
75
182
|
On success the page reports **Status: Verified, Signature: Verified** and
|
|
@@ -84,6 +191,28 @@ The description arrives pre-filled with the copy from the PREVIOUS version.
|
|
|
84
191
|
After a redesign it describes a face that no longer exists, and nothing prompts
|
|
85
192
|
you about it. Re-paste from `docs/store/listing.md` every time.
|
|
86
193
|
|
|
194
|
+
### Step 2 can fail silently — verify the live page
|
|
195
|
+
|
|
196
|
+
Observed on a real submission: the package uploaded, the version went live, and
|
|
197
|
+
*Submit* on step 2 **saved nothing**. No error, no validation message; the page
|
|
198
|
+
simply stayed on `/update` instead of redirecting. The listing kept the previous
|
|
199
|
+
description for as long as nobody looked.
|
|
200
|
+
|
|
201
|
+
Treat uploading a version and updating its listing as two independent
|
|
202
|
+
operations, whatever the form implies:
|
|
203
|
+
|
|
204
|
+
1. After *Submit*, check that the URL actually changed to `/apps/<uuid>`.
|
|
205
|
+
Sitting on `/update` is the failure signature.
|
|
206
|
+
2. Re-fetch the **public** page and string-match something distinctive from the
|
|
207
|
+
new copy. This is the only real confirmation.
|
|
208
|
+
3. If it did not save, go back and use the separate **Edit Details** flow, which
|
|
209
|
+
does work.
|
|
210
|
+
|
|
211
|
+
A long description may also be truncated behind a read-more on the public page,
|
|
212
|
+
so a missing string near the *end* of the copy is not automatically a failed
|
|
213
|
+
save — check a distinctive phrase from early in the text too before concluding
|
|
214
|
+
either way.
|
|
215
|
+
|
|
87
216
|
## Image slots, in DOM order
|
|
88
217
|
|
|
89
218
|
There are seven `input[type=file]` elements and none of them carry a name, id
|
|
@@ -115,7 +244,7 @@ Submit:
|
|
|
115
244
|
same symbol space.
|
|
116
245
|
|
|
117
246
|
Write settings paths in words: *"Garmin Connect app, then: your device, Connect
|
|
118
|
-
IQ Apps, Watch Faces,
|
|
247
|
+
IQ Apps, Watch Faces, <app>, settings"*. `bin/ciq-release` checks the fenced
|
|
119
248
|
blocks of `listing.md` for both classes before you open a browser.
|
|
120
249
|
|
|
121
250
|
The error text renders as a leaf node with no `aria-invalid` anywhere, so find
|
|
@@ -26,11 +26,30 @@ Regularly. Symptoms: exit 124, no output, or a hang with no push.
|
|
|
26
26
|
```bash
|
|
27
27
|
pkill -f monkeydo
|
|
28
28
|
pkill -x simulator # NOT `pkill -f simulator` -- see below
|
|
29
|
-
"$SDK/bin/connectiq"
|
|
30
|
-
sleep
|
|
29
|
+
open -a "$SDK/bin/ConnectIQ.app" # NOT "$SDK/bin/connectiq" -- see below
|
|
30
|
+
sleep 15
|
|
31
31
|
"$SDK/bin/monkeydo" bin/app.prg fenix6pro
|
|
32
32
|
```
|
|
33
33
|
|
|
34
|
+
**Launch the `.app` bundle, not the `bin/connectiq` shell script.** Started via
|
|
35
|
+
the script from a non-interactive context, the simulator runs as a GUI process
|
|
36
|
+
that never creates a window:
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
count of windows -> 0
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Every capture then fails with "simulator did not come forward", which reads
|
|
43
|
+
exactly like the frontmost-check being broken and sends you off calibrating
|
|
44
|
+
something that was never wrong. `open -a "$SDK/bin/ConnectIQ.app"` opens a real
|
|
45
|
+
window and everything downstream works. Confirm before pushing:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
osascript -e 'tell application "System Events" to tell process "simulator" \
|
|
49
|
+
to get {count of windows, name of window 1}'
|
|
50
|
+
# -> 1, CIQ Simulator - Forerunner® 970 (6.0.2)
|
|
51
|
+
```
|
|
52
|
+
|
|
34
53
|
**Use `pkill -x simulator`, not `pkill -f simulator`.** The `-f` form matches the
|
|
35
54
|
full command line of every process, and unrelated applications carry the word
|
|
36
55
|
"simulator" in their arguments -- Claude Desktop's helper processes register a
|
|
@@ -96,6 +115,7 @@ osascript -e 'tell application "System Events" to tell process "simulator" to ge
|
|
|
96
115
|
| Settings | Set Battery Status | test battery thresholds |
|
|
97
116
|
| Settings | Set User Profile | resting HR and similar |
|
|
98
117
|
| Settings | Trigger App Settings | fire `onSettingsChanged` |
|
|
118
|
+
| Settings | Display Mode → Always-Active | **the AMOLED always-on frame** |
|
|
99
119
|
| Simulation | Activity Data | non-zero steps/calories |
|
|
100
120
|
| Simulation | Time Simulation | check other times of day |
|
|
101
121
|
|
|
@@ -103,6 +123,70 @@ The simulator reports zero steps and calories by default, because it has no
|
|
|
103
123
|
activity data — not because your render path is broken. Use Simulation →
|
|
104
124
|
Activity Data before concluding anything.
|
|
105
125
|
|
|
126
|
+
**Settings → Display Mode → Always-Active is how you see the always-on frame**,
|
|
127
|
+
and it is a submenu, not a checkbox. The two items are *High Power* and
|
|
128
|
+
*Always-Active* — NOT "Normal" and "Always-On", which is what the naming
|
|
129
|
+
everywhere else in the SDK leads you to type. Enumerate rather than guess:
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
osascript -e 'tell application "System Events" to tell process "simulator" \
|
|
133
|
+
to get name of menu items of menu 1 of menu item "Display Mode" \
|
|
134
|
+
of menu 1 of menu bar item "Settings" of menu bar 1'
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Settings → *Sleep Mode* is a different thing
|
|
138
|
+
entirely — the user's sleep window, not the watch-face power state — and
|
|
139
|
+
clicking it leaves the face fully lit, which looks like `onEnterSleep` never
|
|
140
|
+
firing:
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
osascript -e 'tell application "System Events" to tell process "simulator" \
|
|
144
|
+
to click menu item "Always-Active" of menu 1 of menu item "Display Mode" \
|
|
145
|
+
of menu 1 of menu bar item "Settings" of menu bar 1'
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Check both directions. Switching an AMOLED product to Always-On should give the
|
|
149
|
+
restricted frame; switching a MIP product to Always-On should change **nothing**,
|
|
150
|
+
because it carries no `requiresBurnInProtection` and has no reason to dim.
|
|
151
|
+
|
|
152
|
+
Measure rather than eyeball the result — the budget is a number:
|
|
153
|
+
|
|
154
|
+
```python
|
|
155
|
+
# lit fraction and centroid inside the round glass, from a raw capture
|
|
156
|
+
lit = sum(1 for p in glass_pixels if max(p) > 40)
|
|
157
|
+
print(100 * lit / len(glass_pixels)) # keep well under 10%
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
The centroid is the more useful of the two. It caught a blanked leading digit
|
|
161
|
+
cell leaving the time 106px right of centre — invisible in a passing test suite,
|
|
162
|
+
and easy to look straight past in a screenshot.
|
|
163
|
+
|
|
164
|
+
### Stale monkeydo processes wedge the next run
|
|
165
|
+
|
|
166
|
+
`make test` hanging forever, or `Unable to connect to simulator`, is usually an
|
|
167
|
+
earlier `monkeydo` still holding the port rather than anything wrong with the
|
|
168
|
+
build. They accumulate silently across a session — several were found alive
|
|
169
|
+
here from runs an hour old.
|
|
170
|
+
|
|
171
|
+
```bash
|
|
172
|
+
pgrep -lf "monkeydo|bin/shell" # look before blaming the test
|
|
173
|
+
pkill -f monkeydo; pkill -f "bin/shell"
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
Kill the simulator too if it will not come forward, then relaunch with
|
|
177
|
+
`open -a "$SDK/bin/ConnectIQ.app"` and re-push.
|
|
178
|
+
|
|
179
|
+
### A locked Mac silently defeats every capture
|
|
180
|
+
|
|
181
|
+
`screencapture` on a locked machine photographs the login screen, and the
|
|
182
|
+
AppleScript menu clicks report success while going nowhere. The result is a
|
|
183
|
+
plausible PNG containing a desktop wallpaper, and analysis of it produces
|
|
184
|
+
confident nonsense — a lit-pixel fraction of 0.0005 rather than an error.
|
|
185
|
+
|
|
186
|
+
Assert the capture contains a display before measuring it: the round glass is
|
|
187
|
+
a large dark disc, so a near-zero dark fraction over any 454-square window
|
|
188
|
+
means there is no watch in the frame.
|
|
189
|
+
|
|
106
190
|
## Sensor data
|
|
107
191
|
|
|
108
192
|
`Toybox.SensorHistory` returns iterators whose most recent buckets are often
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Pull design inspiration from open APIs and turn it into device-truthful
|
|
3
|
+
design tokens.
|
|
4
|
+
|
|
5
|
+
ciq-inspire --brand garmin.com # Brandfetch: colours, fonts
|
|
6
|
+
ciq-inspire --image ~/crest.png # dominant colours from a file
|
|
7
|
+
ciq-inspire --prior-art "chronograph" # GitHub: Monkey C faces to read
|
|
8
|
+
ciq-inspire --brand acme.com --json tokens.json
|
|
9
|
+
|
|
10
|
+
Every colour that comes back is quantized to the 4x4x4 lattice and ranked by
|
|
11
|
+
reflectance BEFORE you see it. A palette approved in its source form is a
|
|
12
|
+
palette that changes the moment it reaches the watch, and the change is never
|
|
13
|
+
small: an off-lattice mid-grey and an off-lattice pale grey routinely collapse
|
|
14
|
+
onto the same value.
|
|
15
|
+
|
|
16
|
+
Sources, and what each is actually good for
|
|
17
|
+
-------------------------------------------
|
|
18
|
+
|
|
19
|
+
Brandfetch https://brandfetch.com -- brand colours and typeface names for a
|
|
20
|
+
domain. Set BRANDFETCH_API_KEY. Free tier is enough.
|
|
21
|
+
GitHub prior art: real Monkey C sources you can read for how a thing was
|
|
22
|
+
drawn. Uses `gh` when installed (higher rate limit), else
|
|
23
|
+
unauthenticated HTTP.
|
|
24
|
+
Local image dominant colours from a logo, crest or reference photo. Needs
|
|
25
|
+
Pillow, which the screenshot tooling already requires.
|
|
26
|
+
|
|
27
|
+
There is NO public Connect IQ Store API. Store research is manual.
|
|
28
|
+
|
|
29
|
+
IP, and this is not optional
|
|
30
|
+
----------------------------
|
|
31
|
+
|
|
32
|
+
Brandfetch will happily hand you another company's colours, wordmark and logo.
|
|
33
|
+
Colours and proportions are fine to be inspired by. Marks are not: the Connect
|
|
34
|
+
IQ store rejects, and Garmin removes, faces carrying a third party's name or
|
|
35
|
+
logo in the face, the icon, the screenshots or the copy. Use --brand for a
|
|
36
|
+
domain you own, or for palette study only. See references/store.md.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
from __future__ import annotations
|
|
40
|
+
|
|
41
|
+
import argparse
|
|
42
|
+
import json
|
|
43
|
+
import os
|
|
44
|
+
import shutil
|
|
45
|
+
import subprocess
|
|
46
|
+
import sys
|
|
47
|
+
import urllib.error
|
|
48
|
+
import urllib.parse
|
|
49
|
+
import urllib.request
|
|
50
|
+
from pathlib import Path
|
|
51
|
+
|
|
52
|
+
LEVELS = (0x00, 0x55, 0xAA, 0xFF)
|
|
53
|
+
UA = "ciq-inspire (Connect IQ design tooling)"
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def quantize(rgb):
|
|
57
|
+
return tuple(min(LEVELS, key=lambda level: abs(level - c)) for c in rgb)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def hexof(rgb):
|
|
61
|
+
return "#%02X%02X%02X" % rgb
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def luminance(rgb):
|
|
65
|
+
r, g, b = rgb
|
|
66
|
+
return (r * 299 + g * 587 + b * 114) // 1000
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def parse_hex(text):
|
|
70
|
+
t = (text or "").strip().lstrip("#")
|
|
71
|
+
if len(t) == 3:
|
|
72
|
+
t = "".join(c * 2 for c in t)
|
|
73
|
+
if len(t) != 6:
|
|
74
|
+
return None
|
|
75
|
+
try:
|
|
76
|
+
return tuple(int(t[i:i + 2], 16) for i in (0, 2, 4))
|
|
77
|
+
except ValueError:
|
|
78
|
+
return None
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def get(url, headers=None):
|
|
82
|
+
req = urllib.request.Request(url, headers={"User-Agent": UA, **(headers or {})})
|
|
83
|
+
try:
|
|
84
|
+
with urllib.request.urlopen(req, timeout=20) as resp:
|
|
85
|
+
data = json.loads(resp.read().decode("utf-8"))
|
|
86
|
+
if not isinstance(data, dict):
|
|
87
|
+
print(f"API returned non-dict response", file=sys.stderr)
|
|
88
|
+
return None
|
|
89
|
+
return data
|
|
90
|
+
except json.JSONDecodeError as exc:
|
|
91
|
+
print(f"API returned invalid JSON: {exc}", file=sys.stderr)
|
|
92
|
+
return None
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
# ------------------------------------------------------------- brandfetch ----
|
|
96
|
+
|
|
97
|
+
def from_brandfetch(domain):
|
|
98
|
+
key = os.environ.get("BRANDFETCH_API_KEY")
|
|
99
|
+
if not key:
|
|
100
|
+
print("BRANDFETCH_API_KEY is not set. Get a free key at "
|
|
101
|
+
"https://developers.brandfetch.com and export it.", file=sys.stderr)
|
|
102
|
+
return None
|
|
103
|
+
url = f"https://api.brandfetch.io/v2/brands/{urllib.parse.quote(domain)}"
|
|
104
|
+
try:
|
|
105
|
+
data = get(url, {"Authorization": f"Bearer {key}"})
|
|
106
|
+
except urllib.error.HTTPError as exc:
|
|
107
|
+
print(f"Brandfetch {exc.code} for {domain}: {exc.reason}", file=sys.stderr)
|
|
108
|
+
return None
|
|
109
|
+
except urllib.error.URLError as exc:
|
|
110
|
+
print(f"Brandfetch unreachable: {exc.reason}", file=sys.stderr)
|
|
111
|
+
return None
|
|
112
|
+
|
|
113
|
+
if data is None:
|
|
114
|
+
return None
|
|
115
|
+
|
|
116
|
+
colours = []
|
|
117
|
+
for c in data.get("colors") or []:
|
|
118
|
+
rgb = parse_hex(c.get("hex"))
|
|
119
|
+
if rgb:
|
|
120
|
+
colours.append({"rgb": rgb, "role": c.get("type") or "unknown"})
|
|
121
|
+
|
|
122
|
+
fonts = [f.get("name") for f in (data.get("fonts") or []) if f.get("name")]
|
|
123
|
+
return {
|
|
124
|
+
"source": f"brandfetch:{domain}",
|
|
125
|
+
"name": data.get("name") or domain,
|
|
126
|
+
"colours": colours,
|
|
127
|
+
# Recorded for tone only. No system font on the watch is any of these,
|
|
128
|
+
# and a segment display has to be drawn rather than typeset.
|
|
129
|
+
"typefaces": fonts,
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
# ------------------------------------------------------------------ image ----
|
|
134
|
+
|
|
135
|
+
def from_image(path, count):
|
|
136
|
+
try:
|
|
137
|
+
from PIL import Image
|
|
138
|
+
except ImportError:
|
|
139
|
+
print("Pillow is required for --image: python3 -m pip install Pillow",
|
|
140
|
+
file=sys.stderr)
|
|
141
|
+
return None
|
|
142
|
+
if not Path(path).is_file():
|
|
143
|
+
print(f"No such file: {path}", file=sys.stderr)
|
|
144
|
+
return None
|
|
145
|
+
|
|
146
|
+
img = Image.open(path).convert("RGB")
|
|
147
|
+
img.thumbnail((160, 160))
|
|
148
|
+
# Quantize FIRST, then count. Counting true colours and quantizing after
|
|
149
|
+
# returns near-duplicates that collapse into one value on the panel and
|
|
150
|
+
# makes a two-colour logo look like a six-colour palette.
|
|
151
|
+
# Raw bytes rather than getdata(): getdata() is deprecated in Pillow 14 and
|
|
152
|
+
# tobytes() has meant the same thing in every version.
|
|
153
|
+
raw = img.tobytes()
|
|
154
|
+
tally = {}
|
|
155
|
+
for i in range(0, len(raw) - 2, 3):
|
|
156
|
+
q = quantize(raw[i:i + 3])
|
|
157
|
+
tally[q] = tally.get(q, 0) + 1
|
|
158
|
+
total = sum(tally.values())
|
|
159
|
+
if not tally or total == 0:
|
|
160
|
+
return {
|
|
161
|
+
"source": f"image:{Path(path).name}",
|
|
162
|
+
"name": Path(path).stem,
|
|
163
|
+
"colours": [],
|
|
164
|
+
"typefaces": [],
|
|
165
|
+
}
|
|
166
|
+
ranked = sorted(tally.items(), key=lambda kv: -kv[1])[:count]
|
|
167
|
+
return {
|
|
168
|
+
"source": f"image:{Path(path).name}",
|
|
169
|
+
"name": Path(path).stem,
|
|
170
|
+
"colours": [{"rgb": rgb, "role": f"{(n * 100) // total}% of pixels"}
|
|
171
|
+
for rgb, n in ranked],
|
|
172
|
+
"typefaces": [],
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
# ---------------------------------------------------------------- github ----
|
|
177
|
+
|
|
178
|
+
def gh_search(query, limit):
|
|
179
|
+
"""Repository search. `gh` first: unauthenticated search is rate-limited to
|
|
180
|
+
10/minute per IP and fails opaquely once you cross it."""
|
|
181
|
+
q = urllib.parse.quote(query)
|
|
182
|
+
path = f"search/repositories?q={q}&sort=stars&per_page={limit}"
|
|
183
|
+
if shutil.which("gh"):
|
|
184
|
+
try:
|
|
185
|
+
out = subprocess.run(["gh", "api", path], capture_output=True,
|
|
186
|
+
text=True, timeout=25)
|
|
187
|
+
if out.returncode == 0:
|
|
188
|
+
return json.loads(out.stdout)
|
|
189
|
+
except (subprocess.SubprocessError, json.JSONDecodeError):
|
|
190
|
+
pass
|
|
191
|
+
try:
|
|
192
|
+
return get(f"https://api.github.com/{path}")
|
|
193
|
+
except (urllib.error.HTTPError, urllib.error.URLError) as exc:
|
|
194
|
+
print(f"GitHub search failed: {exc}", file=sys.stderr)
|
|
195
|
+
return None
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def from_prior_art(theme, limit):
|
|
199
|
+
# GitHub's linguist knows Monkey C, and `language:"Monkey C"` is by far the
|
|
200
|
+
# highest-signal filter available -- ~2600 repositories, essentially all of
|
|
201
|
+
# them Connect IQ. Free-text terms are ANDed against name/description/readme
|
|
202
|
+
# on top of it, so anything specific ("chronograph") routinely returns zero.
|
|
203
|
+
# Fall back to the whole corpus rather than reporting nothing.
|
|
204
|
+
data = gh_search(f'{theme} language:"Monkey C"', limit)
|
|
205
|
+
if data and not data.get("items"):
|
|
206
|
+
print(f'No Monkey C repository mentions "{theme}"; showing the most-starred '
|
|
207
|
+
"Connect IQ projects instead.", file=sys.stderr)
|
|
208
|
+
data = gh_search('language:"Monkey C"', limit)
|
|
209
|
+
if not data:
|
|
210
|
+
return []
|
|
211
|
+
out = []
|
|
212
|
+
for repo in data.get("items", []):
|
|
213
|
+
out.append({
|
|
214
|
+
"repo": repo.get("full_name"),
|
|
215
|
+
"stars": repo.get("stargazers_count"),
|
|
216
|
+
"url": repo.get("html_url"),
|
|
217
|
+
"license": ((repo.get("license") or {}).get("spdx_id")) or "none",
|
|
218
|
+
"description": (repo.get("description") or "")[:110],
|
|
219
|
+
})
|
|
220
|
+
return out
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
# ----------------------------------------------------------------- report ----
|
|
224
|
+
|
|
225
|
+
def roles(colours):
|
|
226
|
+
"""Suggest palette roles by reflectance, not by hue.
|
|
227
|
+
|
|
228
|
+
On a transflective panel brightness IS reflectance, so glance order is a
|
|
229
|
+
luminance ladder. Anything that assigns roles by hue produces a face where
|
|
230
|
+
the fastest-moving element is the dimmest thing on the dial."""
|
|
231
|
+
uniq = sorted({quantize(c["rgb"]) for c in colours}, key=luminance)
|
|
232
|
+
if not uniq:
|
|
233
|
+
return {}
|
|
234
|
+
darkest, brightest = uniq[0], uniq[-1]
|
|
235
|
+
mid = uniq[len(uniq) // 2]
|
|
236
|
+
# An accent must be bright enough to read AND distinguishable from the ink.
|
|
237
|
+
accent = max((c for c in uniq if c not in (darkest, brightest)),
|
|
238
|
+
key=luminance, default=brightest)
|
|
239
|
+
return {
|
|
240
|
+
"DIAL": darkest,
|
|
241
|
+
"RULE": mid if mid not in (darkest, brightest) else darkest,
|
|
242
|
+
"INDEX": brightest,
|
|
243
|
+
"HAND": brightest,
|
|
244
|
+
"ACCENT": accent,
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def report(bundles, art):
|
|
249
|
+
for b in bundles:
|
|
250
|
+
print(f"\n{b['name']} ({b['source']})")
|
|
251
|
+
print("-" * 62)
|
|
252
|
+
if not b["colours"]:
|
|
253
|
+
print(" no colours returned")
|
|
254
|
+
for c in sorted(b["colours"], key=lambda c: -luminance(quantize(c["rgb"]))):
|
|
255
|
+
src, q = tuple(c["rgb"]), quantize(c["rgb"])
|
|
256
|
+
shift = "" if src == q else f" <- {hexof(src)} shifted"
|
|
257
|
+
print(f" {hexof(q)} lum {luminance(q):>3} {c['role']}{shift}")
|
|
258
|
+
if b["typefaces"]:
|
|
259
|
+
print(f" typefaces (tone only, none exist on device): "
|
|
260
|
+
f"{', '.join(b['typefaces'])}")
|
|
261
|
+
|
|
262
|
+
merged = [c for b in bundles for c in b["colours"]]
|
|
263
|
+
if merged:
|
|
264
|
+
print("\nSuggested roles, ordered by reflectance")
|
|
265
|
+
print("-" * 62)
|
|
266
|
+
for role, rgb in roles(merged).items():
|
|
267
|
+
print(f" {role:<8} {hexof(rgb)} lum {luminance(rgb)}")
|
|
268
|
+
print("\n Sanity-check before adopting: DIAL to INDEX should be the "
|
|
269
|
+
"biggest\n luminance gap you have, and ACCENT must not be the "
|
|
270
|
+
"dimmest colour on\n the face -- see references/display.md.")
|
|
271
|
+
|
|
272
|
+
if art:
|
|
273
|
+
print("\nPrior art on GitHub -- read the source, do not copy it")
|
|
274
|
+
print("-" * 62)
|
|
275
|
+
for r in art:
|
|
276
|
+
print(f" {r['stars']:>5}* {r['repo']} [{r['license']}]")
|
|
277
|
+
if r["description"]:
|
|
278
|
+
print(f" {r['description']}")
|
|
279
|
+
print("\n Licences vary and most of these carry none, which means all "
|
|
280
|
+
"rights\n reserved. Read for technique; write your own code.")
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def main():
|
|
284
|
+
ap = argparse.ArgumentParser(
|
|
285
|
+
description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
286
|
+
ap.add_argument("--brand", metavar="DOMAIN", help="Brandfetch lookup")
|
|
287
|
+
ap.add_argument("--image", metavar="PATH", help="dominant colours from a file")
|
|
288
|
+
ap.add_argument("--colours", type=int, default=6, help="how many from --image")
|
|
289
|
+
ap.add_argument("--prior-art", metavar="THEME", help="search GitHub for faces")
|
|
290
|
+
ap.add_argument("--limit", type=int, default=8, help="prior-art results")
|
|
291
|
+
ap.add_argument("--json", metavar="PATH", help="also write tokens as JSON")
|
|
292
|
+
args = ap.parse_args()
|
|
293
|
+
|
|
294
|
+
if not (args.brand or args.image or args.prior_art):
|
|
295
|
+
ap.error("nothing to do: pass --brand, --image and/or --prior-art")
|
|
296
|
+
|
|
297
|
+
bundles = []
|
|
298
|
+
if args.brand:
|
|
299
|
+
b = from_brandfetch(args.brand)
|
|
300
|
+
if b:
|
|
301
|
+
bundles.append(b)
|
|
302
|
+
if args.image:
|
|
303
|
+
b = from_image(args.image, args.colours)
|
|
304
|
+
if b:
|
|
305
|
+
bundles.append(b)
|
|
306
|
+
art = from_prior_art(args.prior_art, args.limit) if args.prior_art else []
|
|
307
|
+
|
|
308
|
+
if not bundles and not art:
|
|
309
|
+
print("No sources returned anything usable.", file=sys.stderr)
|
|
310
|
+
return 1
|
|
311
|
+
|
|
312
|
+
report(bundles, art)
|
|
313
|
+
|
|
314
|
+
if args.json:
|
|
315
|
+
merged = [c for b in bundles for c in b["colours"]]
|
|
316
|
+
Path(args.json).write_text(json.dumps({
|
|
317
|
+
"sources": [b["source"] for b in bundles],
|
|
318
|
+
"palette": [hexof(quantize(c["rgb"])) for c in merged],
|
|
319
|
+
"roles": {k: hexof(v) for k, v in roles(merged).items()},
|
|
320
|
+
"priorArt": art,
|
|
321
|
+
}, indent=2) + "\n", encoding="utf-8")
|
|
322
|
+
print(f"\nWrote {args.json}")
|
|
323
|
+
|
|
324
|
+
print("\nNext: draw exactly three variants as SVG and run ciq-mock on them.")
|
|
325
|
+
return 0
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
if __name__ == "__main__":
|
|
329
|
+
sys.exit(main())
|