spinnerverb 1.0.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/LICENSE +21 -0
- package/README.md +73 -0
- package/index.mjs +471 -0
- package/package.json +39 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ideademic
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# spinnerverb
|
|
2
|
+
|
|
3
|
+
Interactive TUI to customize [Claude Code](https://claude.ai/code) spinner verbs.
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
npx spinnerverb
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## What it does
|
|
10
|
+
|
|
11
|
+
Claude Code shows animated verbs while working ("Thinking", "Reasoning", "Pondering", etc.). This tool lets you customize them through an interactive terminal UI.
|
|
12
|
+
|
|
13
|
+
- **Append** your verbs alongside the defaults, or **replace** them entirely
|
|
14
|
+
- Edit globally (`~/.claude/settings.json`) or per-project (`.claude/settings.json`)
|
|
15
|
+
- Add, edit, delete, and clear verbs with instant save
|
|
16
|
+
|
|
17
|
+
## Screenshots
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
███ spinnerverb
|
|
21
|
+
Scope: Global (~/.claude) │ ~/.claude/settings.json
|
|
22
|
+
|
|
23
|
+
╭──────────────────────────────────────────────────────╮
|
|
24
|
+
│ APPEND — your verbs are added alongside the defaults │
|
|
25
|
+
╰──────────────────────────────────────────────────────╯
|
|
26
|
+
▶ Mode: append
|
|
27
|
+
───
|
|
28
|
+
Vibing
|
|
29
|
+
Manifesting
|
|
30
|
+
Yapping
|
|
31
|
+
───
|
|
32
|
+
+ Add verb
|
|
33
|
+
Clear all verbs
|
|
34
|
+
|
|
35
|
+
↑↓/jk navigate enter select/edit d delete q quit
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Controls
|
|
39
|
+
|
|
40
|
+
| Key | Action |
|
|
41
|
+
|---|---|
|
|
42
|
+
| `↑↓` / `jk` | Navigate |
|
|
43
|
+
| `Enter` / `Space` | Select / edit |
|
|
44
|
+
| `d` | Delete verb |
|
|
45
|
+
| `q` / `Esc` | Quit |
|
|
46
|
+
|
|
47
|
+
## How it works
|
|
48
|
+
|
|
49
|
+
Reads and writes the `spinnerVerbs` field in your Claude Code `settings.json`:
|
|
50
|
+
|
|
51
|
+
```json
|
|
52
|
+
{
|
|
53
|
+
"spinnerVerbs": {
|
|
54
|
+
"mode": "append",
|
|
55
|
+
"verbs": ["Vibing", "Manifesting", "Yapping"]
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
- `"append"` — your verbs are added to Claude Code's built-in set
|
|
61
|
+
- `"replace"` — only your verbs are used
|
|
62
|
+
|
|
63
|
+
## Settings file locations
|
|
64
|
+
|
|
65
|
+
| Scope | File | Committed |
|
|
66
|
+
|---|---|---|
|
|
67
|
+
| Global | `~/.claude/settings.json` | N/A |
|
|
68
|
+
| Project | `.claude/settings.json` | Yes |
|
|
69
|
+
| Project (local) | `.claude/settings.local.json` | No |
|
|
70
|
+
|
|
71
|
+
## License
|
|
72
|
+
|
|
73
|
+
MIT
|
package/index.mjs
ADDED
|
@@ -0,0 +1,471 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import React, { useState, useEffect } from "react";
|
|
3
|
+
import { render, Box, Text, useInput, useApp } from "ink";
|
|
4
|
+
import TextInput from "ink-text-input";
|
|
5
|
+
import fs from "fs";
|
|
6
|
+
import path from "path";
|
|
7
|
+
import os from "os";
|
|
8
|
+
|
|
9
|
+
const GLOBAL_SETTINGS = path.join(os.homedir(), ".claude", "settings.json");
|
|
10
|
+
const LOCAL_SETTINGS = path.join(process.cwd(), ".claude", "settings.json");
|
|
11
|
+
const LOCAL_SETTINGS_LOCAL = path.join(
|
|
12
|
+
process.cwd(),
|
|
13
|
+
".claude",
|
|
14
|
+
"settings.local.json"
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
const DEFAULT_VERBS = [
|
|
18
|
+
"Thinking",
|
|
19
|
+
"Reasoning",
|
|
20
|
+
"Considering",
|
|
21
|
+
"Analyzing",
|
|
22
|
+
"Synthesizing",
|
|
23
|
+
"Evaluating",
|
|
24
|
+
"Processing",
|
|
25
|
+
"Computing",
|
|
26
|
+
"Reflecting",
|
|
27
|
+
"Examining",
|
|
28
|
+
"Contemplating",
|
|
29
|
+
"Deliberating",
|
|
30
|
+
"Pondering",
|
|
31
|
+
"Working",
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
function readSettings(filePath) {
|
|
35
|
+
try {
|
|
36
|
+
return JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
37
|
+
} catch {
|
|
38
|
+
return {};
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function writeSettings(filePath, settings) {
|
|
43
|
+
const dir = path.dirname(filePath);
|
|
44
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
45
|
+
fs.writeFileSync(filePath, JSON.stringify(settings, null, 2) + "\n");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function getSpinnerVerbs(settings) {
|
|
49
|
+
return settings.spinnerVerbs || null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ── Screens ──
|
|
53
|
+
|
|
54
|
+
const SCREEN = {
|
|
55
|
+
SCOPE: "scope",
|
|
56
|
+
MAIN: "main",
|
|
57
|
+
ADD: "add",
|
|
58
|
+
CONFIRM_DELETE: "confirm_delete",
|
|
59
|
+
CONFIRM_CLEAR: "confirm_clear",
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
function Header({ scope, filePath }) {
|
|
63
|
+
return React.createElement(
|
|
64
|
+
Box,
|
|
65
|
+
{ flexDirection: "column", marginBottom: 1 },
|
|
66
|
+
React.createElement(
|
|
67
|
+
Text,
|
|
68
|
+
{ bold: true, color: "cyan" },
|
|
69
|
+
"\u2588\u2588\u2588 spinnerverb"
|
|
70
|
+
),
|
|
71
|
+
React.createElement(
|
|
72
|
+
Text,
|
|
73
|
+
{ dimColor: true },
|
|
74
|
+
`Scope: ${scope === "global" ? "Global (~/.claude)" : "Project (./.claude)"} \u2502 ${filePath}`
|
|
75
|
+
)
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function ScopeScreen() {
|
|
80
|
+
const { exit } = useApp();
|
|
81
|
+
const [selected, setSelected] = useState(0);
|
|
82
|
+
|
|
83
|
+
const globalExists = fs.existsSync(GLOBAL_SETTINGS);
|
|
84
|
+
const localExists = fs.existsSync(LOCAL_SETTINGS);
|
|
85
|
+
const localLocalExists = fs.existsSync(LOCAL_SETTINGS_LOCAL);
|
|
86
|
+
|
|
87
|
+
const options = [
|
|
88
|
+
{
|
|
89
|
+
label: "Global",
|
|
90
|
+
desc: `~/.claude/settings.json ${globalExists ? "(exists)" : "(will create)"}`,
|
|
91
|
+
value: "global",
|
|
92
|
+
file: GLOBAL_SETTINGS,
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
label: "Project",
|
|
96
|
+
desc: `.claude/settings.json ${localExists ? "(exists)" : "(will create)"}`,
|
|
97
|
+
value: "local",
|
|
98
|
+
file: LOCAL_SETTINGS,
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
label: "Project (local)",
|
|
102
|
+
desc: `.claude/settings.local.json ${localLocalExists ? "(exists)" : "(will create)"}`,
|
|
103
|
+
value: "local-local",
|
|
104
|
+
file: LOCAL_SETTINGS_LOCAL,
|
|
105
|
+
},
|
|
106
|
+
];
|
|
107
|
+
|
|
108
|
+
useInput((input, key) => {
|
|
109
|
+
if (key.upArrow || input === "k")
|
|
110
|
+
setSelected((s) => (s - 1 + options.length) % options.length);
|
|
111
|
+
if (key.downArrow || input === "j")
|
|
112
|
+
setSelected((s) => (s + 1) % options.length);
|
|
113
|
+
if (key.return) {
|
|
114
|
+
const opt = options[selected];
|
|
115
|
+
renderApp(opt.value, opt.file);
|
|
116
|
+
}
|
|
117
|
+
if (input === "q" || key.escape) exit();
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
return React.createElement(
|
|
121
|
+
Box,
|
|
122
|
+
{ flexDirection: "column" },
|
|
123
|
+
React.createElement(
|
|
124
|
+
Box,
|
|
125
|
+
{ marginBottom: 1, flexDirection: "column" },
|
|
126
|
+
React.createElement(
|
|
127
|
+
Text,
|
|
128
|
+
{ bold: true, color: "cyan" },
|
|
129
|
+
"\u2588\u2588\u2588 spinnerverb"
|
|
130
|
+
),
|
|
131
|
+
React.createElement(
|
|
132
|
+
Text,
|
|
133
|
+
{ dimColor: true },
|
|
134
|
+
"Choose which settings file to edit"
|
|
135
|
+
)
|
|
136
|
+
),
|
|
137
|
+
...options.map((opt, i) =>
|
|
138
|
+
React.createElement(
|
|
139
|
+
Box,
|
|
140
|
+
{ key: opt.value },
|
|
141
|
+
React.createElement(
|
|
142
|
+
Text,
|
|
143
|
+
{
|
|
144
|
+
color: i === selected ? "cyan" : undefined,
|
|
145
|
+
bold: i === selected,
|
|
146
|
+
},
|
|
147
|
+
`${i === selected ? "\u25b6 " : " "}${opt.label}`
|
|
148
|
+
),
|
|
149
|
+
React.createElement(Text, { dimColor: true }, ` ${opt.desc}`)
|
|
150
|
+
)
|
|
151
|
+
),
|
|
152
|
+
React.createElement(
|
|
153
|
+
Box,
|
|
154
|
+
{ marginTop: 1 },
|
|
155
|
+
React.createElement(
|
|
156
|
+
Text,
|
|
157
|
+
{ dimColor: true },
|
|
158
|
+
"\u2191\u2193/jk navigate enter select q quit"
|
|
159
|
+
)
|
|
160
|
+
)
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function MainApp({ scope, filePath }) {
|
|
165
|
+
const { exit } = useApp();
|
|
166
|
+
const [settings, setSettings] = useState(() => readSettings(filePath));
|
|
167
|
+
const [screen, setScreen] = useState(SCREEN.MAIN);
|
|
168
|
+
const [cursor, setCursor] = useState(0);
|
|
169
|
+
const [newVerb, setNewVerb] = useState("");
|
|
170
|
+
const [deleteIdx, setDeleteIdx] = useState(-1);
|
|
171
|
+
const [saved, setSaved] = useState(false);
|
|
172
|
+
const [editIdx, setEditIdx] = useState(-1);
|
|
173
|
+
const [editValue, setEditValue] = useState("");
|
|
174
|
+
|
|
175
|
+
const sv = getSpinnerVerbs(settings);
|
|
176
|
+
const mode = sv?.mode || "append";
|
|
177
|
+
const verbs = sv?.verbs || [];
|
|
178
|
+
|
|
179
|
+
function persist(newSettings) {
|
|
180
|
+
setSettings(newSettings);
|
|
181
|
+
writeSettings(filePath, newSettings);
|
|
182
|
+
setSaved(true);
|
|
183
|
+
setTimeout(() => setSaved(false), 1500);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function updateSpinnerVerbs(newMode, newVerbs) {
|
|
187
|
+
const updated = { ...settings };
|
|
188
|
+
if (newVerbs.length === 0) {
|
|
189
|
+
delete updated.spinnerVerbs;
|
|
190
|
+
} else {
|
|
191
|
+
updated.spinnerVerbs = { mode: newMode, verbs: newVerbs };
|
|
192
|
+
}
|
|
193
|
+
persist(updated);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function toggleMode() {
|
|
197
|
+
const newMode = mode === "append" ? "replace" : "append";
|
|
198
|
+
updateSpinnerVerbs(newMode, verbs);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// ── Main screen ──
|
|
202
|
+
if (screen === SCREEN.MAIN) {
|
|
203
|
+
const menuItems = [
|
|
204
|
+
{ type: "toggle", label: `Mode: ${mode}` },
|
|
205
|
+
{ type: "separator" },
|
|
206
|
+
...verbs.map((v, i) => ({ type: "verb", label: v, index: i })),
|
|
207
|
+
{ type: "separator" },
|
|
208
|
+
{ type: "add", label: "+ Add verb" },
|
|
209
|
+
{ type: "clear", label: "Clear all verbs" },
|
|
210
|
+
];
|
|
211
|
+
|
|
212
|
+
// Filter separators for cleanliness if no verbs
|
|
213
|
+
const items =
|
|
214
|
+
verbs.length === 0
|
|
215
|
+
? menuItems.filter(
|
|
216
|
+
(m) => m.type !== "separator" && m.type !== "clear"
|
|
217
|
+
)
|
|
218
|
+
: menuItems;
|
|
219
|
+
|
|
220
|
+
useInput((input, key) => {
|
|
221
|
+
if (screen !== SCREEN.MAIN) return;
|
|
222
|
+
if (key.upArrow || input === "k")
|
|
223
|
+
setCursor((c) => {
|
|
224
|
+
let next = (c - 1 + items.length) % items.length;
|
|
225
|
+
while (items[next].type === "separator")
|
|
226
|
+
next = (next - 1 + items.length) % items.length;
|
|
227
|
+
return next;
|
|
228
|
+
});
|
|
229
|
+
if (key.downArrow || input === "j")
|
|
230
|
+
setCursor((c) => {
|
|
231
|
+
let next = (c + 1) % items.length;
|
|
232
|
+
while (items[next].type === "separator")
|
|
233
|
+
next = (next + 1) % items.length;
|
|
234
|
+
return next;
|
|
235
|
+
});
|
|
236
|
+
if (key.return || input === " ") {
|
|
237
|
+
const item = items[cursor];
|
|
238
|
+
if (item.type === "toggle") toggleMode();
|
|
239
|
+
if (item.type === "add") {
|
|
240
|
+
setNewVerb("");
|
|
241
|
+
setScreen(SCREEN.ADD);
|
|
242
|
+
}
|
|
243
|
+
if (item.type === "clear") setScreen(SCREEN.CONFIRM_CLEAR);
|
|
244
|
+
if (item.type === "verb") {
|
|
245
|
+
setEditIdx(item.index);
|
|
246
|
+
setEditValue(item.label);
|
|
247
|
+
setScreen(SCREEN.ADD);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
if (input === "d" || key.delete) {
|
|
251
|
+
const item = items[cursor];
|
|
252
|
+
if (item.type === "verb") {
|
|
253
|
+
setDeleteIdx(item.index);
|
|
254
|
+
setScreen(SCREEN.CONFIRM_DELETE);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
if (input === "q" || key.escape) exit();
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
// Clamp cursor
|
|
261
|
+
useEffect(() => {
|
|
262
|
+
if (cursor >= items.length) setCursor(Math.max(0, items.length - 1));
|
|
263
|
+
if (items[cursor]?.type === "separator")
|
|
264
|
+
setCursor((c) => Math.min(c + 1, items.length - 1));
|
|
265
|
+
}, [items.length]);
|
|
266
|
+
|
|
267
|
+
return React.createElement(
|
|
268
|
+
Box,
|
|
269
|
+
{ flexDirection: "column" },
|
|
270
|
+
React.createElement(Header, { scope, filePath }),
|
|
271
|
+
|
|
272
|
+
// Mode info box
|
|
273
|
+
React.createElement(
|
|
274
|
+
Box,
|
|
275
|
+
{
|
|
276
|
+
borderStyle: "round",
|
|
277
|
+
borderColor: mode === "replace" ? "yellow" : "green",
|
|
278
|
+
paddingX: 1,
|
|
279
|
+
marginBottom: 1,
|
|
280
|
+
},
|
|
281
|
+
React.createElement(
|
|
282
|
+
Text,
|
|
283
|
+
null,
|
|
284
|
+
mode === "append"
|
|
285
|
+
? React.createElement(
|
|
286
|
+
React.Fragment,
|
|
287
|
+
null,
|
|
288
|
+
React.createElement(
|
|
289
|
+
Text,
|
|
290
|
+
{ color: "green", bold: true },
|
|
291
|
+
"APPEND"
|
|
292
|
+
),
|
|
293
|
+
React.createElement(
|
|
294
|
+
Text,
|
|
295
|
+
null,
|
|
296
|
+
` \u2014 your verbs are added alongside the ${DEFAULT_VERBS.length} defaults`
|
|
297
|
+
)
|
|
298
|
+
)
|
|
299
|
+
: React.createElement(
|
|
300
|
+
React.Fragment,
|
|
301
|
+
null,
|
|
302
|
+
React.createElement(
|
|
303
|
+
Text,
|
|
304
|
+
{ color: "yellow", bold: true },
|
|
305
|
+
"REPLACE"
|
|
306
|
+
),
|
|
307
|
+
React.createElement(
|
|
308
|
+
Text,
|
|
309
|
+
null,
|
|
310
|
+
" \u2014 only your verbs are used, defaults are hidden"
|
|
311
|
+
)
|
|
312
|
+
)
|
|
313
|
+
)
|
|
314
|
+
),
|
|
315
|
+
|
|
316
|
+
// Menu items
|
|
317
|
+
...items.map((item, i) => {
|
|
318
|
+
if (item.type === "separator")
|
|
319
|
+
return React.createElement(
|
|
320
|
+
Text,
|
|
321
|
+
{ key: `sep-${i}`, dimColor: true },
|
|
322
|
+
" \u2500\u2500\u2500"
|
|
323
|
+
);
|
|
324
|
+
const sel = i === cursor;
|
|
325
|
+
const pointer = sel ? "\u25b6 " : " ";
|
|
326
|
+
let color = undefined;
|
|
327
|
+
if (item.type === "add") color = "green";
|
|
328
|
+
if (item.type === "clear") color = "red";
|
|
329
|
+
if (item.type === "toggle") color = "cyan";
|
|
330
|
+
return React.createElement(
|
|
331
|
+
Text,
|
|
332
|
+
{ key: `item-${i}`, bold: sel, color: sel ? color || "cyan" : color },
|
|
333
|
+
`${pointer}${item.label}`
|
|
334
|
+
);
|
|
335
|
+
}),
|
|
336
|
+
|
|
337
|
+
// Footer
|
|
338
|
+
React.createElement(
|
|
339
|
+
Box,
|
|
340
|
+
{ marginTop: 1, flexDirection: "column" },
|
|
341
|
+
saved
|
|
342
|
+
? React.createElement(Text, { color: "green" }, "\u2713 Saved!")
|
|
343
|
+
: null,
|
|
344
|
+
React.createElement(
|
|
345
|
+
Text,
|
|
346
|
+
{ dimColor: true },
|
|
347
|
+
"\u2191\u2193/jk navigate enter select/edit d delete q quit"
|
|
348
|
+
)
|
|
349
|
+
)
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// ── Add / Edit screen ──
|
|
354
|
+
if (screen === SCREEN.ADD) {
|
|
355
|
+
const isEdit = editIdx >= 0;
|
|
356
|
+
|
|
357
|
+
useInput((input, key) => {
|
|
358
|
+
if (screen !== SCREEN.ADD) return;
|
|
359
|
+
if (key.escape) {
|
|
360
|
+
setEditIdx(-1);
|
|
361
|
+
setEditValue("");
|
|
362
|
+
setScreen(SCREEN.MAIN);
|
|
363
|
+
}
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
return React.createElement(
|
|
367
|
+
Box,
|
|
368
|
+
{ flexDirection: "column" },
|
|
369
|
+
React.createElement(Header, { scope, filePath }),
|
|
370
|
+
React.createElement(
|
|
371
|
+
Text,
|
|
372
|
+
{ bold: true },
|
|
373
|
+
isEdit ? `Edit verb (was "${verbs[editIdx]}"):` : "Enter new verb:"
|
|
374
|
+
),
|
|
375
|
+
React.createElement(
|
|
376
|
+
Box,
|
|
377
|
+
null,
|
|
378
|
+
React.createElement(Text, null, "> "),
|
|
379
|
+
React.createElement(TextInput, {
|
|
380
|
+
value: isEdit ? editValue : newVerb,
|
|
381
|
+
onChange: isEdit ? setEditValue : setNewVerb,
|
|
382
|
+
onSubmit: (val) => {
|
|
383
|
+
const trimmed = val.trim();
|
|
384
|
+
if (trimmed) {
|
|
385
|
+
const newVerbs = [...verbs];
|
|
386
|
+
if (isEdit) {
|
|
387
|
+
newVerbs[editIdx] = trimmed;
|
|
388
|
+
} else {
|
|
389
|
+
newVerbs.push(trimmed);
|
|
390
|
+
}
|
|
391
|
+
updateSpinnerVerbs(mode, newVerbs);
|
|
392
|
+
}
|
|
393
|
+
setEditIdx(-1);
|
|
394
|
+
setEditValue("");
|
|
395
|
+
setNewVerb("");
|
|
396
|
+
setScreen(SCREEN.MAIN);
|
|
397
|
+
},
|
|
398
|
+
})
|
|
399
|
+
),
|
|
400
|
+
React.createElement(
|
|
401
|
+
Text,
|
|
402
|
+
{ dimColor: true, marginTop: 1 },
|
|
403
|
+
"enter confirm esc cancel"
|
|
404
|
+
)
|
|
405
|
+
);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// ── Confirm delete ──
|
|
409
|
+
if (screen === SCREEN.CONFIRM_DELETE) {
|
|
410
|
+
useInput((input, key) => {
|
|
411
|
+
if (screen !== SCREEN.CONFIRM_DELETE) return;
|
|
412
|
+
if (input === "y" || input === "Y") {
|
|
413
|
+
const newVerbs = verbs.filter((_, i) => i !== deleteIdx);
|
|
414
|
+
updateSpinnerVerbs(mode, newVerbs);
|
|
415
|
+
setScreen(SCREEN.MAIN);
|
|
416
|
+
}
|
|
417
|
+
if (input === "n" || input === "N" || key.escape) setScreen(SCREEN.MAIN);
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
return React.createElement(
|
|
421
|
+
Box,
|
|
422
|
+
{ flexDirection: "column" },
|
|
423
|
+
React.createElement(Header, { scope, filePath }),
|
|
424
|
+
React.createElement(
|
|
425
|
+
Text,
|
|
426
|
+
{ color: "yellow" },
|
|
427
|
+
`Delete "${verbs[deleteIdx]}"? (y/n)`
|
|
428
|
+
)
|
|
429
|
+
);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// ── Confirm clear ──
|
|
433
|
+
if (screen === SCREEN.CONFIRM_CLEAR) {
|
|
434
|
+
useInput((input, key) => {
|
|
435
|
+
if (screen !== SCREEN.CONFIRM_CLEAR) return;
|
|
436
|
+
if (input === "y" || input === "Y") {
|
|
437
|
+
updateSpinnerVerbs(mode, []);
|
|
438
|
+
setCursor(0);
|
|
439
|
+
setScreen(SCREEN.MAIN);
|
|
440
|
+
}
|
|
441
|
+
if (input === "n" || input === "N" || key.escape) setScreen(SCREEN.MAIN);
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
return React.createElement(
|
|
445
|
+
Box,
|
|
446
|
+
{ flexDirection: "column" },
|
|
447
|
+
React.createElement(Header, { scope, filePath }),
|
|
448
|
+
React.createElement(
|
|
449
|
+
Text,
|
|
450
|
+
{ color: "red" },
|
|
451
|
+
`Remove all ${verbs.length} custom verbs? (y/n)`
|
|
452
|
+
)
|
|
453
|
+
);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
return null;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// ── Entrypoint ──
|
|
460
|
+
|
|
461
|
+
let activeInstance = null;
|
|
462
|
+
|
|
463
|
+
function renderApp(scope, filePath) {
|
|
464
|
+
if (activeInstance) activeInstance.unmount();
|
|
465
|
+
activeInstance = render(
|
|
466
|
+
React.createElement(MainApp, { scope, filePath })
|
|
467
|
+
);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// Start with scope picker
|
|
471
|
+
activeInstance = render(React.createElement(ScopeScreen));
|
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "spinnerverb",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Interactive TUI to customize Claude Code spinner verbs",
|
|
5
|
+
"bin": {
|
|
6
|
+
"spinnerverb": "./index.mjs"
|
|
7
|
+
},
|
|
8
|
+
"type": "module",
|
|
9
|
+
"license": "MIT",
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "git+https://github.com/ideademic/spinnerverb.git"
|
|
13
|
+
},
|
|
14
|
+
"author": "Ideademic",
|
|
15
|
+
"keywords": [
|
|
16
|
+
"claude",
|
|
17
|
+
"claude-code",
|
|
18
|
+
"spinner",
|
|
19
|
+
"spinner-verbs",
|
|
20
|
+
"customization",
|
|
21
|
+
"tui",
|
|
22
|
+
"cli",
|
|
23
|
+
"anthropic",
|
|
24
|
+
"settings"
|
|
25
|
+
],
|
|
26
|
+
"files": [
|
|
27
|
+
"index.mjs",
|
|
28
|
+
"README.md",
|
|
29
|
+
"LICENSE"
|
|
30
|
+
],
|
|
31
|
+
"engines": {
|
|
32
|
+
"node": ">=18"
|
|
33
|
+
},
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"ink": "^5.1.0",
|
|
36
|
+
"ink-text-input": "^6.0.0",
|
|
37
|
+
"react": "^18.3.1"
|
|
38
|
+
}
|
|
39
|
+
}
|