@olegkoval/agent-skills 1.19.0 → 1.20.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 +3 -2
- package/.cursor-plugin/index.json +5 -0
- package/.grok-plugin/index.json +5 -0
- package/.kiro/steering/garmin-watchface.md +201 -0
- package/.kiro/steering/qodoloop.md +13 -1
- package/.windsurf/rules/garmin-watchface.md +200 -0
- package/.windsurf/rules/qodoloop.md +13 -1
- package/README.md +7 -1
- package/catalog/skills.json +28 -4
- package/collections/wearables.json +7 -0
- package/package.json +1 -1
- package/packages/marketing/viral-launch/adapters/claude/skills/viral-launch/references/external-skills.md +12 -0
- package/packages/marketing/viral-launch/adapters/claude/skills/viral-launch/references/launch-checklist.md +42 -0
- package/packages/marketing/viral-launch/adapters/cursor/skills/viral-launch/references/external-skills.md +12 -0
- package/packages/marketing/viral-launch/adapters/cursor/skills/viral-launch/references/launch-checklist.md +42 -0
- package/packages/marketing/viral-launch/adapters/grok/skills/viral-launch/references/external-skills.md +12 -0
- package/packages/marketing/viral-launch/adapters/grok/skills/viral-launch/references/launch-checklist.md +42 -0
- package/packages/software-development/qodoloop/SKILL.md +13 -1
- package/packages/software-development/qodoloop/adapters/claude/skills/qodoloop/SKILL.md +13 -1
- package/packages/software-development/qodoloop/adapters/claude/skills/qodoloop/references/graphql-queries.md +151 -0
- package/packages/software-development/qodoloop/adapters/cursor/skills/qodoloop/SKILL.md +13 -1
- package/packages/software-development/qodoloop/adapters/cursor/skills/qodoloop/references/graphql-queries.md +151 -0
- package/packages/software-development/qodoloop/adapters/grok/skills/qodoloop/SKILL.md +13 -1
- package/packages/software-development/qodoloop/adapters/grok/skills/qodoloop/references/graphql-queries.md +151 -0
- package/packages/software-development/qodoloop/adapters/kiro/steering/qodoloop.md +13 -1
- package/packages/software-development/qodoloop/adapters/windsurf/rules/qodoloop.md +13 -1
- package/packages/software-development/semantic-release-beta/adapters/claude/skills/semantic-release-beta/references/github-actions.md +16 -0
- package/packages/software-development/semantic-release-beta/adapters/claude/skills/semantic-release-beta/references/node-semantic-release.md +13 -0
- package/packages/software-development/semantic-release-beta/adapters/claude/skills/semantic-release-beta/references/preset.md +47 -0
- package/packages/software-development/semantic-release-beta/adapters/claude/skills/semantic-release-beta/references/validation.md +17 -0
- package/packages/software-development/semantic-release-beta/adapters/cursor/skills/semantic-release-beta/references/github-actions.md +16 -0
- package/packages/software-development/semantic-release-beta/adapters/cursor/skills/semantic-release-beta/references/node-semantic-release.md +13 -0
- package/packages/software-development/semantic-release-beta/adapters/cursor/skills/semantic-release-beta/references/preset.md +47 -0
- package/packages/software-development/semantic-release-beta/adapters/cursor/skills/semantic-release-beta/references/validation.md +17 -0
- package/packages/software-development/semantic-release-beta/adapters/grok/skills/semantic-release-beta/references/github-actions.md +16 -0
- package/packages/software-development/semantic-release-beta/adapters/grok/skills/semantic-release-beta/references/node-semantic-release.md +13 -0
- package/packages/software-development/semantic-release-beta/adapters/grok/skills/semantic-release-beta/references/preset.md +47 -0
- package/packages/software-development/semantic-release-beta/adapters/grok/skills/semantic-release-beta/references/validation.md +17 -0
- package/packages/wearables/garmin-watchface/SKILL.md +204 -0
- package/packages/wearables/garmin-watchface/adapters/claude/plugin.json +5 -0
- package/packages/wearables/garmin-watchface/adapters/claude/skills/garmin-watchface/SKILL.md +205 -0
- package/packages/wearables/garmin-watchface/adapters/claude/skills/garmin-watchface/references/devices.md +98 -0
- package/packages/wearables/garmin-watchface/adapters/claude/skills/garmin-watchface/references/display.md +120 -0
- package/packages/wearables/garmin-watchface/adapters/claude/skills/garmin-watchface/references/layout.md +224 -0
- package/packages/wearables/garmin-watchface/adapters/claude/skills/garmin-watchface/references/simulator.md +146 -0
- package/packages/wearables/garmin-watchface/adapters/claude/skills/garmin-watchface/references/store.md +151 -0
- package/packages/wearables/garmin-watchface/adapters/claude/skills/garmin-watchface/references/testing.md +154 -0
- package/packages/wearables/garmin-watchface/adapters/claude/skills/garmin-watchface/scripts/ciq-calibrate +184 -0
- package/packages/wearables/garmin-watchface/adapters/claude/skills/garmin-watchface/scripts/ciq-capture +215 -0
- package/packages/wearables/garmin-watchface/adapters/claude/skills/garmin-watchface/scripts/ciq-devices +177 -0
- package/packages/wearables/garmin-watchface/adapters/claude/skills/garmin-watchface/scripts/ciq-doctor +85 -0
- package/packages/wearables/garmin-watchface/adapters/claude/skills/garmin-watchface/templates/Makefile +72 -0
- package/packages/wearables/garmin-watchface/adapters/claude/skills/garmin-watchface/templates/monkey.jungle +13 -0
- package/packages/wearables/garmin-watchface/adapters/claude/skills/garmin-watchface/templates/monkey.test.jungle +7 -0
- package/packages/wearables/garmin-watchface/adapters/codex/README.md +3 -0
- package/packages/wearables/garmin-watchface/adapters/cursor/plugin.json +6 -0
- package/packages/wearables/garmin-watchface/adapters/cursor/skills/garmin-watchface/SKILL.md +205 -0
- package/packages/wearables/garmin-watchface/adapters/cursor/skills/garmin-watchface/references/devices.md +98 -0
- package/packages/wearables/garmin-watchface/adapters/cursor/skills/garmin-watchface/references/display.md +120 -0
- package/packages/wearables/garmin-watchface/adapters/cursor/skills/garmin-watchface/references/layout.md +224 -0
- package/packages/wearables/garmin-watchface/adapters/cursor/skills/garmin-watchface/references/simulator.md +146 -0
- package/packages/wearables/garmin-watchface/adapters/cursor/skills/garmin-watchface/references/store.md +151 -0
- package/packages/wearables/garmin-watchface/adapters/cursor/skills/garmin-watchface/references/testing.md +154 -0
- package/packages/wearables/garmin-watchface/adapters/cursor/skills/garmin-watchface/scripts/ciq-calibrate +184 -0
- package/packages/wearables/garmin-watchface/adapters/cursor/skills/garmin-watchface/scripts/ciq-capture +215 -0
- package/packages/wearables/garmin-watchface/adapters/cursor/skills/garmin-watchface/scripts/ciq-devices +177 -0
- package/packages/wearables/garmin-watchface/adapters/cursor/skills/garmin-watchface/scripts/ciq-doctor +85 -0
- package/packages/wearables/garmin-watchface/adapters/cursor/skills/garmin-watchface/templates/Makefile +72 -0
- package/packages/wearables/garmin-watchface/adapters/cursor/skills/garmin-watchface/templates/monkey.jungle +13 -0
- package/packages/wearables/garmin-watchface/adapters/cursor/skills/garmin-watchface/templates/monkey.test.jungle +7 -0
- package/packages/wearables/garmin-watchface/adapters/grok/plugin.json +6 -0
- package/packages/wearables/garmin-watchface/adapters/grok/skills/garmin-watchface/SKILL.md +205 -0
- package/packages/wearables/garmin-watchface/adapters/grok/skills/garmin-watchface/references/devices.md +98 -0
- package/packages/wearables/garmin-watchface/adapters/grok/skills/garmin-watchface/references/display.md +120 -0
- package/packages/wearables/garmin-watchface/adapters/grok/skills/garmin-watchface/references/layout.md +224 -0
- package/packages/wearables/garmin-watchface/adapters/grok/skills/garmin-watchface/references/simulator.md +146 -0
- package/packages/wearables/garmin-watchface/adapters/grok/skills/garmin-watchface/references/store.md +151 -0
- package/packages/wearables/garmin-watchface/adapters/grok/skills/garmin-watchface/references/testing.md +154 -0
- package/packages/wearables/garmin-watchface/adapters/grok/skills/garmin-watchface/scripts/ciq-calibrate +184 -0
- package/packages/wearables/garmin-watchface/adapters/grok/skills/garmin-watchface/scripts/ciq-capture +215 -0
- package/packages/wearables/garmin-watchface/adapters/grok/skills/garmin-watchface/scripts/ciq-devices +177 -0
- package/packages/wearables/garmin-watchface/adapters/grok/skills/garmin-watchface/scripts/ciq-doctor +85 -0
- package/packages/wearables/garmin-watchface/adapters/grok/skills/garmin-watchface/templates/Makefile +72 -0
- package/packages/wearables/garmin-watchface/adapters/grok/skills/garmin-watchface/templates/monkey.jungle +13 -0
- package/packages/wearables/garmin-watchface/adapters/grok/skills/garmin-watchface/templates/monkey.test.jungle +7 -0
- package/packages/wearables/garmin-watchface/adapters/kiro/steering/garmin-watchface.md +201 -0
- package/packages/wearables/garmin-watchface/adapters/windsurf/rules/garmin-watchface.md +200 -0
- package/packages/wearables/garmin-watchface/references/devices.md +98 -0
- package/packages/wearables/garmin-watchface/references/display.md +120 -0
- package/packages/wearables/garmin-watchface/references/layout.md +224 -0
- package/packages/wearables/garmin-watchface/references/simulator.md +146 -0
- package/packages/wearables/garmin-watchface/references/store.md +151 -0
- package/packages/wearables/garmin-watchface/references/testing.md +154 -0
- package/packages/wearables/garmin-watchface/scripts/ciq-calibrate +184 -0
- package/packages/wearables/garmin-watchface/scripts/ciq-capture +215 -0
- package/packages/wearables/garmin-watchface/scripts/ciq-devices +177 -0
- package/packages/wearables/garmin-watchface/scripts/ciq-doctor +85 -0
- package/packages/wearables/garmin-watchface/templates/Makefile +72 -0
- package/packages/wearables/garmin-watchface/templates/monkey.jungle +13 -0
- package/packages/wearables/garmin-watchface/templates/monkey.test.jungle +7 -0
- package/scripts/build-adapters.sh +1 -1
- package/scripts/validate-catalog.sh +2 -1
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# The simulator
|
|
2
|
+
|
|
3
|
+
## It persists app settings and ignores your defaults
|
|
4
|
+
|
|
5
|
+
Changing a default in `properties.xml` does **not** change what the simulator
|
|
6
|
+
shows. It keeps the value it persisted the first time the app ran, so a property
|
|
7
|
+
reads `false` while the XML plainly says `true`. This looks exactly like broken
|
|
8
|
+
settings code and will send you debugging the wrong thing.
|
|
9
|
+
|
|
10
|
+
Clear it: **File → Reset All App Data**. That also drops the loaded device, so
|
|
11
|
+
relaunch the simulator and re-push afterwards.
|
|
12
|
+
|
|
13
|
+
Diagnose before assuming a bug — print what the app actually sees:
|
|
14
|
+
|
|
15
|
+
```monkeyc
|
|
16
|
+
System.println("SETTINGS pro=" + _proMode + " ghost=" + _ghost);
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
If one property matches its XML default and another does not, you are looking at
|
|
20
|
+
stale persistence, not a bug.
|
|
21
|
+
|
|
22
|
+
## monkeydo wedges
|
|
23
|
+
|
|
24
|
+
Regularly. Symptoms: exit 124, no output, or a hang with no push.
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
pkill -f monkeydo
|
|
28
|
+
pkill -x simulator # NOT `pkill -f simulator` -- see below
|
|
29
|
+
"$SDK/bin/connectiq" &
|
|
30
|
+
sleep 12
|
|
31
|
+
"$SDK/bin/monkeydo" bin/app.prg fenix6pro
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
**Use `pkill -x simulator`, not `pkill -f simulator`.** The `-f` form matches the
|
|
35
|
+
full command line of every process, and unrelated applications carry the word
|
|
36
|
+
"simulator" in their arguments -- Claude Desktop's helper processes register a
|
|
37
|
+
`claude-simulator` URL scheme, for instance. `pkill -f simulator` kills them too.
|
|
38
|
+
`-x` matches the process name exactly.
|
|
39
|
+
|
|
40
|
+
The simulator needs about 10-12 seconds before it will accept a push, and a
|
|
41
|
+
further beat before its window is scriptable. Pushing too early gives a window
|
|
42
|
+
titled "CIQ Simulator" with no device name and no display.
|
|
43
|
+
|
|
44
|
+
For a watch face `monkeydo` does not exit on its own — it stays attached. That is
|
|
45
|
+
normal; Ctrl-C once the face renders.
|
|
46
|
+
|
|
47
|
+
## Screenshots
|
|
48
|
+
|
|
49
|
+
Use `bin/ciq-capture`. If you must do it by hand, know the three failure modes:
|
|
50
|
+
|
|
51
|
+
**1. `screencapture -R` grabs a screen region, not a window.** If the simulator is
|
|
52
|
+
not in front you get a photograph of your terminal, saved successfully, with no
|
|
53
|
+
error. Always raise the window and verify it came forward:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
osascript -e 'tell application "System Events" to tell process "simulator" to set frontmost to true'
|
|
57
|
+
sleep 2
|
|
58
|
+
osascript -e 'tell application "System Events" to get name of first process whose frontmost is true'
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
**2. The window moves between restarts.** Read its bounds every time:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
osascript -e 'tell application "System Events" to tell process "simulator" to get {position, size} of (first window whose name contains "CIQ Simulator")'
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
**3. The display is a sub-rectangle of the window.** Do not guess it. Render a
|
|
68
|
+
full-screen marker colour, capture, and take its bounding box — that is the
|
|
69
|
+
display rect exactly. `bin/ciq-calibrate` automates this. On a 2x Retina Mac with
|
|
70
|
+
the fenix 6 Pro skin the answer is 518x518 at (155,345), a clean 2:1 for the
|
|
71
|
+
260x260 display.
|
|
72
|
+
|
|
73
|
+
Sanity check the result before believing it. A watch face is neither a blank
|
|
74
|
+
white page nor solid black; if the dark fraction is outside roughly 25-90%, you
|
|
75
|
+
captured the wrong window.
|
|
76
|
+
|
|
77
|
+
**Mask round displays.** The square capture picks up a few pixels of the
|
|
78
|
+
simulator's bezel skin in its corners, which do not exist on the device. Mask at
|
|
79
|
+
4x and downsample or the edge stair-steps.
|
|
80
|
+
|
|
81
|
+
The simulator's own **File → Save Screen Capture** works too, but its save dialog
|
|
82
|
+
is a Java panel that does not respond to `Cmd+Shift+G` or normal AppleScript UI
|
|
83
|
+
scripting. Fine by hand, not automatable.
|
|
84
|
+
|
|
85
|
+
## Menus worth knowing (AppleScript-drivable)
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
osascript -e 'tell application "System Events" to tell process "simulator" to get name of every menu item of menu 1 of menu bar item "File" of menu bar 1'
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
| Menu | Item | Use |
|
|
92
|
+
| --- | --- | --- |
|
|
93
|
+
| File | Save Screen Capture | manual screenshot |
|
|
94
|
+
| File | Reset All App Data | clear persisted settings |
|
|
95
|
+
| File | Kill App | stop without closing |
|
|
96
|
+
| Settings | Set Battery Status | test battery thresholds |
|
|
97
|
+
| Settings | Set User Profile | resting HR and similar |
|
|
98
|
+
| Settings | Trigger App Settings | fire `onSettingsChanged` |
|
|
99
|
+
| Simulation | Activity Data | non-zero steps/calories |
|
|
100
|
+
| Simulation | Time Simulation | check other times of day |
|
|
101
|
+
|
|
102
|
+
The simulator reports zero steps and calories by default, because it has no
|
|
103
|
+
activity data — not because your render path is broken. Use Simulation →
|
|
104
|
+
Activity Data before concluding anything.
|
|
105
|
+
|
|
106
|
+
## Sensor data
|
|
107
|
+
|
|
108
|
+
`Toybox.SensorHistory` returns iterators whose most recent buckets are often
|
|
109
|
+
`null`. Take the newest **non-null** sample rather than the newest sample:
|
|
110
|
+
|
|
111
|
+
```monkeyc
|
|
112
|
+
const SCAN_DEPTH = 24;
|
|
113
|
+
|
|
114
|
+
function bodyBattery() as Number? {
|
|
115
|
+
if (!(Toybox has :SensorHistory)
|
|
116
|
+
|| !(Toybox.SensorHistory has :getBodyBatteryHistory)) {
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
var it = Toybox.SensorHistory.getBodyBatteryHistory(
|
|
120
|
+
{:period => SCAN_DEPTH, :order => SensorHistory.ORDER_NEWEST_FIRST});
|
|
121
|
+
if (it == null) { return null; }
|
|
122
|
+
var sample = it.next();
|
|
123
|
+
var seen = 0;
|
|
124
|
+
while (sample != null && seen < SCAN_DEPTH) {
|
|
125
|
+
if (sample.data != null) { return sample.data.toNumber(); }
|
|
126
|
+
sample = it.next();
|
|
127
|
+
seen += 1;
|
|
128
|
+
}
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Always guard with `Toybox has :SensorHistory` — not every device has it, and the
|
|
134
|
+
face must degrade to `--` rather than crash.
|
|
135
|
+
|
|
136
|
+
## Verifying on real hardware
|
|
137
|
+
|
|
138
|
+
The simulator renders on a backlit LCD and flatters everything. Colour, contrast
|
|
139
|
+
and legibility decisions are not verified until seen on the device in daylight.
|
|
140
|
+
Deploy over USB:
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
make build
|
|
144
|
+
cp bin/app.prg /Volumes/GARMIN/GARMIN/APPS/
|
|
145
|
+
# eject, then pick the face on-device
|
|
146
|
+
```
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# Publishing
|
|
2
|
+
|
|
3
|
+
**Portal:** <https://apps.garmin.com/developer/dashboard>
|
|
4
|
+
**Guidelines:** <https://developer.garmin.com/connect-iq/app-review-guidelines/>
|
|
5
|
+
|
|
6
|
+
Upload is a single file: the `.iq` from `make package`. It is a multi-device
|
|
7
|
+
bundle — every declared product is inside it. Nothing else to attach.
|
|
8
|
+
|
|
9
|
+
## App settings live on the phone, not the watch
|
|
10
|
+
|
|
11
|
+
Connect IQ watch face settings are **not reachable from the watch**. The
|
|
12
|
+
on-device face menu offers only Apply. Users configure via:
|
|
13
|
+
|
|
14
|
+
Garmin Connect app → device → Connect IQ Apps → Watch Faces → *your face* → gear.
|
|
15
|
+
|
|
16
|
+
Say this in the listing or it reads as a missing feature.
|
|
17
|
+
|
|
18
|
+
Wiring, three files plus a handler:
|
|
19
|
+
|
|
20
|
+
```xml
|
|
21
|
+
<!-- resources/settings/properties.xml -->
|
|
22
|
+
<properties>
|
|
23
|
+
<property id="proMode" type="boolean">false</property>
|
|
24
|
+
</properties>
|
|
25
|
+
|
|
26
|
+
<!-- resources/settings/settings.xml -->
|
|
27
|
+
<settings>
|
|
28
|
+
<setting propertyKey="@Properties.proMode" title="@Strings.SettingProMode">
|
|
29
|
+
<settingConfig type="boolean" />
|
|
30
|
+
</setting>
|
|
31
|
+
</settings>
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
```monkeyc
|
|
35
|
+
// getValue THROWS when a key is absent -- which happens on a fresh install
|
|
36
|
+
// before the Connect app has ever written settings. Default every read.
|
|
37
|
+
private function boolSetting(key as String, fallback as Boolean) as Boolean {
|
|
38
|
+
try {
|
|
39
|
+
var value = Application.Properties.getValue(key);
|
|
40
|
+
if (value instanceof Lang.Boolean) { return value as Boolean; }
|
|
41
|
+
} catch (e) {
|
|
42
|
+
return fallback;
|
|
43
|
+
}
|
|
44
|
+
return fallback;
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
```monkeyc
|
|
49
|
+
// In the AppBase subclass -- without this the face keeps its old settings
|
|
50
|
+
// until it is reselected.
|
|
51
|
+
function onSettingsChanged() as Void {
|
|
52
|
+
WatchUi.requestUpdate();
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Re-read settings in both `onLayout` and `onShow`.
|
|
57
|
+
|
|
58
|
+
See `reference/simulator.md` for why the simulator will lie to you about this.
|
|
59
|
+
|
|
60
|
+
## Intellectual property
|
|
61
|
+
|
|
62
|
+
Section 3(a) is stricter than people expect and explicitly covers your **listing
|
|
63
|
+
text and images**, not just code:
|
|
64
|
+
|
|
65
|
+
> You may not infringe any copyright, trademark, patent, trade secret, or any
|
|
66
|
+
> other form of intellectual property. You must own or have a license to use all
|
|
67
|
+
> IP included in or used by your app. This includes your app's software and
|
|
68
|
+
> content, your developer account name, as well as the logos, images, and content
|
|
69
|
+
> that you use to promote your app in the Connect IQ store.
|
|
70
|
+
|
|
71
|
+
> Be careful using brand names and logos ... Be careful before making any mention
|
|
72
|
+
> of a brand or using any logo or name that is not yours.
|
|
73
|
+
|
|
74
|
+
And Garmin will not adjudicate:
|
|
75
|
+
|
|
76
|
+
> It is not for us to decide whether your use of a third party's IP may be
|
|
77
|
+
> infringing, properly used under a license, or fair use.
|
|
78
|
+
|
|
79
|
+
So there is no approval to win. They accept it, and §5(b) lets them suspend with
|
|
80
|
+
**no notice** if a rights holder complains — against the account, which carries
|
|
81
|
+
your other apps too.
|
|
82
|
+
|
|
83
|
+
Practical tiers for an homage:
|
|
84
|
+
|
|
85
|
+
- **Highest risk:** a film title or real brand in the app name, launcher icon, or
|
|
86
|
+
screenshots. That is trademark use for product identity.
|
|
87
|
+
- **Lower risk:** one factual, referential sentence in the description body.
|
|
88
|
+
- **No risk, and usually better copy:** describe the *object* and the *era*. The
|
|
89
|
+
people who will recognise it recognise the shape, not the name.
|
|
90
|
+
|
|
91
|
+
"The brand is dead so I can use it" does not hold. Abandonment is a legal finding
|
|
92
|
+
requiring genuine non-use with no intent to resume, and it is moot if anyone holds
|
|
93
|
+
a live registration — renewal is cheap and dormant heritage marks are routinely
|
|
94
|
+
warehoused and revived. Check the registers before relying on it: USPTO Trademark
|
|
95
|
+
Search, EUIPO eSearch, WIPO Global Brand Database, and the national register of
|
|
96
|
+
the owner's country. A lapsed mark you do not own is still not a mark you *own*,
|
|
97
|
+
which is what §3(a) asks for.
|
|
98
|
+
|
|
99
|
+
Not legal advice. When it matters, ask a lawyer, not the store.
|
|
100
|
+
|
|
101
|
+
## Donation links
|
|
102
|
+
|
|
103
|
+
Permitted, with care. §1(a) prohibits listings that are advertising-focused or
|
|
104
|
+
"serve primarily to drive affiliate traffic to a website", and §4(d) requires
|
|
105
|
+
stating whether payment is needed. A single support line **after** a complete
|
|
106
|
+
description, prefixed with *free*, clears both. Leading with it, or repeating it
|
|
107
|
+
in the short description, does not.
|
|
108
|
+
|
|
109
|
+
## Screenshots
|
|
110
|
+
|
|
111
|
+
Regenerate from the build you are uploading, not an older one. Stale screenshots
|
|
112
|
+
are the most common thing to forget after a redesign, and they misrepresent the
|
|
113
|
+
version under review.
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
make build && make sim
|
|
117
|
+
bin/ciq-capture docs/store/screenshot.png --face --size 260 --mask
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Generate the launcher icon and any hero image from the same geometry the face
|
|
121
|
+
uses, in a checked-in script, so they cannot drift from what the device renders.
|
|
122
|
+
Two things that bite when reusing face-drawing code for artwork:
|
|
123
|
+
|
|
124
|
+
- **Polarity must invert with the surface.** Face colours drawn on a hero's dark
|
|
125
|
+
background paint dark-on-dark and vanish. Take colours as parameters.
|
|
126
|
+
- **Detail that sells at full size destroys legibility at icon size.** Ghost
|
|
127
|
+
segments turn every digit into an `8` at 40px. Drop them there.
|
|
128
|
+
|
|
129
|
+
## Checklist
|
|
130
|
+
|
|
131
|
+
- [ ] `make test` passes — and the tests actually compile (`reference/testing.md`)
|
|
132
|
+
- [ ] `make lint` clean at level 3
|
|
133
|
+
- [ ] `make package` builds every declared device
|
|
134
|
+
- [ ] Developer key not committed
|
|
135
|
+
- [ ] Screenshots regenerated from this build
|
|
136
|
+
- [ ] Launcher icon is yours, at the size each device wants
|
|
137
|
+
- [ ] Listing states that settings are in the Garmin Connect app
|
|
138
|
+
- [ ] No third-party marks in name, icon, screenshots, or copy
|
|
139
|
+
- [ ] Verified on real hardware in daylight, not just the simulator
|
|
140
|
+
|
|
141
|
+
## Key hygiene
|
|
142
|
+
|
|
143
|
+
The developer key signs your app id. Lose it and you cannot update the listing;
|
|
144
|
+
commit it and anyone can impersonate your app.
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
make key # generates ~/.Garmin/ConnectIQ/developer_key.der
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Back it up somewhere private. Keep `bin/` and the key out of git, and run a
|
|
151
|
+
pre-commit check that greps for `.der`/`.pem` in the index.
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
# Testing
|
|
2
|
+
|
|
3
|
+
## The trap: a green run with zero tests compiled
|
|
4
|
+
|
|
5
|
+
`monkey.jungle` almost always contains:
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
base.excludeAnnotations = test
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
That is correct — test code must not ship in the store build. But it strips
|
|
12
|
+
`(:test)` from **every** target, including `--unit-test`. The result:
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
BUILD SUCCESSFUL
|
|
16
|
+
Ran 0 tests
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
or, worse, a green run reported against stale test files that reference deleted
|
|
20
|
+
symbols. Tests written months ago may never have executed once.
|
|
21
|
+
|
|
22
|
+
**Fix** — a second jungle, passed as an additional `-f`:
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
# monkey.test.jungle
|
|
26
|
+
# Loaded only for `make test`, after monkey.jungle, so (:test) code actually
|
|
27
|
+
# compiles. The grammar requires a value here, so this is a placeholder that
|
|
28
|
+
# matches no annotation.
|
|
29
|
+
base.excludeAnnotations = none
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
```makefile
|
|
33
|
+
TEST_JUNGLE := monkey.jungle;monkey.test.jungle
|
|
34
|
+
|
|
35
|
+
test:
|
|
36
|
+
$(MONKEYC) -f "$(TEST_JUNGLE)" -d $(DEVICE) -o $(APP) -y $(KEY) -w -l 2 --unit-test
|
|
37
|
+
$(MONKEYDO) $(APP) $(DEVICE) -t
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Note `base.excludeAnnotations =` with an empty value is a **syntax error**. Use a
|
|
41
|
+
placeholder like `none`.
|
|
42
|
+
|
|
43
|
+
**Prove your tests compile.** Do not take a green run on trust:
|
|
44
|
+
|
|
45
|
+
```monkeyc
|
|
46
|
+
(:test)
|
|
47
|
+
function someExistingTest(logger as Logger) as Boolean {
|
|
48
|
+
return ThisDoesNotExist.definitelyNotAFunction(); // must FAIL the build
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
If the build still succeeds, nothing is being compiled. Run this check whenever
|
|
53
|
+
you inherit a project or change the jungle.
|
|
54
|
+
|
|
55
|
+
## What is worth testing
|
|
56
|
+
|
|
57
|
+
Monkey C unit tests run on-device or in the simulator and can only assert on pure
|
|
58
|
+
logic — you cannot inspect rendered pixels. So test the two things that are pure:
|
|
59
|
+
**geometry** and **formatting**. Keep `Dc` out of `Layout` and `Fields` and they
|
|
60
|
+
stay testable.
|
|
61
|
+
|
|
62
|
+
### Geometry invariants
|
|
63
|
+
|
|
64
|
+
```monkeyc
|
|
65
|
+
(:test)
|
|
66
|
+
function goalBarClearsBothTextColumns(logger as Logger) as Boolean {
|
|
67
|
+
var barEnd = Layout.BAR_X + Layout.BAR_W;
|
|
68
|
+
return (Layout.BAR_X - Layout.LABEL_X) >= 40
|
|
69
|
+
&& (Layout.VALUE_X - barEnd) >= 40;
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Sweeps, not single values
|
|
74
|
+
|
|
75
|
+
The single most valuable pattern here. A point test passes with the font height
|
|
76
|
+
you assumed; the sweep catches the device you have not tried:
|
|
77
|
+
|
|
78
|
+
```monkeyc
|
|
79
|
+
(:test)
|
|
80
|
+
function footerStaysOnScreenForAnyPlausibleFont(logger as Logger) as Boolean {
|
|
81
|
+
for (var labelH = 14; labelH <= 40; labelH += 1) {
|
|
82
|
+
for (var timeH = 40; timeH <= 120; timeH += 4) {
|
|
83
|
+
if (footerBottom(labelH, timeH / 2, timeH) >= Layout.SCREEN) {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### Lookup tables where a typo is plausible
|
|
93
|
+
|
|
94
|
+
A mistyped segment mask produces a *plausible* wrong digit — a `6` missing its
|
|
95
|
+
top bar still reads as a `6` to a careless eye. Pin by count plus the specific
|
|
96
|
+
features that distinguish neighbours:
|
|
97
|
+
|
|
98
|
+
```monkeyc
|
|
99
|
+
(:test)
|
|
100
|
+
function everyDigitLightsTheRightNumberOfSegments(logger as Logger) as Boolean {
|
|
101
|
+
var expected = [6, 2, 5, 5, 4, 5, 6, 3, 7, 6];
|
|
102
|
+
for (var d = 0; d <= 9; d += 1) {
|
|
103
|
+
var count = 0;
|
|
104
|
+
for (var bit = 0; bit < 7; bit += 1) {
|
|
105
|
+
if ((Seg7.maskFor(d) & (1 << bit)) != 0) { count += 1; }
|
|
106
|
+
}
|
|
107
|
+
if (count != expected[d]) { return false; }
|
|
108
|
+
}
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
(:test)
|
|
113
|
+
function zeroHasNoMiddleBarButEightDoes(logger as Logger) as Boolean {
|
|
114
|
+
return !Seg7.isLit(Seg7.maskFor(0), Seg7.SEG_G)
|
|
115
|
+
&& Seg7.isLit(Seg7.maskFor(8), Seg7.SEG_G);
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### Thresholds, with the reasoning in the test name
|
|
120
|
+
|
|
121
|
+
```monkeyc
|
|
122
|
+
// A 14-day watch at 50% has a week left. Treating that as a warning was the bug.
|
|
123
|
+
(:test)
|
|
124
|
+
function halfChargeIsHealthyNotCaution(logger as Logger) as Boolean {
|
|
125
|
+
return Fields.batteryColor(50.0) == Palette.LABEL;
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### Dead code
|
|
130
|
+
|
|
131
|
+
If a function is tested but never called, the tests pass and the feature does not
|
|
132
|
+
exist. Grep for call sites of anything you have just written a test for.
|
|
133
|
+
|
|
134
|
+
## What tests cannot catch
|
|
135
|
+
|
|
136
|
+
Everything visual. Overlapping rows, a bar through a label, a face that is too
|
|
137
|
+
dim, content drawn off-screen — all of it passes a green suite. **Capture a
|
|
138
|
+
screenshot and look at it before committing.** Every visual bug in this skill's
|
|
139
|
+
history was found by looking.
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
make build && make sim
|
|
143
|
+
bin/ciq-capture /tmp/face.png --face --size 260
|
|
144
|
+
# then Read the PNG
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
## Running
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
make test # compile with the test jungle and run
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
`monkeydo ... -t` prints per-test PASS/FAIL and a summary. It also wedges often;
|
|
154
|
+
see `reference/simulator.md`.
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Find where the device display sits inside the simulator window.
|
|
3
|
+
|
|
4
|
+
Guessing this crops in bezel skin or clips the face. Instead: build a throwaway
|
|
5
|
+
app that floods the whole screen with one unmistakable colour, push it, capture,
|
|
6
|
+
and take the bounding box of that colour. That box IS the display rect, to the
|
|
7
|
+
pixel, for whatever device skin is loaded.
|
|
8
|
+
|
|
9
|
+
ciq-calibrate # uses the fenix6pro device by default
|
|
10
|
+
ciq-calibrate --device fr955
|
|
11
|
+
|
|
12
|
+
Writes ~/.cache/ciq-capture/calibration.json, which ciq-capture then uses.
|
|
13
|
+
Re-run when switching to a device whose skin is a different size.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import argparse
|
|
19
|
+
import json
|
|
20
|
+
import shutil
|
|
21
|
+
import subprocess
|
|
22
|
+
import sys
|
|
23
|
+
import tempfile
|
|
24
|
+
import time
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
|
|
27
|
+
CACHE = Path.home() / ".cache" / "ciq-capture"
|
|
28
|
+
CIQ = Path.home() / "Library/Application Support/Garmin/ConnectIQ"
|
|
29
|
+
HERE = Path(__file__).resolve().parent
|
|
30
|
+
|
|
31
|
+
MARKER_RGB = (0xFF, 0x00, 0xFF)
|
|
32
|
+
|
|
33
|
+
APP = """import Toybox.Application;
|
|
34
|
+
import Toybox.Graphics;
|
|
35
|
+
import Toybox.Lang;
|
|
36
|
+
import Toybox.WatchUi;
|
|
37
|
+
|
|
38
|
+
class CalApp extends Application.AppBase {
|
|
39
|
+
function initialize() { AppBase.initialize(); }
|
|
40
|
+
function getInitialView() as [WatchUi.Views] or [WatchUi.Views, WatchUi.InputDelegates] {
|
|
41
|
+
return [new CalView()];
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
class CalView extends WatchUi.WatchFace {
|
|
46
|
+
function initialize() { WatchFace.initialize(); }
|
|
47
|
+
function onUpdate(dc as Dc) as Void {
|
|
48
|
+
dc.setColor(0xFF00FF, 0xFF00FF);
|
|
49
|
+
dc.clear();
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
MANIFEST = """<?xml version="1.0" encoding="UTF-8"?>
|
|
55
|
+
<iq:manifest xmlns:iq="http://www.garmin.com/xml/connectiq" version="3">
|
|
56
|
+
<iq:application entry="CalApp" id="{appid}" launcherIcon="@Drawables.LauncherIcon"
|
|
57
|
+
minApiLevel="3.1.0" name="@Strings.AppName" type="watchface">
|
|
58
|
+
<iq:products><iq:product id="{device}"/></iq:products>
|
|
59
|
+
<iq:permissions/>
|
|
60
|
+
<iq:languages><iq:language>eng</iq:language></iq:languages>
|
|
61
|
+
<iq:barrels/>
|
|
62
|
+
</iq:application>
|
|
63
|
+
</iq:manifest>
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
JUNGLE = "project.manifest = manifest.xml\nbase.sourcePath = source\nbase.resourcePath = resources\n"
|
|
67
|
+
STRINGS = '<?xml version="1.0" encoding="UTF-8"?>\n<strings><string id="AppName">cal</string></strings>\n'
|
|
68
|
+
DRAWABLES = ('<?xml version="1.0" encoding="UTF-8"?>\n<drawables>'
|
|
69
|
+
'<bitmap id="LauncherIcon" filename="icon.png"/></drawables>\n')
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def sdk_bin():
|
|
73
|
+
cfg = CIQ / "current-sdk.cfg"
|
|
74
|
+
if not cfg.is_file():
|
|
75
|
+
sys.exit("No SDK selected. Open SdkManager.app and install/select an SDK.")
|
|
76
|
+
return Path(cfg.read_text().strip()) / "bin"
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def osa(script):
|
|
80
|
+
r = subprocess.run(["osascript", "-e", script], capture_output=True, text=True)
|
|
81
|
+
return r.stdout.strip() if r.returncode == 0 else ""
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def main():
|
|
85
|
+
ap = argparse.ArgumentParser(description=__doc__,
|
|
86
|
+
formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
87
|
+
ap.add_argument("--device", default="fenix6pro")
|
|
88
|
+
ap.add_argument("--key", default=str(Path.home() / ".Garmin/ConnectIQ/developer_key.der"))
|
|
89
|
+
args = ap.parse_args()
|
|
90
|
+
|
|
91
|
+
try:
|
|
92
|
+
from PIL import Image
|
|
93
|
+
except ImportError:
|
|
94
|
+
sys.exit("Needs Pillow: pip install pillow")
|
|
95
|
+
|
|
96
|
+
key = Path(args.key)
|
|
97
|
+
if not key.is_file():
|
|
98
|
+
sys.exit(f"No developer key at {key}. Generate one with `make key`.")
|
|
99
|
+
|
|
100
|
+
sdk = sdk_bin()
|
|
101
|
+
import uuid
|
|
102
|
+
|
|
103
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
104
|
+
root = Path(tmp)
|
|
105
|
+
(root / "source").mkdir()
|
|
106
|
+
(root / "resources/drawables").mkdir(parents=True)
|
|
107
|
+
(root / "resources/strings").mkdir(parents=True)
|
|
108
|
+
|
|
109
|
+
(root / "source/CalApp.mc").write_text(APP)
|
|
110
|
+
(root / "manifest.xml").write_text(
|
|
111
|
+
MANIFEST.format(appid=uuid.uuid4().hex, device=args.device))
|
|
112
|
+
(root / "monkey.jungle").write_text(JUNGLE)
|
|
113
|
+
(root / "resources/strings/strings.xml").write_text(STRINGS)
|
|
114
|
+
(root / "resources/drawables/drawables.xml").write_text(DRAWABLES)
|
|
115
|
+
Image.new("RGB", (40, 40), (0, 0, 0)).save(root / "resources/drawables/icon.png")
|
|
116
|
+
|
|
117
|
+
prg = root / "cal.prg"
|
|
118
|
+
print(f"building marker app for {args.device} ...")
|
|
119
|
+
r = subprocess.run(
|
|
120
|
+
[str(sdk / "monkeyc"), "-f", str(root / "monkey.jungle"),
|
|
121
|
+
"-d", args.device, "-o", str(prg), "-y", str(key), "-w"],
|
|
122
|
+
capture_output=True, text=True, cwd=root)
|
|
123
|
+
if "BUILD SUCCESSFUL" not in (r.stdout + r.stderr):
|
|
124
|
+
sys.exit("marker build failed:\n" + r.stdout + r.stderr)
|
|
125
|
+
|
|
126
|
+
subprocess.run(["pkill", "-f", "monkeydo"], capture_output=True)
|
|
127
|
+
subprocess.Popen([str(sdk / "connectiq")],
|
|
128
|
+
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
129
|
+
time.sleep(10)
|
|
130
|
+
subprocess.Popen([str(sdk / "monkeydo"), str(prg), args.device],
|
|
131
|
+
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
132
|
+
print("pushing to simulator ...")
|
|
133
|
+
time.sleep(20)
|
|
134
|
+
|
|
135
|
+
osa('tell application "System Events" to tell process "simulator" '
|
|
136
|
+
"to set frontmost to true")
|
|
137
|
+
time.sleep(2)
|
|
138
|
+
|
|
139
|
+
out = osa('tell application "System Events" to tell process "simulator" to get '
|
|
140
|
+
'{position, size} of (first window whose name contains "CIQ Simulator")')
|
|
141
|
+
parts = [p.strip() for p in out.split(",") if p.strip()]
|
|
142
|
+
if len(parts) != 4:
|
|
143
|
+
sys.exit("could not read the simulator window bounds")
|
|
144
|
+
x, y, w, h = (int(p) for p in parts)
|
|
145
|
+
|
|
146
|
+
shot = root / "cal.png"
|
|
147
|
+
subprocess.run(["screencapture", "-x", f"-R{x},{y},{w},{h}", str(shot)], check=True)
|
|
148
|
+
|
|
149
|
+
im = Image.open(shot).convert("RGB")
|
|
150
|
+
px = im.load()
|
|
151
|
+
xs, ys = [], []
|
|
152
|
+
for yy in range(im.height):
|
|
153
|
+
for xx in range(im.width):
|
|
154
|
+
r_, g_, b_ = px[xx, yy]
|
|
155
|
+
if r_ > 200 and b_ > 200 and g_ < 60:
|
|
156
|
+
xs.append(xx)
|
|
157
|
+
ys.append(yy)
|
|
158
|
+
|
|
159
|
+
if not xs:
|
|
160
|
+
sys.exit(
|
|
161
|
+
"marker not found in the capture.\n"
|
|
162
|
+
"The simulator was probably not in front, or the app did not load."
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
left, top = min(xs), min(ys)
|
|
166
|
+
width, height = max(xs) - left + 1, max(ys) - top + 1
|
|
167
|
+
if abs(width - height) > 2:
|
|
168
|
+
print(f"warning: marker is {width}x{height}, not square -- "
|
|
169
|
+
"non-square display or a partial capture", file=sys.stderr)
|
|
170
|
+
|
|
171
|
+
scale = round(width / max(im.width and 1, 1)) or 1
|
|
172
|
+
rect = {"left": left, "top": top, "size": max(width, height),
|
|
173
|
+
"scale": 2 if width > 300 else 1, "device": args.device}
|
|
174
|
+
|
|
175
|
+
CACHE.mkdir(parents=True, exist_ok=True)
|
|
176
|
+
CALIB = CACHE / "calibration.json"
|
|
177
|
+
CALIB.write_text(json.dumps(rect, indent=2) + "\n")
|
|
178
|
+
print(f"\ndisplay rect: {width}x{height} at ({left},{top}) in a "
|
|
179
|
+
f"{im.width}x{im.height} capture")
|
|
180
|
+
print(f"written to {CALIB}")
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
if __name__ == "__main__":
|
|
184
|
+
main()
|