summaryrefslogtreecommitdiff
path: root/spike
diff options
context:
space:
mode:
Diffstat (limited to 'spike')
-rw-r--r--spike/settings/icons.jungle10
-rw-r--r--spike/settings/manifest.xml26
-rw-r--r--spike/settings/monkey.jungle8
-rw-r--r--spike/settings/resources-icons/40x40/drawables.xml7
-rw-r--r--spike/settings/resources-icons/40x40/launcher_icon.pngbin0 -> 2509 bytes
-rw-r--r--spike/settings/resources-icons/54x54/drawables.xml7
-rw-r--r--spike/settings/resources-icons/54x54/launcher_icon.pngbin0 -> 3219 bytes
-rw-r--r--spike/settings/resources-icons/65x65/drawables.xml7
-rw-r--r--spike/settings/resources-icons/65x65/launcher_icon.pngbin0 -> 4002 bytes
-rw-r--r--spike/settings/resources/drawables/drawables.xml7
-rw-r--r--spike/settings/resources/drawables/launcher_icon.pngbin0 -> 3506 bytes
-rw-r--r--spike/settings/resources/settings/properties.xml23
-rw-r--r--spike/settings/resources/settings/settings.xml58
-rw-r--r--spike/settings/resources/strings/strings.xml31
-rw-r--r--spike/settings/source/DebugSelfTest.mc35
-rw-r--r--spike/settings/source/SpikeApp.mc43
-rw-r--r--spike/settings/source/SpikeDelegate.mc35
-rw-r--r--spike/settings/source/SpikeReport.mc144
-rw-r--r--spike/settings/source/SpikeView.mc53
-rw-r--r--spike/settings/test-strings/index.html30
-rw-r--r--spike/settings/test-strings/len-0100.txt5
-rw-r--r--spike/settings/test-strings/len-0255.txt5
-rw-r--r--spike/settings/test-strings/len-0256.txt5
-rw-r--r--spike/settings/test-strings/len-0500.txt5
-rw-r--r--spike/settings/test-strings/len-0800.txt5
-rw-r--r--spike/settings/test-strings/len-0801.txt5
-rw-r--r--spike/settings/test-strings/len-1000.txt5
-rw-r--r--spike/settings/test-strings/len-2000.txt5
-rw-r--r--spike/settings/test-strings/len-4000.txt5
-rw-r--r--spike/settings/test-strings/len-8000.txt5
-rw-r--r--spike/settings/test-strings/make_test_strings.py92
-rw-r--r--spike/settings/test-strings/special.txt5
32 files changed, 671 insertions, 0 deletions
diff --git a/spike/settings/icons.jungle b/spike/settings/icons.jungle
new file mode 100644
index 0000000..de24aea
--- /dev/null
+++ b/spike/settings/icons.jungle
@@ -0,0 +1,10 @@
+# Author: cirakg & claude ai
+# Summary: Per-device launcher-icon resources; generated by tools/make_icon.py --all (do not edit).
+#
+# Each product gets the icon folder matching the launcher-icon size in its
+# device definition, so the compiler never has to scale (and warn).
+
+fr255s.resourcePath = $(fr255s.resourcePath);resources-icons/40x40
+venusq2.resourcePath = $(venusq2.resourcePath);resources-icons/40x40
+fr70.resourcePath = $(fr70.resourcePath);resources-icons/54x54
+fr965.resourcePath = $(fr965.resourcePath);resources-icons/65x65
diff --git a/spike/settings/manifest.xml b/spike/settings/manifest.xml
new file mode 100644
index 0000000..3144ffa
--- /dev/null
+++ b/spike/settings/manifest.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Author: cirakg & claude ai
+ Summary: Manifest of the M1 settings spike, a throwaway app that measures Connect IQ settings limits.
+
+ * Its own app id, separate from OTP Wallet, so it can be uploaded as its own
+ private beta and deleted afterwards.
+ * No permissions, so no network.
+ * Products: the four test devices, including your Forerunner 70.
+-->
+<iq:manifest version="3" xmlns:iq="http://www.garmin.com/xml/connectiq">
+ <iq:application id="9e455c06fab44b58b6d431a2d8cf2900" type="watch-app" name="@Strings.AppName"
+ entry="SpikeApp" launcherIcon="@Drawables.LauncherIcon" minApiLevel="5.0.0">
+ <iq:products>
+ <iq:product id="fr255s"/>
+ <iq:product id="fr70"/>
+ <iq:product id="fr965"/>
+ <iq:product id="venusq2"/>
+ </iq:products>
+ <iq:permissions/>
+ <iq:languages>
+ <iq:language>eng</iq:language>
+ </iq:languages>
+ <iq:barrels/>
+ </iq:application>
+</iq:manifest>
diff --git a/spike/settings/monkey.jungle b/spike/settings/monkey.jungle
new file mode 100644
index 0000000..6a70a0b
--- /dev/null
+++ b/spike/settings/monkey.jungle
@@ -0,0 +1,8 @@
+# Author: cirakg & claude ai
+# Summary: Build configuration of the M1 settings spike (used with icons.jungle by build-spike.sh).
+#
+# Paths are relative to this folder.
+project.manifest = manifest.xml
+
+base.sourcePath = source
+base.resourcePath = resources
diff --git a/spike/settings/resources-icons/40x40/drawables.xml b/spike/settings/resources-icons/40x40/drawables.xml
new file mode 100644
index 0000000..6d638f8
--- /dev/null
+++ b/spike/settings/resources-icons/40x40/drawables.xml
@@ -0,0 +1,7 @@
+<!--
+ Author: cirakg & claude ai
+ Summary: Launcher icon at 40x40 px for the devices listed in icons.jungle (generated by tools/make_icon.py).
+-->
+<drawables>
+ <bitmap id="LauncherIcon" filename="launcher_icon.png" />
+</drawables>
diff --git a/spike/settings/resources-icons/40x40/launcher_icon.png b/spike/settings/resources-icons/40x40/launcher_icon.png
new file mode 100644
index 0000000..39193ac
--- /dev/null
+++ b/spike/settings/resources-icons/40x40/launcher_icon.png
Binary files differ
diff --git a/spike/settings/resources-icons/54x54/drawables.xml b/spike/settings/resources-icons/54x54/drawables.xml
new file mode 100644
index 0000000..b703a0e
--- /dev/null
+++ b/spike/settings/resources-icons/54x54/drawables.xml
@@ -0,0 +1,7 @@
+<!--
+ Author: cirakg & claude ai
+ Summary: Launcher icon at 54x54 px for the devices listed in icons.jungle (generated by tools/make_icon.py).
+-->
+<drawables>
+ <bitmap id="LauncherIcon" filename="launcher_icon.png" />
+</drawables>
diff --git a/spike/settings/resources-icons/54x54/launcher_icon.png b/spike/settings/resources-icons/54x54/launcher_icon.png
new file mode 100644
index 0000000..a6cd756
--- /dev/null
+++ b/spike/settings/resources-icons/54x54/launcher_icon.png
Binary files differ
diff --git a/spike/settings/resources-icons/65x65/drawables.xml b/spike/settings/resources-icons/65x65/drawables.xml
new file mode 100644
index 0000000..9450580
--- /dev/null
+++ b/spike/settings/resources-icons/65x65/drawables.xml
@@ -0,0 +1,7 @@
+<!--
+ Author: cirakg & claude ai
+ Summary: Launcher icon at 65x65 px for the devices listed in icons.jungle (generated by tools/make_icon.py).
+-->
+<drawables>
+ <bitmap id="LauncherIcon" filename="launcher_icon.png" />
+</drawables>
diff --git a/spike/settings/resources-icons/65x65/launcher_icon.png b/spike/settings/resources-icons/65x65/launcher_icon.png
new file mode 100644
index 0000000..892b731
--- /dev/null
+++ b/spike/settings/resources-icons/65x65/launcher_icon.png
Binary files differ
diff --git a/spike/settings/resources/drawables/drawables.xml b/spike/settings/resources/drawables/drawables.xml
new file mode 100644
index 0000000..4425922
--- /dev/null
+++ b/spike/settings/resources/drawables/drawables.xml
@@ -0,0 +1,7 @@
+<!--
+ Author: cirakg & claude ai
+ Summary: Default launcher icon of the spike (per-size icons come from resources-icons/ via icons.jungle).
+-->
+<drawables>
+ <bitmap id="LauncherIcon" filename="launcher_icon.png" />
+</drawables>
diff --git a/spike/settings/resources/drawables/launcher_icon.png b/spike/settings/resources/drawables/launcher_icon.png
new file mode 100644
index 0000000..a4b5902
--- /dev/null
+++ b/spike/settings/resources/drawables/launcher_icon.png
Binary files differ
diff --git a/spike/settings/resources/settings/properties.xml b/spike/settings/resources/settings/properties.xml
new file mode 100644
index 0000000..ff444cf
--- /dev/null
+++ b/spike/settings/resources/settings/properties.xml
@@ -0,0 +1,23 @@
+<!--
+ Author: cirakg & claude ai
+ Summary: Properties behind the spike's settings: eight text fields plus one of each other field type.
+-->
+<properties>
+ <!-- Length tests: no maxLength, so the phone app's own limit shows. -->
+ <property id="text1" type="string"></property>
+ <property id="text2" type="string"></property>
+ <property id="text3" type="string"></property>
+ <property id="text4" type="string"></property>
+ <property id="text5" type="string"></property>
+ <property id="text6" type="string"></property>
+ <property id="text7" type="string"></property>
+ <property id="text8" type="string"></property>
+ <!-- Masked input; the watch clears it after reading (write-back test). -->
+ <property id="secretField" type="string"></property>
+ <!-- Read-only on the phone; the watch writes a status line here. -->
+ <property id="statusField" type="string"></property>
+ <property id="switchField" type="boolean">false</property>
+ <property id="listField" type="number">0</property>
+ <property id="numberField" type="number">30</property>
+ <property id="longField" type="long">0</property>
+</properties>
diff --git a/spike/settings/resources/settings/settings.xml b/spike/settings/resources/settings/settings.xml
new file mode 100644
index 0000000..967e27c
--- /dev/null
+++ b/spike/settings/resources/settings/settings.xml
@@ -0,0 +1,58 @@
+<!--
+ Author: cirakg & claude ai
+ Summary: Settings screen of the spike as shown in Garmin Connect: length-test fields and one of each field type.
+-->
+<settings>
+ <setting propertyKey="@Properties.statusField" title="@Strings.Status">
+ <settingConfig type="alphaNumeric" readonly="true" />
+ </setting>
+
+ <group id="lengthTests" title="@Strings.GroupLength" description="@Strings.GroupLengthHelp">
+ <setting propertyKey="@Properties.text1" title="@Strings.Text1" prompt="@Strings.TextPrompt">
+ <settingConfig type="alphaNumeric" />
+ </setting>
+ <setting propertyKey="@Properties.text2" title="@Strings.Text2" prompt="@Strings.TextPrompt">
+ <settingConfig type="alphaNumeric" />
+ </setting>
+ <setting propertyKey="@Properties.text3" title="@Strings.Text3" prompt="@Strings.TextPrompt">
+ <settingConfig type="alphaNumeric" />
+ </setting>
+ <setting propertyKey="@Properties.text4" title="@Strings.Text4" prompt="@Strings.TextPrompt">
+ <settingConfig type="alphaNumeric" />
+ </setting>
+ <setting propertyKey="@Properties.text5" title="@Strings.Text5" prompt="@Strings.TextPrompt">
+ <settingConfig type="alphaNumeric" />
+ </setting>
+ <setting propertyKey="@Properties.text6" title="@Strings.Text6" prompt="@Strings.TextPrompt">
+ <settingConfig type="alphaNumeric" />
+ </setting>
+ <setting propertyKey="@Properties.text7" title="@Strings.Text7" prompt="@Strings.TextPrompt">
+ <settingConfig type="alphaNumeric" />
+ </setting>
+ <setting propertyKey="@Properties.text8" title="@Strings.Text8" prompt="@Strings.TextPrompt">
+ <settingConfig type="alphaNumeric" />
+ </setting>
+ </group>
+
+ <group id="otherTypes" title="@Strings.GroupOther" description="@Strings.GroupOtherHelp">
+ <setting propertyKey="@Properties.secretField" title="@Strings.Secret" prompt="@Strings.SecretPrompt">
+ <settingConfig type="password" />
+ </setting>
+ <setting propertyKey="@Properties.switchField" title="@Strings.Switch">
+ <settingConfig type="boolean" />
+ </setting>
+ <setting propertyKey="@Properties.listField" title="@Strings.ListTitle">
+ <settingConfig type="list">
+ <listEntry value="0">@Strings.ListA</listEntry>
+ <listEntry value="1">@Strings.ListB</listEntry>
+ <listEntry value="2">@Strings.ListC</listEntry>
+ </settingConfig>
+ </setting>
+ <setting propertyKey="@Properties.numberField" title="@Strings.NumberTitle">
+ <settingConfig type="numeric" min="1" max="300" errorMessage="@Strings.NumberError" />
+ </setting>
+ <setting propertyKey="@Properties.longField" title="@Strings.LongTitle">
+ <settingConfig type="numeric" min="0" />
+ </setting>
+ </group>
+</settings>
diff --git a/spike/settings/resources/strings/strings.xml b/spike/settings/resources/strings/strings.xml
new file mode 100644
index 0000000..302ffb0
--- /dev/null
+++ b/spike/settings/resources/strings/strings.xml
@@ -0,0 +1,31 @@
+<!--
+ Author: cirakg & claude ai
+ Summary: Strings of the settings spike (app name and settings titles).
+-->
+<strings>
+ <string id="AppName">Settings Spike</string>
+ <string id="Status">Status written by the watch</string>
+ <string id="GroupLength">Length tests</string>
+ <string id="GroupLengthHelp">Paste one test string per field, then save.</string>
+ <string id="Text1">Text 1</string>
+ <string id="Text2">Text 2</string>
+ <string id="Text3">Text 3</string>
+ <string id="Text4">Text 4</string>
+ <string id="Text5">Text 5</string>
+ <string id="Text6">Text 6</string>
+ <string id="Text7">Text 7</string>
+ <string id="Text8">Text 8</string>
+ <string id="TextPrompt">Paste a test string</string>
+ <string id="GroupOther">Other field types</string>
+ <string id="GroupOtherHelp">Password, switch, list and numbers.</string>
+ <string id="Secret">Password field</string>
+ <string id="SecretPrompt">Type anything (fake)</string>
+ <string id="Switch">Switch</string>
+ <string id="ListTitle">List</string>
+ <string id="ListA">Option A</string>
+ <string id="ListB">Option B</string>
+ <string id="ListC">Option C</string>
+ <string id="NumberTitle">Number (1-300)</string>
+ <string id="NumberError">Enter 1 to 300</string>
+ <string id="LongTitle">Long number</string>
+</strings>
diff --git a/spike/settings/source/DebugSelfTest.mc b/spike/settings/source/DebugSelfTest.mc
new file mode 100644
index 0000000..ef50148
--- /dev/null
+++ b/spike/settings/source/DebugSelfTest.mc
@@ -0,0 +1,35 @@
+// Author: cirakg & claude ai
+// Summary: Debug-only self-test: fills one field and runs the settings-change path once, so the simulator shows real output.
+//
+// The simulator has no phone, so this stands in for one save from Garmin
+// Connect: if Text 1 is empty it receives a fake 100-character test string,
+// then the normal onSettingsChanged path runs (analysis plus write-back).
+// It prints a "SPIKE ready" line that build-spike.sh waits for. Release
+// builds compile the empty (:release) version, so the beta you upload never
+// changes your settings by itself.
+
+import Toybox.Application.Properties;
+import Toybox.Lang;
+import Toybox.System;
+
+module DebugSelfTest {
+
+ // Fake 100-character test string in the same format as the phone tests.
+ (:debug)
+ const SAMPLE = "N=100|abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrP0050stuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghEND";
+
+ (:debug)
+ function run(app as SpikeApp) as Void {
+ var current = Properties.getValue("text1");
+ if (current == null || (current instanceof String && current.length() == 0)) {
+ Properties.setValue("text1", SAMPLE);
+ app.onSettingsChanged();
+ }
+ System.println("SPIKE ready total_chars=" + app.report.totalChars + " changes=" + app.report.changeCount
+ + " status='" + Properties.getValue("statusField") + "'");
+ }
+
+ (:release)
+ function run(app as SpikeApp) as Void {
+ }
+}
diff --git a/spike/settings/source/SpikeApp.mc b/spike/settings/source/SpikeApp.mc
new file mode 100644
index 0000000..c5adcb2
--- /dev/null
+++ b/spike/settings/source/SpikeApp.mc
@@ -0,0 +1,43 @@
+// Author: cirakg & claude ai
+// Summary: Settings spike entry point: analyses every setting on start and on each change, and writes a status back.
+//
+// The spike measures what Garmin doesn't document (README section 16, M1):
+// how long a text setting may be, which characters survive, whether values
+// the watch writes back reach the phone, and when onSettingsChanged fires.
+// It is a throwaway app with its own app id; nothing here is reused by OTP
+// Wallet except the measured numbers.
+
+import Toybox.Application;
+import Toybox.Lang;
+import Toybox.WatchUi;
+
+class SpikeApp extends Application.AppBase {
+
+ // The analysis shown on screen, refreshed on start and on every change.
+ var report as SpikeReport;
+
+ function initialize() {
+ AppBase.initialize();
+ report = new SpikeReport();
+ }
+
+ // Read the values the watch already has (from the last sync).
+ function onStart(state as Dictionary?) as Void {
+ report.analyseOnStart();
+ DebugSelfTest.run(self);
+ }
+
+ function onStop(state as Dictionary?) as Void {
+ }
+
+ function getInitialView() as [Views] or [Views, InputDelegates] {
+ var view = new SpikeView(report);
+ return [view, new SpikeDelegate(view)];
+ }
+
+ // Called when you save the settings on the phone while the app runs.
+ function onSettingsChanged() as Void {
+ report.settingsChanged();
+ WatchUi.requestUpdate();
+ }
+}
diff --git a/spike/settings/source/SpikeDelegate.mc b/spike/settings/source/SpikeDelegate.mc
new file mode 100644
index 0000000..94b3ece
--- /dev/null
+++ b/spike/settings/source/SpikeDelegate.mc
@@ -0,0 +1,35 @@
+// Author: cirakg & claude ai
+// Summary: Input of the spike screen: UP/DOWN or swipes scroll, START/tap jumps back to the top.
+
+import Toybox.Lang;
+import Toybox.WatchUi;
+
+class SpikeDelegate extends WatchUi.BehaviorDelegate {
+
+ // Lines moved per button press or swipe.
+ private const SCROLL_STEP = 3;
+ // Large enough to reach the first line from anywhere.
+ private const TO_TOP = -1000;
+
+ private var _view as SpikeView;
+
+ function initialize(view as SpikeView) {
+ BehaviorDelegate.initialize();
+ _view = view;
+ }
+
+ function onNextPage() as Boolean {
+ _view.scroll(SCROLL_STEP);
+ return true;
+ }
+
+ function onPreviousPage() as Boolean {
+ _view.scroll(-SCROLL_STEP);
+ return true;
+ }
+
+ function onSelect() as Boolean {
+ _view.scroll(TO_TOP);
+ return true;
+ }
+}
diff --git a/spike/settings/source/SpikeReport.mc b/spike/settings/source/SpikeReport.mc
new file mode 100644
index 0000000..09f27fa
--- /dev/null
+++ b/spike/settings/source/SpikeReport.mc
@@ -0,0 +1,144 @@
+// Author: cirakg & claude ai
+// Summary: Reads every spike setting, measures lengths and characters, writes the status back, builds the screen lines.
+//
+// Per text field it reports: length in characters and UTF-8 bytes, the first
+// 12 and last 8 characters (the test strings start with "N=<length>|" and
+// end with "END", so truncation is obvious), and which of the characters
+// & < > \ " { } % and non-ASCII it contains.
+//
+// Write-back test: on every settings change the watch writes a status line
+// into the read-only "statusField" and clears "secretField". Opening the
+// settings on the phone afterwards shows whether the phone displays values
+// the watch wrote.
+
+import Toybox.Application;
+import Toybox.Application.Properties;
+import Toybox.Lang;
+import Toybox.System;
+
+module SpikeKeys {
+ const TEXT_FIELDS = ["text1", "text2", "text3", "text4", "text5", "text6", "text7", "text8"];
+ const SECRET = "secretField";
+ const STATUS = "statusField";
+ const OTHER_FIELDS = ["switchField", "listField", "numberField", "longField"];
+ const HEAD_CHARS = 12;
+ const TAIL_CHARS = 8;
+ const ASCII_BACKSLASH = 92;
+ const ASCII_QUOTE = 34;
+}
+
+class SpikeReport {
+
+ // Lines for SpikeView, rebuilt by analyse().
+ var lines as Array<String> = [] as Array<String>;
+ // onSettingsChanged calls since the app started, and the time of the last.
+ var changeCount as Number = 0;
+ var lastChange as String = "never";
+ // Totals over the eight text fields, from the latest analysis.
+ var totalChars as Number = 0;
+ var totalBytes as Number = 0;
+ // Length of the password field when it was last read (before clearing).
+ var passwordReceived as Number = 0;
+ // The status line the watch wrote last ("" before the first change).
+ var lastWriteBack as String = "";
+
+ function initialize() {
+ }
+
+ // On start: no write-back, just show what the last sync delivered.
+ function analyseOnStart() as Void {
+ passwordReceived = readString(SpikeKeys.SECRET).length();
+ analyse();
+ }
+
+ // Settings were saved on the phone: measure, write back, show.
+ function settingsChanged() as Void {
+ changeCount++;
+ lastChange = clockText();
+ passwordReceived = readString(SpikeKeys.SECRET).length();
+ analyse();
+ lastWriteBack = "seen " + totalChars + " chars at " + lastChange + ", change #" + changeCount;
+ Properties.setValue(SpikeKeys.STATUS, lastWriteBack);
+ Properties.setValue(SpikeKeys.SECRET, "");
+ analyse();
+ }
+
+ // Rebuild every screen line from the current property values.
+ function analyse() as Void {
+ lines = [] as Array<String>;
+ totalChars = 0;
+ totalBytes = 0;
+ var fieldLines = [] as Array<String>;
+ for (var i = 0; i < SpikeKeys.TEXT_FIELDS.size(); i++) {
+ describeText(fieldLines, "T" + (i + 1), readString(SpikeKeys.TEXT_FIELDS[i]));
+ }
+ lines.add("Settings spike");
+ lines.add("Changes seen: " + changeCount);
+ lines.add("Last change: " + lastChange);
+ lines.add("Total: " + totalChars + " chars, " + totalBytes + " B");
+ lines.addAll(fieldLines);
+ lines.add("Password: got " + passwordReceived + " chars,");
+ lines.add(" now " + readString(SpikeKeys.SECRET).length() + " (watch clears it)");
+ for (var i = 0; i < SpikeKeys.OTHER_FIELDS.size(); i++) {
+ lines.add(SpikeKeys.OTHER_FIELDS[i] + ": " + valueText(SpikeKeys.OTHER_FIELDS[i]));
+ }
+ lines.add("Status now: " + readString(SpikeKeys.STATUS));
+ lines.add("Wrote: " + (lastWriteBack.equals("") ? "nothing yet" : lastWriteBack));
+ }
+
+ // Length, ends and special characters of one text field.
+ private function describeText(out as Array<String>, label as String, value as String) as Void {
+ var chars = value.length();
+ var bytes = value.toUtf8Array().size();
+ totalChars += chars;
+ totalBytes += bytes;
+ out.add(label + ": " + chars + " chars, " + bytes + " B");
+ if (chars == 0) {
+ return;
+ }
+ var head = part(value, 0, chars < SpikeKeys.HEAD_CHARS ? chars : SpikeKeys.HEAD_CHARS);
+ var tailStart = chars > SpikeKeys.TAIL_CHARS ? chars - SpikeKeys.TAIL_CHARS : 0;
+ out.add(" " + head + " .. " + part(value, tailStart, chars));
+ out.add(" has: " + specialsIn(value, chars != bytes));
+ }
+
+ // Which of the characters the plan worries about occur in the value.
+ private function specialsIn(value as String, nonAscii as Boolean) as String {
+ var probes = [
+ "&", "<", ">", SpikeKeys.ASCII_BACKSLASH.toChar().toString(),
+ SpikeKeys.ASCII_QUOTE.toChar().toString(), "{", "}", "%"
+ ];
+ var found = "";
+ for (var i = 0; i < probes.size(); i++) {
+ if (value.find(probes[i]) != null) {
+ found += probes[i] + " ";
+ }
+ }
+ if (nonAscii) {
+ found += "non-ASCII";
+ }
+ return found.equals("") ? "none" : found;
+ }
+
+ // A property as a String; anything missing or of another type reads as "".
+ private function readString(key as String) as String {
+ var value = Properties.getValue(key);
+ return value instanceof String ? value : "";
+ }
+
+ // Any property as display text.
+ private function valueText(key as String) as String {
+ // getValue throws for unknown keys and never returns null for declared ones.
+ return Properties.getValue(key).toString() as String;
+ }
+
+ private function part(value as String, startIndex as Number, endIndex as Number) as String {
+ var result = value.substring(startIndex, endIndex);
+ return result == null ? "" : result;
+ }
+
+ private function clockText() as String {
+ var time = System.getClockTime();
+ return time.hour.format("%02d") + ":" + time.min.format("%02d") + ":" + time.sec.format("%02d");
+ }
+}
diff --git a/spike/settings/source/SpikeView.mc b/spike/settings/source/SpikeView.mc
new file mode 100644
index 0000000..68bacc7
--- /dev/null
+++ b/spike/settings/source/SpikeView.mc
@@ -0,0 +1,53 @@
+// Author: cirakg & claude ai
+// Summary: Scrollable text screen showing the spike's measurements.
+//
+// Lines come from SpikeReport. The first visible line moves with
+// SpikeDelegate (UP/DOWN buttons or swipes); text stays in the middle band of
+// round screens so nothing is cut off at the top or bottom.
+
+import Toybox.Graphics;
+import Toybox.Lang;
+import Toybox.WatchUi;
+
+class SpikeView extends WatchUi.View {
+
+ // Share of the screen height left free at the top and at the bottom.
+ private const MARGIN_FRACTION = 0.14;
+
+ private var _report as SpikeReport;
+ private var _firstLine as Number = 0;
+
+ function initialize(report as SpikeReport) {
+ View.initialize();
+ _report = report;
+ }
+
+ // Move the first visible line by delta lines, staying within the text.
+ function scroll(delta as Number) as Void {
+ var last = _report.lines.size() - 1;
+ _firstLine += delta;
+ if (_firstLine > last) {
+ _firstLine = last;
+ }
+ if (_firstLine < 0) {
+ _firstLine = 0;
+ }
+ WatchUi.requestUpdate();
+ }
+
+ function onUpdate(dc as Graphics.Dc) as Void {
+ dc.setColor(Graphics.COLOR_WHITE, Graphics.COLOR_BLACK);
+ dc.clear();
+ var font = Graphics.FONT_XTINY;
+ var lineHeight = dc.getFontHeight(font);
+ var top = (dc.getHeight() * MARGIN_FRACTION).toNumber();
+ var bottom = dc.getHeight() - top;
+ var x = dc.getWidth() / 2;
+ var y = top;
+ var lines = _report.lines;
+ for (var i = _firstLine; i < lines.size() && y + lineHeight <= bottom; i++) {
+ dc.drawText(x, y, font, lines[i], Graphics.TEXT_JUSTIFY_CENTER);
+ y += lineHeight;
+ }
+ }
+}
diff --git a/spike/settings/test-strings/index.html b/spike/settings/test-strings/index.html
new file mode 100644
index 0000000..d330b52
--- /dev/null
+++ b/spike/settings/test-strings/index.html
@@ -0,0 +1,30 @@
+<!doctype html>
+<!-- Author: cirakg & claude ai -->
+<!-- Summary: Settings spike test strings with copy buttons; open on the phone, copy, paste into a settings field. -->
+<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
+<title>Settings spike test strings</title>
+<style>body{font-family:sans-serif;margin:16px}textarea{width:100%;font-family:monospace}
+button{font-size:1.1em;padding:6px 16px;margin:4px 0 16px}</style></head>
+<body><h1>Settings spike test strings</h1>
+<p>Tap Copy, then paste into one of the Text fields in the app settings. Expected lengths are in each heading.</p>
+<section><h2>100 characters</h2><textarea id="len-0100" readonly rows="3">N=100|abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrP0050stuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghEND</textarea><button onclick="copyText('len-0100')">Copy</button></section>
+<section><h2>255 characters</h2><textarea id="len-0255" readonly rows="3">N=255|abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrP0050stuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijkP0100lmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdP0150efghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwP0200xyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrEND</textarea><button onclick="copyText('len-0255')">Copy</button></section>
+<section><h2>256 characters</h2><textarea id="len-0256" readonly rows="3">N=256|abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrP0050stuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijkP0100lmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdP0150efghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwP0200xyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrsEND</textarea><button onclick="copyText('len-0256')">Copy</button></section>
+<section><h2>500 characters</h2><textarea id="len-0500" readonly rows="3">N=500|abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrP0050stuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijkP0100lmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdP0150efghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwP0200xyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopP0250qrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghiP0300jklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabP0350cdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuP0400vwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnP0450opqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdEND</textarea><button onclick="copyText('len-0500')">Copy</button></section>
+<section><h2>800 characters</h2><textarea id="len-0800" readonly rows="3">N=800|abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrP0050stuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijkP0100lmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdP0150efghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwP0200xyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopP0250qrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghiP0300jklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabP0350cdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuP0400vwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnP0450opqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefgP0500hijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzP0550abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrsP0600tuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklP0650mnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdeP0700fghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxP0750yzabcdefghijklmnopqrstuvwxyzabcdefghijklmnEND</textarea><button onclick="copyText('len-0800')">Copy</button></section>
+<section><h2>801 characters</h2><textarea id="len-0801" readonly rows="3">N=801|abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrP0050stuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijkP0100lmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdP0150efghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwP0200xyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopP0250qrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghiP0300jklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabP0350cdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuP0400vwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnP0450opqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefgP0500hijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzP0550abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrsP0600tuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklP0650mnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdeP0700fghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxP0750yzabcdefghijklmnopqrstuvwxyzabcdefghijklmnoEND</textarea><button onclick="copyText('len-0801')">Copy</button></section>
+<section><h2>1000 characters</h2><textarea id="len-1000" readonly rows="3">N=1000|abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqP0050rstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijP0100klmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcP0150defghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvP0200wxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnoP0250pqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghP0300ijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzaP0350bcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstP0400uvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmP0450nopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefP0500ghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyP0550zabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrP0600stuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijkP0650lmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdP0700efghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwP0750xyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopP0800qrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghiP0850jklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabP0900cdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuP0950vwxyzabcdefghijklmnopqrstuvwxyzabcdefghijkEND</textarea><button onclick="copyText('len-1000')">Copy</button></section>
+<section><h2>2000 characters</h2><textarea id="len-2000" readonly rows="3">N=2000|abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqP0050rstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijP0100klmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcP0150defghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvP0200wxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnoP0250pqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghP0300ijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzaP0350bcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstP0400uvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmP0450nopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefP0500ghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyP0550zabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrP0600stuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijkP0650lmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdP0700efghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwP0750xyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopP0800qrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghiP0850jklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabP0900cdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuP0950vwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnP1000opqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefgP1050hijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzP1100abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrsP1150tuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklP1200mnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdeP1250fghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxP1300yzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqP1350rstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijP1400klmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcP1450defghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvP1500wxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnoP1550pqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghP1600ijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzaP1650bcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstP1700uvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmP1750nopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefP1800ghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyP1850zabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrP1900stuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijkP1950lmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzaEND</textarea><button onclick="copyText('len-2000')">Copy</button></section>
+<section><h2>4000 characters</h2><textarea id="len-4000" readonly rows="3">N=4000|abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqP0050rstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijP0100klmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcP0150defghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvP0200wxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnoP0250pqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghP0300ijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzaP0350bcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstP0400uvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmP0450nopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefP0500ghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyP0550zabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrP0600stuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijkP0650lmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdP0700efghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwP0750xyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopP0800qrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghiP0850jklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabP0900cdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuP0950vwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnP1000opqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefgP1050hijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzP1100abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrsP1150tuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklP1200mnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdeP1250fghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxP1300yzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqP1350rstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijP1400klmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcP1450defghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvP1500wxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnoP1550pqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghP1600ijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzaP1650bcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstP1700uvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmP1750nopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefP1800ghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyP1850zabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrP1900stuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijkP1950lmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdP2000efghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwP2050xyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopP2100qrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghiP2150jklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabP2200cdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuP2250vwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnP2300opqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefgP2350hijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzP2400abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrsP2450tuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklP2500mnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdeP2550fghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxP2600yzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqP2650rstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijP2700klmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcP2750defghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvP2800wxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnoP2850pqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghP2900ijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzaP2950bcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstP3000uvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmP3050nopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefP3100ghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyP3150zabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrP3200stuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijkP3250lmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdP3300efghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwP3350xyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopP3400qrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghiP3450jklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabP3500cdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuP3550vwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnP3600opqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefgP3650hijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzP3700abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrsP3750tuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklP3800mnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdeP3850fghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxP3900yzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqP3950rstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefgEND</textarea><button onclick="copyText('len-4000')">Copy</button></section>
+<section><h2>8000 characters</h2><textarea id="len-8000" readonly rows="3">N=8000|abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqP0050rstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijP0100klmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcP0150defghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvP0200wxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnoP0250pqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghP0300ijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzaP0350bcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstP0400uvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmP0450nopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefP0500ghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyP0550zabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrP0600stuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijkP0650lmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdP0700efghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwP0750xyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopP0800qrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghiP0850jklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabP0900cdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuP0950vwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnP1000opqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefgP1050hijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzP1100abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrsP1150tuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklP1200mnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdeP1250fghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxP1300yzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqP1350rstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijP1400klmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcP1450defghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvP1500wxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnoP1550pqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghP1600ijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzaP1650bcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstP1700uvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmP1750nopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefP1800ghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyP1850zabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrP1900stuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijkP1950lmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdP2000efghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwP2050xyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopP2100qrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghiP2150jklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabP2200cdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuP2250vwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnP2300opqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefgP2350hijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzP2400abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrsP2450tuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklP2500mnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdeP2550fghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxP2600yzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqP2650rstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijP2700klmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcP2750defghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvP2800wxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnoP2850pqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghP2900ijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzaP2950bcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstP3000uvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmP3050nopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefP3100ghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyP3150zabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrP3200stuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijkP3250lmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdP3300efghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwP3350xyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopP3400qrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghiP3450jklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabP3500cdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuP3550vwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnP3600opqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefgP3650hijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzP3700abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrsP3750tuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklP3800mnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdeP3850fghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxP3900yzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqP3950rstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijP4000klmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcP4050defghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvP4100wxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnoP4150pqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghP4200ijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzaP4250bcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstP4300uvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmP4350nopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefP4400ghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyP4450zabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrP4500stuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijkP4550lmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdP4600efghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwP4650xyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopP4700qrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghiP4750jklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabP4800cdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuP4850vwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnP4900opqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefgP4950hijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzP5000abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrsP5050tuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklP5100mnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdeP5150fghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxP5200yzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqP5250rstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijP5300klmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcP5350defghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvP5400wxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnoP5450pqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghP5500ijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzaP5550bcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstP5600uvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmP5650nopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefP5700ghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyP5750zabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrP5800stuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijkP5850lmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdP5900efghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwP5950xyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopP6000qrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghiP6050jklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabP6100cdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuP6150vwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnP6200opqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefgP6250hijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzP6300abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrsP6350tuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklP6400mnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdeP6450fghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxP6500yzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqP6550rstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijP6600klmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcP6650defghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvP6700wxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnoP6750pqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghP6800ijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzaP6850bcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstP6900uvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmP6950nopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefP7000ghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyP7050zabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrP7100stuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijkP7150lmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdP7200efghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwP7250xyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopP7300qrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghiP7350jklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabP7400cdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuP7450vwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnP7500opqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefgP7550hijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzP7600abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrsP7650tuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklP7700mnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdeP7750fghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxP7800yzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqP7850rstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijP7900klmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcP7950defghijklmnopqrstuvwxyzabcdefghijklmnopqrsEND</textarea><button onclick="copyText('len-8000')">Copy</button></section>
+<section><h2>special characters</h2><textarea id="special" readonly rows="3">special: &amp; &lt; &gt; \ &quot; { } % | non-ASCII: äöü ß € 😀 | otpauth://totp/Fake:alice?secret=JBSWY3DPEHPK3PXP&amp;issuer=Fake&amp;digits=6 | {&quot;name&quot;:&quot;Fake&quot;,&quot;path&quot;:&quot;a\/b&quot;} END</textarea><button onclick="copyText('special')">Copy</button></section>
+<script>
+function copyText(id) {
+ const area = document.getElementById(id);
+ area.select();
+ const done = () => alert("Copied " + area.value.length + " characters");
+ if (navigator.clipboard && window.isSecureContext) {
+ navigator.clipboard.writeText(area.value).then(done, () => { document.execCommand("copy"); done(); });
+ } else { document.execCommand("copy"); done(); }
+}
+</script></body></html>
diff --git a/spike/settings/test-strings/len-0100.txt b/spike/settings/test-strings/len-0100.txt
new file mode 100644
index 0000000..a4f972e
--- /dev/null
+++ b/spike/settings/test-strings/len-0100.txt
@@ -0,0 +1,5 @@
+# Author: cirakg & claude ai
+# Summary: Settings spike test string: 100 characters.
+# Copy ONLY the last line (the test string) into a settings field.
+
+N=100|abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrP0050stuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghEND
diff --git a/spike/settings/test-strings/len-0255.txt b/spike/settings/test-strings/len-0255.txt
new file mode 100644
index 0000000..b8cc080
--- /dev/null
+++ b/spike/settings/test-strings/len-0255.txt
@@ -0,0 +1,5 @@
+# Author: cirakg & claude ai
+# Summary: Settings spike test string: 255 characters.
+# Copy ONLY the last line (the test string) into a settings field.
+
+N=255|abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrP0050stuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijkP0100lmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdP0150efghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwP0200xyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrEND
diff --git a/spike/settings/test-strings/len-0256.txt b/spike/settings/test-strings/len-0256.txt
new file mode 100644
index 0000000..bbd0ca2
--- /dev/null
+++ b/spike/settings/test-strings/len-0256.txt
@@ -0,0 +1,5 @@
+# Author: cirakg & claude ai
+# Summary: Settings spike test string: 256 characters.
+# Copy ONLY the last line (the test string) into a settings field.
+
+N=256|abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrP0050stuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijkP0100lmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdP0150efghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwP0200xyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrsEND
diff --git a/spike/settings/test-strings/len-0500.txt b/spike/settings/test-strings/len-0500.txt
new file mode 100644
index 0000000..80f5d25
--- /dev/null
+++ b/spike/settings/test-strings/len-0500.txt
@@ -0,0 +1,5 @@
+# Author: cirakg & claude ai
+# Summary: Settings spike test string: 500 characters.
+# Copy ONLY the last line (the test string) into a settings field.
+
+N=500|abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrP0050stuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijkP0100lmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdP0150efghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwP0200xyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopP0250qrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghiP0300jklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabP0350cdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuP0400vwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnP0450opqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdEND
diff --git a/spike/settings/test-strings/len-0800.txt b/spike/settings/test-strings/len-0800.txt
new file mode 100644
index 0000000..bc90b28
--- /dev/null
+++ b/spike/settings/test-strings/len-0800.txt
@@ -0,0 +1,5 @@
+# Author: cirakg & claude ai
+# Summary: Settings spike test string: 800 characters.
+# Copy ONLY the last line (the test string) into a settings field.
+
+N=800|abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrP0050stuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijkP0100lmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdP0150efghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwP0200xyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopP0250qrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghiP0300jklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabP0350cdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuP0400vwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnP0450opqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefgP0500hijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzP0550abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrsP0600tuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklP0650mnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdeP0700fghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxP0750yzabcdefghijklmnopqrstuvwxyzabcdefghijklmnEND
diff --git a/spike/settings/test-strings/len-0801.txt b/spike/settings/test-strings/len-0801.txt
new file mode 100644
index 0000000..38ef08d
--- /dev/null
+++ b/spike/settings/test-strings/len-0801.txt
@@ -0,0 +1,5 @@
+# Author: cirakg & claude ai
+# Summary: Settings spike test string: 801 characters.
+# Copy ONLY the last line (the test string) into a settings field.
+
+N=801|abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrP0050stuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijkP0100lmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdP0150efghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwP0200xyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopP0250qrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghiP0300jklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabP0350cdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuP0400vwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnP0450opqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefgP0500hijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzP0550abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrsP0600tuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklP0650mnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdeP0700fghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxP0750yzabcdefghijklmnopqrstuvwxyzabcdefghijklmnoEND
diff --git a/spike/settings/test-strings/len-1000.txt b/spike/settings/test-strings/len-1000.txt
new file mode 100644
index 0000000..0979c3c
--- /dev/null
+++ b/spike/settings/test-strings/len-1000.txt
@@ -0,0 +1,5 @@
+# Author: cirakg & claude ai
+# Summary: Settings spike test string: 1000 characters.
+# Copy ONLY the last line (the test string) into a settings field.
+
+N=1000|abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqP0050rstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijP0100klmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcP0150defghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvP0200wxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnoP0250pqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghP0300ijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzaP0350bcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstP0400uvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmP0450nopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefP0500ghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyP0550zabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrP0600stuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijkP0650lmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdP0700efghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwP0750xyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopP0800qrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghiP0850jklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabP0900cdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuP0950vwxyzabcdefghijklmnopqrstuvwxyzabcdefghijkEND
diff --git a/spike/settings/test-strings/len-2000.txt b/spike/settings/test-strings/len-2000.txt
new file mode 100644
index 0000000..0358ab2
--- /dev/null
+++ b/spike/settings/test-strings/len-2000.txt
@@ -0,0 +1,5 @@
+# Author: cirakg & claude ai
+# Summary: Settings spike test string: 2000 characters.
+# Copy ONLY the last line (the test string) into a settings field.
+
+N=2000|abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqP0050rstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijP0100klmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcP0150defghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvP0200wxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnoP0250pqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghP0300ijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzaP0350bcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstP0400uvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmP0450nopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefP0500ghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyP0550zabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrP0600stuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijkP0650lmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdP0700efghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwP0750xyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopP0800qrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghiP0850jklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabP0900cdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuP0950vwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnP1000opqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefgP1050hijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzP1100abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrsP1150tuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklP1200mnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdeP1250fghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxP1300yzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqP1350rstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijP1400klmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcP1450defghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvP1500wxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnoP1550pqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghP1600ijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzaP1650bcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstP1700uvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmP1750nopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefP1800ghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyP1850zabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrP1900stuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijkP1950lmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzaEND
diff --git a/spike/settings/test-strings/len-4000.txt b/spike/settings/test-strings/len-4000.txt
new file mode 100644
index 0000000..934e0d5
--- /dev/null
+++ b/spike/settings/test-strings/len-4000.txt
@@ -0,0 +1,5 @@
+# Author: cirakg & claude ai
+# Summary: Settings spike test string: 4000 characters.
+# Copy ONLY the last line (the test string) into a settings field.
+
+N=4000|abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqP0050rstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijP0100klmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcP0150defghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvP0200wxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnoP0250pqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghP0300ijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzaP0350bcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstP0400uvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmP0450nopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefP0500ghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyP0550zabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrP0600stuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijkP0650lmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdP0700efghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwP0750xyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopP0800qrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghiP0850jklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabP0900cdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuP0950vwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnP1000opqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefgP1050hijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzP1100abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrsP1150tuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklP1200mnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdeP1250fghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxP1300yzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqP1350rstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijP1400klmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcP1450defghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvP1500wxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnoP1550pqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghP1600ijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzaP1650bcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstP1700uvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmP1750nopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefP1800ghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyP1850zabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrP1900stuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijkP1950lmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdP2000efghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwP2050xyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopP2100qrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghiP2150jklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabP2200cdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuP2250vwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnP2300opqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefgP2350hijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzP2400abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrsP2450tuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklP2500mnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdeP2550fghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxP2600yzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqP2650rstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijP2700klmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcP2750defghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvP2800wxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnoP2850pqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghP2900ijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzaP2950bcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstP3000uvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmP3050nopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefP3100ghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyP3150zabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrP3200stuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijkP3250lmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdP3300efghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwP3350xyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopP3400qrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghiP3450jklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabP3500cdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuP3550vwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnP3600opqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefgP3650hijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzP3700abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrsP3750tuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklP3800mnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdeP3850fghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxP3900yzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqP3950rstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefgEND
diff --git a/spike/settings/test-strings/len-8000.txt b/spike/settings/test-strings/len-8000.txt
new file mode 100644
index 0000000..1f75f8d
--- /dev/null
+++ b/spike/settings/test-strings/len-8000.txt
@@ -0,0 +1,5 @@
+# Author: cirakg & claude ai
+# Summary: Settings spike test string: 8000 characters.
+# Copy ONLY the last line (the test string) into a settings field.
+
+N=8000|abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqP0050rstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijP0100klmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcP0150defghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvP0200wxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnoP0250pqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghP0300ijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzaP0350bcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstP0400uvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmP0450nopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefP0500ghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyP0550zabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrP0600stuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijkP0650lmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdP0700efghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwP0750xyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopP0800qrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghiP0850jklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabP0900cdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuP0950vwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnP1000opqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefgP1050hijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzP1100abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrsP1150tuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklP1200mnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdeP1250fghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxP1300yzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqP1350rstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijP1400klmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcP1450defghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvP1500wxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnoP1550pqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghP1600ijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzaP1650bcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstP1700uvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmP1750nopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefP1800ghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyP1850zabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrP1900stuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijkP1950lmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdP2000efghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwP2050xyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopP2100qrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghiP2150jklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabP2200cdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuP2250vwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnP2300opqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefgP2350hijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzP2400abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrsP2450tuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklP2500mnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdeP2550fghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxP2600yzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqP2650rstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijP2700klmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcP2750defghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvP2800wxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnoP2850pqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghP2900ijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzaP2950bcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstP3000uvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmP3050nopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefP3100ghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyP3150zabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrP3200stuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijkP3250lmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdP3300efghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwP3350xyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopP3400qrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghiP3450jklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabP3500cdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuP3550vwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnP3600opqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefgP3650hijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzP3700abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrsP3750tuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklP3800mnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdeP3850fghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxP3900yzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqP3950rstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijP4000klmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcP4050defghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvP4100wxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnoP4150pqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghP4200ijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzaP4250bcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstP4300uvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmP4350nopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefP4400ghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyP4450zabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrP4500stuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijkP4550lmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdP4600efghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwP4650xyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopP4700qrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghiP4750jklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabP4800cdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuP4850vwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnP4900opqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefgP4950hijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzP5000abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrsP5050tuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklP5100mnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdeP5150fghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxP5200yzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqP5250rstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijP5300klmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcP5350defghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvP5400wxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnoP5450pqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghP5500ijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzaP5550bcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstP5600uvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmP5650nopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefP5700ghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyP5750zabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrP5800stuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijkP5850lmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdP5900efghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwP5950xyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopP6000qrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghiP6050jklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabP6100cdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuP6150vwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnP6200opqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefgP6250hijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzP6300abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrsP6350tuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklP6400mnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdeP6450fghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxP6500yzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqP6550rstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijP6600klmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcP6650defghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvP6700wxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnoP6750pqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghP6800ijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzaP6850bcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstP6900uvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmP6950nopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefP7000ghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyP7050zabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrP7100stuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijkP7150lmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdP7200efghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwP7250xyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopP7300qrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghiP7350jklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabP7400cdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuP7450vwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnP7500opqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefgP7550hijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzP7600abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrsP7650tuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklP7700mnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdeP7750fghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxP7800yzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqP7850rstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijP7900klmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcP7950defghijklmnopqrstuvwxyzabcdefghijklmnopqrsEND
diff --git a/spike/settings/test-strings/make_test_strings.py b/spike/settings/test-strings/make_test_strings.py
new file mode 100644
index 0000000..8d43977
--- /dev/null
+++ b/spike/settings/test-strings/make_test_strings.py
@@ -0,0 +1,92 @@
+#!/usr/bin/env python3
+# Author: cirakg & claude ai
+# Summary: Generate the settings-spike test strings: one .txt per string and an index.html with copy buttons.
+"""Generate the test strings for the M1 settings spike (docs/SETTINGS_SPIKE.md).
+
+Length strings look like
+ N=1000|abcdef...P0050...P0100...xyzEND
+"N=<length>|" says how long the string should be, a marker "P<position>"
+sits at every 50th character, and "END" closes it. If the phone app cuts a
+string, the watch shows a wrong length and no "END"; the last marker it shows
+tells roughly where the cut happened. Only letters, digits, "=", "|" are
+used, so the length tests don't also test character handling.
+
+special.txt holds the characters that may be rejected (& < > \\ " { } %),
+non-ASCII text, a fake otpauth URI and a JSON fragment.
+
+Usage: python3 make_test_strings.py (writes into its own folder)
+"""
+
+from __future__ import annotations
+
+import html
+from pathlib import Path
+
+LENGTHS = (100, 255, 256, 500, 800, 801, 1000, 2000, 4000, 8000)
+SPECIAL = (
+ 'special: & < > \\ " { } % | non-ASCII: äöü ß € 😀 | '
+ "otpauth://totp/Fake:alice?secret=JBSWY3DPEHPK3PXP&issuer=Fake&digits=6 | "
+ '{"name":"Fake","path":"a\\/b"} END'
+)
+HEADER = (
+ "# Author: cirakg & claude ai\n# Summary: {summary}\n"
+ "# Copy ONLY the last line (the test string) into a settings field.\n\n"
+)
+LETTERS = "abcdefghijklmnopqrstuvwxyz"
+
+
+def marker_string(length: int) -> str:
+ """A string of exactly `length` characters with position markers."""
+ text = f"N={length}|"
+ i = 0
+ while len(text) < length - 3:
+ pos = len(text)
+ if pos % 50 == 0 and pos + 5 <= length - 3:
+ text += f"P{pos:04d}"
+ else:
+ text += LETTERS[i % len(LETTERS)]
+ i += 1
+ text += "END"
+ assert len(text) == length, (length, len(text))
+ return text
+
+
+def main() -> None:
+ here = Path(__file__).resolve().parent
+ entries = [(f"len-{n:04d}", f"{n} characters", marker_string(n)) for n in LENGTHS]
+ entries.append(("special", "special characters", SPECIAL))
+ for name, label, value in entries:
+ (here / f"{name}.txt").write_text(HEADER.format(summary=f"Settings spike test string: {label}.") + value + "\n")
+ cards = "\n".join(
+ f"<section><h2>{html.escape(label)}</h2>"
+ f'<textarea id="{name}" readonly rows="3">{html.escape(value)}</textarea>'
+ f"<button onclick=\"copyText('{name}')\">Copy</button></section>"
+ for name, label, value in entries
+ )
+ page = f"""<!doctype html>
+<!-- Author: cirakg & claude ai -->
+<!-- Summary: Settings spike test strings with copy buttons; open on the phone, copy, paste into a settings field. -->
+<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
+<title>Settings spike test strings</title>
+<style>body{{font-family:sans-serif;margin:16px}}textarea{{width:100%;font-family:monospace}}
+button{{font-size:1.1em;padding:6px 16px;margin:4px 0 16px}}</style></head>
+<body><h1>Settings spike test strings</h1>
+<p>Tap Copy, then paste into one of the Text fields in the app settings. Expected lengths are in each heading.</p>
+{cards}
+<script>
+function copyText(id) {{
+ const area = document.getElementById(id);
+ area.select();
+ const done = () => alert("Copied " + area.value.length + " characters");
+ if (navigator.clipboard && window.isSecureContext) {{
+ navigator.clipboard.writeText(area.value).then(done, () => {{ document.execCommand("copy"); done(); }});
+ }} else {{ document.execCommand("copy"); done(); }}
+}}
+</script></body></html>
+"""
+ (here / "index.html").write_text(page)
+ print(f"wrote {len(entries)} test strings and index.html into {here}")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/spike/settings/test-strings/special.txt b/spike/settings/test-strings/special.txt
new file mode 100644
index 0000000..38d0d79
--- /dev/null
+++ b/spike/settings/test-strings/special.txt
@@ -0,0 +1,5 @@
+# Author: cirakg & claude ai
+# Summary: Settings spike test string: special characters.
+# Copy ONLY the last line (the test string) into a settings field.
+
+special: & < > \ " { } % | non-ASCII: äöü ß € 😀 | otpauth://totp/Fake:alice?secret=JBSWY3DPEHPK3PXP&issuer=Fake&digits=6 | {"name":"Fake","path":"a\/b"} END