@underactive/pi-topping 0.1.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/menu.ts ADDED
@@ -0,0 +1,327 @@
1
+ /**
2
+ * menu.ts
3
+ *
4
+ * A reusable, dependency-light box-drawing toggle-menu component for Pi
5
+ * extensions, built on top of `@earendil-works/pi-tui`'s `Component`
6
+ * contract and `ctx.ui.custom()` overlay API.
7
+ *
8
+ * Renders a titled box containing one or more sections of boolean toggle
9
+ * items, plus an optional live-updating preview section driven by a
10
+ * caller-supplied render callback, e.g.:
11
+ *
12
+ * ╔═[ Pi Topping: Settings ]════════════════════════╗
13
+ * ╟─ Preview ─────────────────────────────────────╢
14
+ * ║ ║
15
+ * ║ ⠋ Cerebrating… ⣤⣤⣤⣤ (0m 03s · ↓ 84 tokens) ║
16
+ * ║ ║
17
+ * ╟─ Decorations ───────────────────────────────────╢
18
+ * ║ ▸ [■] Animated spinner ON ║
19
+ * ║ [ ] "Working..." text shimmer OFF ║
20
+ * ║ ║
21
+ * ╟──────────────────────────────────────────────────╢
22
+ * ║ ↑↓ move ␣ toggle ⏎ apply esc cancel ║
23
+ * ╚══════════════════════════════════════════[ 1/2 ]═╝
24
+ *
25
+ * Intended to be reused by any extension that needs a simple modal toggle
26
+ * menu; it has no dependency on this extension's own settings shape.
27
+ */
28
+
29
+ import type { ExtensionCommandContext, Theme } from "@earendil-works/pi-coding-agent";
30
+ import { type Component, Key, matchesKey, truncateToWidth, type TUI, visibleWidth } from "@earendil-works/pi-tui";
31
+
32
+ export interface MenuItem {
33
+ id: string;
34
+ label: string;
35
+ value: boolean;
36
+ }
37
+
38
+ export interface MenuSection {
39
+ title: string;
40
+ items: MenuItem[];
41
+ }
42
+
43
+ export interface MenuConfig {
44
+ title: string;
45
+ sections: MenuSection[];
46
+ hints?: string[];
47
+ /**
48
+ * Optional live preview renderer, shown in its own "Preview" section above
49
+ * the toggle sections. Called on every render with the menu's current
50
+ * (possibly toggled but not-yet-applied) values and the number of
51
+ * milliseconds elapsed since the menu opened, so callers can drive
52
+ * animation (e.g. a shimmer sweep or a scrolling meter). Return one string
53
+ * per preview line; lines may contain ANSI styling and are truncated/
54
+ * padded to fit automatically.
55
+ *
56
+ * When set, the menu re-renders itself on an interval (see
57
+ * `previewIntervalMs`) so the preview animates even without keyboard
58
+ * input.
59
+ */
60
+ preview?: (values: Record<string, boolean>, elapsedMs: number) => string[];
61
+ /** Interval in ms between preview re-renders when `preview` is set. Default 50. */
62
+ previewIntervalMs?: number;
63
+ }
64
+
65
+ export interface MenuResult<T> {
66
+ applied: boolean;
67
+ values: T;
68
+ }
69
+
70
+ const DEFAULT_HINTS = ["\u2191\u2193 move", "\u2423 toggle", "\u23ce apply", "esc cancel"];
71
+ const MIN_WIDTH = 36;
72
+ const MAX_WIDTH = 76;
73
+
74
+ interface FlatItem {
75
+ id: string;
76
+ label: string;
77
+ }
78
+
79
+ /** Internal Component implementing the box-drawing toggle menu. */
80
+ export class MenuComponent implements Component {
81
+ private readonly theme: Theme;
82
+ private readonly done: (result: MenuResult<Record<string, boolean>>) => void;
83
+ private readonly title: string;
84
+ private readonly sections: MenuSection[];
85
+ private readonly hints: string[];
86
+ private readonly initialValues: Record<string, boolean>;
87
+ private readonly values: Record<string, boolean>;
88
+ private readonly flat: FlatItem[];
89
+ private readonly previewFn: MenuConfig["preview"];
90
+ private readonly previewOrigin: number | undefined;
91
+ private previewTimer: ReturnType<typeof setInterval> | undefined;
92
+ private cursor = 0;
93
+ private cachedWidth: number | undefined;
94
+ private cachedLines: string[] | undefined;
95
+
96
+ constructor(
97
+ config: MenuConfig,
98
+ theme: Theme,
99
+ done: (result: MenuResult<Record<string, boolean>>) => void,
100
+ tui?: TUI,
101
+ ) {
102
+ this.theme = theme;
103
+ this.done = done;
104
+ this.title = config.title;
105
+ this.sections = config.sections;
106
+ this.hints = config.hints ?? DEFAULT_HINTS;
107
+ this.values = {};
108
+ this.flat = [];
109
+ for (const section of config.sections) {
110
+ for (const item of section.items) {
111
+ this.values[item.id] = item.value;
112
+ this.flat.push({ id: item.id, label: item.label });
113
+ }
114
+ }
115
+ this.initialValues = { ...this.values };
116
+
117
+ this.previewFn = config.preview;
118
+ if (this.previewFn) {
119
+ this.previewOrigin = Date.now();
120
+ if (tui) {
121
+ const intervalMs = config.previewIntervalMs ?? 50;
122
+ this.previewTimer = setInterval(() => {
123
+ this.invalidate();
124
+ tui.requestRender();
125
+ }, intervalMs);
126
+ }
127
+ }
128
+ }
129
+
130
+ /** Stops the preview animation timer, if any. Called automatically when the overlay closes. */
131
+ dispose(): void {
132
+ if (this.previewTimer) {
133
+ clearInterval(this.previewTimer);
134
+ this.previewTimer = undefined;
135
+ }
136
+ }
137
+
138
+ handleInput(data: string): void {
139
+ if (this.flat.length === 0) return;
140
+
141
+ if (matchesKey(data, Key.up)) {
142
+ this.cursor = (this.cursor - 1 + this.flat.length) % this.flat.length;
143
+ this.invalidate();
144
+ } else if (matchesKey(data, Key.down)) {
145
+ this.cursor = (this.cursor + 1) % this.flat.length;
146
+ this.invalidate();
147
+ } else if (matchesKey(data, Key.space)) {
148
+ const item = this.flat[this.cursor]!;
149
+ this.values[item.id] = !this.values[item.id];
150
+ this.invalidate();
151
+ } else if (matchesKey(data, Key.enter)) {
152
+ this.done({ applied: true, values: { ...this.values } });
153
+ } else if (matchesKey(data, Key.escape) || matchesKey(data, Key.ctrl("c"))) {
154
+ this.done({ applied: false, values: { ...this.initialValues } });
155
+ }
156
+ }
157
+
158
+ invalidate(): void {
159
+ this.cachedWidth = undefined;
160
+ this.cachedLines = undefined;
161
+ }
162
+
163
+ render(width: number): string[] {
164
+ if (this.cachedWidth === width && this.cachedLines) return this.cachedLines;
165
+ const lines = this.buildLines(width);
166
+ this.cachedWidth = width;
167
+ this.cachedLines = lines;
168
+ return lines;
169
+ }
170
+
171
+ private preferredWidth(previewLines: string[] | undefined): number {
172
+ const candidates: number[] = [MIN_WIDTH - 2, 5 + this.title.length];
173
+ if (previewLines && previewLines.length > 0) {
174
+ candidates.push(3 + "Preview".length);
175
+ // +1 for the leading space rendered in front of each preview line.
176
+ for (const line of previewLines) candidates.push(visibleWidth(line) + 1);
177
+ }
178
+ for (const section of this.sections) {
179
+ candidates.push(3 + section.title.length);
180
+ for (const item of section.items) {
181
+ // 8 fixed left columns + label + 2-space default gap + "OFF" (3) + 2 trailing.
182
+ candidates.push(8 + item.label.length + 2 + 3 + 2);
183
+ }
184
+ }
185
+ candidates.push(2 + this.hints.join(" ").length);
186
+ return 2 + Math.max(...candidates);
187
+ }
188
+
189
+ private buildLines(maxWidth: number): string[] {
190
+ // Computed once per render so a stateful `preview` callback (e.g. one
191
+ // driving a rolling activity meter) is never sampled twice per frame.
192
+ const previewLines = this.previewFn
193
+ ? this.previewFn(this.values, this.previewOrigin !== undefined ? Date.now() - this.previewOrigin : 0)
194
+ : undefined;
195
+
196
+ const desired = Math.max(MIN_WIDTH, Math.min(MAX_WIDTH, this.preferredWidth(previewLines)));
197
+ const boxWidth = Math.max(0, Math.min(desired, maxWidth));
198
+ const innerWidth = Math.max(0, boxWidth - 2);
199
+
200
+ const lines: string[] = [];
201
+ lines.push(this.renderTopBorder(innerWidth));
202
+
203
+ if (previewLines && previewLines.length > 0) {
204
+ lines.push(this.renderSectionDivider("Preview", innerWidth));
205
+ lines.push(this.renderBlankRow(innerWidth));
206
+ for (const line of previewLines) lines.push(this.renderContentRow(` ${line}`, innerWidth));
207
+ lines.push(this.renderBlankRow(innerWidth));
208
+ }
209
+
210
+ let flatIndex = 0;
211
+ for (const section of this.sections) {
212
+ lines.push(this.renderSectionDivider(section.title, innerWidth));
213
+ for (const item of section.items) {
214
+ lines.push(this.renderItemRow(item, flatIndex === this.cursor, innerWidth));
215
+ flatIndex++;
216
+ }
217
+ lines.push(this.renderBlankRow(innerWidth));
218
+ }
219
+
220
+ lines.push(this.renderSeparator(innerWidth));
221
+ lines.push(this.renderHintsRow(innerWidth));
222
+ lines.push(this.renderBottomBorder(innerWidth));
223
+
224
+ return lines.map((line) => truncateToWidth(line, boxWidth, ""));
225
+ }
226
+
227
+ private wrap(left: string, content: string, right: string): string {
228
+ const th = this.theme;
229
+ return th.fg("accent", left) + content + th.fg("accent", right);
230
+ }
231
+
232
+ private renderTopBorder(innerWidth: number): string {
233
+ const th = this.theme;
234
+ const maxTitleLen = Math.max(0, innerWidth - 5);
235
+ const title = this.title.length > maxTitleLen ? truncateToWidth(this.title, maxTitleLen) : this.title;
236
+ const fillCount = Math.max(0, innerWidth - 5 - visibleWidth(title));
237
+ const content = th.fg("accent", `\u2550[ ${th.bold(title)} `) + th.fg("accent", `]${"\u2550".repeat(fillCount)}`);
238
+ return this.wrap("\u2554", content, "\u2557");
239
+ }
240
+
241
+ private renderBottomBorder(innerWidth: number): string {
242
+ const counter = `[ ${this.cursor + 1}/${this.flat.length} ]`;
243
+ const fillCount = Math.max(0, innerWidth - counter.length);
244
+ const content = `${"\u2550".repeat(fillCount)}${counter}`;
245
+ return this.wrap("\u255a", this.theme.fg("accent", content), "\u255d");
246
+ }
247
+
248
+ private renderSectionDivider(title: string, innerWidth: number): string {
249
+ const th = this.theme;
250
+ const maxTitleLen = Math.max(0, innerWidth - 3);
251
+ const shownTitle = title.length > maxTitleLen ? truncateToWidth(title, maxTitleLen) : title;
252
+ const fillCount = Math.max(0, innerWidth - 3 - visibleWidth(shownTitle));
253
+ const content = th.fg("accent", `\u2500 ${shownTitle} ${"\u2500".repeat(fillCount)}`);
254
+ return this.wrap("\u255f", content, "\u2562");
255
+ }
256
+
257
+ private renderSeparator(innerWidth: number): string {
258
+ const content = this.theme.fg("accent", "\u2500".repeat(innerWidth));
259
+ return this.wrap("\u255f", content, "\u2562");
260
+ }
261
+
262
+ private renderBlankRow(innerWidth: number): string {
263
+ return this.wrap("\u2551", " ".repeat(innerWidth), "\u2551");
264
+ }
265
+
266
+ /** Pads/truncates an already-styled content string to exactly `innerWidth` and wraps it in border chars. */
267
+ private renderContentRow(content: string, innerWidth: number): string {
268
+ const shown = visibleWidth(content) > innerWidth ? truncateToWidth(content, innerWidth) : content;
269
+ const pad = Math.max(0, innerWidth - visibleWidth(shown));
270
+ return this.wrap("\u2551", `${shown}${" ".repeat(pad)}`, "\u2551");
271
+ }
272
+
273
+ private renderHintsRow(innerWidth: number): string {
274
+ const plain = ` ${this.hints.join(" ")}`;
275
+ return this.renderContentRow(this.theme.fg("dim", plain), innerWidth);
276
+ }
277
+
278
+ private renderItemRow(item: MenuItem, selected: boolean, innerWidth: number): string {
279
+ const th = this.theme;
280
+ const value = this.values[item.id]!;
281
+ const marker = selected ? "\u25b8" : " ";
282
+ const box = value ? "\u25a0" : " ";
283
+ const stateWord = value ? "ON" : "OFF";
284
+ const rightPlain = `${stateWord} `;
285
+ const fixedLeftLen = 8; // " " + marker(1) + " " + "[" + box(1) + "]" + " "
286
+ const minGap = 1;
287
+ const maxLabelLen = Math.max(0, innerWidth - fixedLeftLen - rightPlain.length - minGap);
288
+ const label = item.label.length > maxLabelLen ? truncateToWidth(item.label, maxLabelLen) : item.label;
289
+
290
+ const leftPlain = ` ${marker} [${box}] ${label}`;
291
+ const gap = Math.max(1, innerWidth - visibleWidth(leftPlain) - visibleWidth(rightPlain));
292
+
293
+ const markerColored = selected ? th.fg("accent", marker) : marker;
294
+ const boxColored = value ? th.fg("success", box) : th.fg("muted", box);
295
+ const labelColored = th.fg("text", label);
296
+ const stateColored = value ? th.fg("success", stateWord) : th.fg("muted", stateWord);
297
+
298
+ const content = ` ${markerColored} [${boxColored}] ${labelColored}${" ".repeat(gap)}${stateColored} `;
299
+ return this.wrap("\u2551", content, "\u2551");
300
+ }
301
+ }
302
+
303
+ /**
304
+ * Show a modal box-drawing toggle menu and resolve once the user applies
305
+ * (Enter) or cancels (Escape / Ctrl+C) it.
306
+ *
307
+ * Requires TUI mode; in any other mode this resolves immediately with
308
+ * `applied: false` and the menu's initial values, doing nothing visible.
309
+ */
310
+ export async function showMenu<T extends Record<string, boolean>>(
311
+ ctx: ExtensionCommandContext,
312
+ config: MenuConfig,
313
+ ): Promise<MenuResult<T>> {
314
+ const initialValues = Object.fromEntries(
315
+ config.sections.flatMap((section) => section.items.map((item) => [item.id, item.value])),
316
+ ) as T;
317
+
318
+ if (ctx.mode !== "tui") {
319
+ return { applied: false, values: initialValues };
320
+ }
321
+
322
+ return ctx.ui.custom<MenuResult<T>>(
323
+ (tui, theme, _keybindings, done) =>
324
+ new MenuComponent(config, theme, done as (result: MenuResult<Record<string, boolean>>) => void, tui),
325
+ { overlay: true },
326
+ );
327
+ }
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "@underactive/pi-topping",
3
+ "version": "0.1.0",
4
+ "description": "A Pi extension that replaces the default \"Working...\" loader with a shimmering, randomized activity word, live elapsed timer, and output-token activity meter",
5
+ "keywords": ["pi-package", "pi-extension"],
6
+ "license": "MIT",
7
+ "author": "Eric Sison",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/underactive/pi-topping.git"
11
+ },
12
+ "bugs": {
13
+ "url": "https://github.com/underactive/pi-topping/issues"
14
+ },
15
+ "homepage": "https://github.com/underactive/pi-topping#readme",
16
+ "type": "module",
17
+ "engines": {
18
+ "node": ">=22.19.0"
19
+ },
20
+ "publishConfig": {
21
+ "access": "public"
22
+ },
23
+ "files": ["*.ts", "README.md", "LICENSE"],
24
+ "scripts": {
25
+ "test": "node --test tests/*.test.ts",
26
+ "typecheck": "tsc --noEmit"
27
+ },
28
+ "peerDependencies": {
29
+ "@earendil-works/pi-coding-agent": ">=0.80.0"
30
+ },
31
+ "devDependencies": {
32
+ "@earendil-works/pi-coding-agent": "0.80.6",
33
+ "@earendil-works/pi-tui": "0.80.6",
34
+ "@types/node": "^24.0.0",
35
+ "typescript": "^5.9.3"
36
+ },
37
+ "pi": {
38
+ "extensions": ["./index.ts"],
39
+ "video": "https://cdn.jsdelivr.net/gh/underactive/pi-topping@main/demo.mp4"
40
+ }
41
+ }
package/settings.ts ADDED
@@ -0,0 +1,106 @@
1
+ /**
2
+ * settings.ts
3
+ *
4
+ * Persistent, per-user settings for pi-topping, stored at
5
+ * `~/.pi/agent/pi-topping/settings.json`. All six toggles
6
+ * default to enabled so behavior is unchanged for users who never open
7
+ * `/topping-settings`.
8
+ */
9
+
10
+ import { getAgentDir } from "@earendil-works/pi-coding-agent";
11
+ import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
12
+ import { dirname, join } from "node:path";
13
+
14
+ export interface DecoratorSettings {
15
+ decorations: {
16
+ animatedSpinner: boolean;
17
+ shimmer: boolean;
18
+ tokenActivityMonitor: boolean;
19
+ };
20
+ features: {
21
+ substituteDefaultMessage: boolean;
22
+ elapsedTime: boolean;
23
+ outputTokens: boolean;
24
+ };
25
+ }
26
+
27
+ export const DEFAULT_SETTINGS: DecoratorSettings = {
28
+ decorations: {
29
+ animatedSpinner: true,
30
+ shimmer: true,
31
+ tokenActivityMonitor: true,
32
+ },
33
+ features: {
34
+ substituteDefaultMessage: true,
35
+ elapsedTime: true,
36
+ outputTokens: true,
37
+ },
38
+ };
39
+
40
+ /** Resolve the absolute path to this extension's settings file. */
41
+ export function settingsPath(): string {
42
+ return join(getAgentDir(), "pi-topping", "settings.json");
43
+ }
44
+
45
+ /**
46
+ * True for plain `{}`-style objects only -- excludes `null`, arrays, and
47
+ * primitives. Used to guard against malformed/hand-edited settings.json
48
+ * content (e.g. `"decorations": "oops"` or `"decorations": [1,2,3]`) before
49
+ * spreading it onto the defaults, so a wrong-shaped field can't leak stray
50
+ * properties (like numeric-string keys from spreading a string) into the
51
+ * merged settings object.
52
+ */
53
+ function isPlainObject(value: unknown): value is Record<string, unknown> {
54
+ return typeof value === "object" && value !== null && !Array.isArray(value);
55
+ }
56
+
57
+ function cloneDefaults(): DecoratorSettings {
58
+ return {
59
+ decorations: { ...DEFAULT_SETTINGS.decorations },
60
+ features: { ...DEFAULT_SETTINGS.features },
61
+ };
62
+ }
63
+
64
+ /**
65
+ * Load settings from disk, deep-merging any persisted values onto the
66
+ * defaults. Returns a fresh clone of `DEFAULT_SETTINGS` if the file is
67
+ * missing or contains malformed JSON -- this function never throws.
68
+ */
69
+ export function loadSettings(): DecoratorSettings {
70
+ const defaults = cloneDefaults();
71
+ let raw: string;
72
+ try {
73
+ raw = readFileSync(settingsPath(), "utf8");
74
+ } catch {
75
+ return defaults;
76
+ }
77
+
78
+ try {
79
+ const parsed = JSON.parse(raw) as Partial<DecoratorSettings> | null;
80
+ if (!isPlainObject(parsed)) return defaults;
81
+ return {
82
+ decorations: { ...defaults.decorations, ...(isPlainObject(parsed.decorations) ? parsed.decorations : undefined) },
83
+ features: { ...defaults.features, ...(isPlainObject(parsed.features) ? parsed.features : undefined) },
84
+ };
85
+ } catch {
86
+ return defaults;
87
+ }
88
+ }
89
+
90
+ /**
91
+ * Persist settings to disk, creating the parent directory if needed.
92
+ * Writes to a temp file and renames it into place so a crash mid-write
93
+ * (kill -9, OOM, disk full) can never leave `settings.json` truncated or
94
+ * corrupt -- `renameSync` is atomic on POSIX filesystems when source and
95
+ * destination are on the same directory/filesystem, which they always are
96
+ * here. A stale `.tmp` file left behind by a crash is harmless: the next
97
+ * successful `saveSettings()` call overwrites it, and `loadSettings()`
98
+ * never reads it.
99
+ */
100
+ export function saveSettings(settings: DecoratorSettings): void {
101
+ const path = settingsPath();
102
+ mkdirSync(dirname(path), { recursive: true });
103
+ const tmpPath = `${path}.tmp`;
104
+ writeFileSync(tmpPath, `${JSON.stringify(settings, null, 2)}\n`);
105
+ renameSync(tmpPath, path);
106
+ }
package/words.ts ADDED
@@ -0,0 +1,195 @@
1
+ /**
2
+ * Activity word list for the randomized "Working..." indicator.
3
+ *
4
+ * A random word is picked on prompt and on tool execution start
5
+ * (see index.ts).
6
+ */
7
+
8
+ export const WORDS: string[] = [
9
+ "Accomplishing",
10
+ "Actioning",
11
+ "Actualizing",
12
+ "Architecting",
13
+ "Baking",
14
+ "Beaming",
15
+ "Beboppin'",
16
+ "Befuddling",
17
+ "Billowing",
18
+ "Blanching",
19
+ "Bloviating",
20
+ "Boogieing",
21
+ "Boondoggling",
22
+ "Booping",
23
+ "Bootstrapping",
24
+ "Brewing",
25
+ "Bunning",
26
+ "Burrowing",
27
+ "Calculating",
28
+ "Canoodling",
29
+ "Caramelizing",
30
+ "Cascading",
31
+ "Catapulting",
32
+ "Cerebrating",
33
+ "Channeling",
34
+ "Channelling",
35
+ "Choreographing",
36
+ "Churning",
37
+ "Clauding",
38
+ "Coalescing",
39
+ "Cogitating",
40
+ "Combobulating",
41
+ "Composing",
42
+ "Computing",
43
+ "Concocting",
44
+ "Considering",
45
+ "Contemplating",
46
+ "Cooking",
47
+ "Crafting",
48
+ "Creating",
49
+ "Crunching",
50
+ "Crystallizing",
51
+ "Cultivating",
52
+ "Deciphering",
53
+ "Deliberating",
54
+ "Determining",
55
+ "Dilly-dallying",
56
+ "Discombobulating",
57
+ "Doing",
58
+ "Doodling",
59
+ "Drizzling",
60
+ "Ebbing",
61
+ "Effecting",
62
+ "Elucidating",
63
+ "Embellishing",
64
+ "Enchanting",
65
+ "Envisioning",
66
+ "Fermenting",
67
+ "Fiddle-faddling",
68
+ "Finagling",
69
+ "Flambéing",
70
+ "Flibbertigibbeting",
71
+ "Flowing",
72
+ "Flummoxing",
73
+ "Fluttering",
74
+ "Forging",
75
+ "Forming",
76
+ "Frolicking",
77
+ "Frosting",
78
+ "Gallivanting",
79
+ "Galloping",
80
+ "Garnishing",
81
+ "Generating",
82
+ "Gesticulating",
83
+ "Germinating",
84
+ "Gitifying",
85
+ "Grooving",
86
+ "Gusting",
87
+ "Harmonizing",
88
+ "Hashing",
89
+ "Hatching",
90
+ "Herding",
91
+ "Honking",
92
+ "Hullaballooing",
93
+ "Hyperspacing",
94
+ "Ideating",
95
+ "Imagining",
96
+ "Improvising",
97
+ "Incubating",
98
+ "Inferring",
99
+ "Infusing",
100
+ "Ionizing",
101
+ "Jitterbugging",
102
+ "Julienning",
103
+ "Kneading",
104
+ "Leavening",
105
+ "Levitating",
106
+ "Lollygagging",
107
+ "Manifesting",
108
+ "Marinating",
109
+ "Meandering",
110
+ "Metamorphosing",
111
+ "Misting",
112
+ "Moonwalking",
113
+ "Moseying",
114
+ "Mulling",
115
+ "Mustering",
116
+ "Musing",
117
+ "Nebulizing",
118
+ "Nesting",
119
+ "Newspapering",
120
+ "Noodling",
121
+ "Nucleating",
122
+ "Orbiting",
123
+ "Orchestrating",
124
+ "Osmosing",
125
+ "Perambulating",
126
+ "Percolating",
127
+ "Perusing",
128
+ "Philosophising",
129
+ "Photosynthesizing",
130
+ "Pollinating",
131
+ "Pondering",
132
+ "Pontificating",
133
+ "Pouncing",
134
+ "Precipitating",
135
+ "Prestidigitating",
136
+ "Processing",
137
+ "Proofing",
138
+ "Propagating",
139
+ "Puttering",
140
+ "Puzzling",
141
+ "Quantumizing",
142
+ "Razzle-dazzling",
143
+ "Razzmatazzing",
144
+ "Recombobulating",
145
+ "Reticulating",
146
+ "Roosting",
147
+ "Ruminating",
148
+ "Sautéing",
149
+ "Scampering",
150
+ "Schlepping",
151
+ "Scurrying",
152
+ "Seasoning",
153
+ "Shenaniganing",
154
+ "Shimmying",
155
+ "Simmering",
156
+ "Skedaddling",
157
+ "Sketching",
158
+ "Slithering",
159
+ "Smooshing",
160
+ "Sock-hopping",
161
+ "Spelunking",
162
+ "Spinning",
163
+ "Sprouting",
164
+ "Stewing",
165
+ "Sublimating",
166
+ "Swirling",
167
+ "Swooping",
168
+ "Symbioting",
169
+ "Synthesizing",
170
+ "Tempering",
171
+ "Thinking",
172
+ "Thundering",
173
+ "Tinkering",
174
+ "Tomfoolering",
175
+ "Topsy-turvying",
176
+ "Transfiguring",
177
+ "Transmuting",
178
+ "Twisting",
179
+ "Undulating",
180
+ "Unfurling",
181
+ "Unravelling",
182
+ "Vibing",
183
+ "Waddling",
184
+ "Wandering",
185
+ "Warping",
186
+ "Whatchamacalliting",
187
+ "Whirlpooling",
188
+ "Whirring",
189
+ "Whisking",
190
+ "Wibbling",
191
+ "Working",
192
+ "Wrangling",
193
+ "Zesting",
194
+ "Zigzagging",
195
+ ];