เปลี่ยน spinner ตอน Claude กำลังทำงาน 20 แบบ รวมแบบไทย (ตุ๊กตุ๊ก ดอกบัว กาแฟ) และคำกริยาภาษาไทย

เปลี่ยนตัวหมุน (spinner) ที่แสดงตอน Claude กำลังทำงานได้จริง 20 แบบใน 5 กลุ่ม รวมแบบไทย (ตุ๊กตุ๊กวิ่ง ดอกบัวบาน กาแฟหยด สวัสดีครับ) เปลี่ยนคำกริยาเป็นภาษาไทยขำๆ ได้ (กำลังต้มยำ กำลังตำส้มตำ ...) ปรับความเร็ว หรือปิดกลับเป็นของเดิม
| คำสั่ง | ทำอะไร | |
|---|---|---|
/spinner | ดูทุกแบบ | |
/spinner use <id> | เปลี่ยนแบบ เช่น tuktuk lotus moon | |
/spinner random | สุ่มแบบ | |
| `/spinner thai on\ | off` | ใช้คำกริยาไทย |
/spinner speed <ms> | ความเร็ว 60-400 ms ต่อเฟรม | |
/spinner off | กลับไปใช้ของเดิม |
/plugin install spinner-th --marketplace tksoft11/claude-code-mods-th
ต้องตั้ง CLAUDE_CODE_ENABLE_FUNCTION_HOOKS=1 ก่อนเปิด claude (ดู README หลัก)
เปิด hooks/register.tsx แล้วอ่านตามบทที่เขียนไว้ในคอมเมนต์:
บางแบบใช้ emoji ซึ่งความกว้างอาจไม่เท่ากันในบาง terminal
hooks/register.tsx 202 lines1/**
2 * ============================================================================
3 * MOD 35: Spinner TH - เปลี่ยนตัวหมุนตอน Claude ทำงาน 20 แบบ (ฉบับสอนทำ)
4 * ============================================================================
5 * แรงบันดาลใจจาก: cli-spinners (ชุด spinner ยอดนิยมของ terminal)
6 *
7 * หน้าที่ของ Mod นี้:
8 * 1. วาด spinner ของ Claude Code ใหม่ด้วยแบบที่เลือก (ui.render ของ component "Spinner")
9 * 2. 20 แบบ: คลาสสิก, จุด, แท่ง, สร้างสรรค์ และแบบไทย (ตุ๊กตุ๊ก ดอกบัว กาแฟ ไหว้)
10 * 3. เปลี่ยนคำว่า "Thinking/Sauteing" เป็นคำกริยาไทยขำๆ ได้
11 * 4. ปรับความเร็ว, สุ่มแบบ, ปิดกลับเป็นของเดิม
12 *
13 * เทคนิค: spinner ต้องขยับเอง จึงตั้ง timer ตอนเริ่ม turn ให้เรียก $.ui.invalidate ถี่ๆ
14 * แล้วหยุดตอน turn จบ (ไม่ให้ timer วิ่งตลอดเวลา)
15 */
16
17import type { EngineInterface, Register, Timer } from 'claude-code'
18
19interface Style {
20 id: string
21 name: string
22 group: 'คลาสสิก' | 'จุด' | 'แท่ง' | 'สร้างสรรค์' | 'ไทย'
23 frames: string[]
24 color: string
25}
26
27// ==========================================================================
28// บทที่ 1: แต่ละแบบคือ "เฟรม" ที่วนแสดงทีละอัน
29// ==========================================================================
30const STYLES: Style[] = [
31 { id: 'dots', name: 'จุดเบรลล์', group: 'จุด', frames: ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'], color: '#7c83fd' },
32 { id: 'line', name: 'เส้นหมุน', group: 'คลาสสิก', frames: ['|', '/', '-', '\\'], color: '#90a4ae' },
33 { id: 'arc', name: 'ส่วนโค้ง', group: 'คลาสสิก', frames: ['◜', '◠', '◝', '◞', '◡', '◟'], color: '#4fc3f7' },
34 { id: 'circle', name: 'วงกลมครึ่ง', group: 'คลาสสิก', frames: ['◐', '◓', '◑', '◒'], color: '#7c83fd' },
35 { id: 'square', name: 'สี่เหลี่ยมหมุน', group: 'คลาสสิก', frames: ['◰', '◳', '◲', '◱'], color: '#ba68c8' },
36 { id: 'arrows', name: 'ลูกศร', group: 'คลาสสิก', frames: ['←', '↖', '↑', '↗', '→', '↘', '↓', '↙'], color: '#ffb74d' },
37 { id: 'bounce', name: 'ลูกบอลเด้ง', group: 'จุด', frames: ['⠁', '⠂', '⠄', '⡀', '⢀', '⠠', '⠐', '⠈'], color: '#2ee6a6' },
38 { id: 'pulse', name: 'จุดกระพริบ', group: 'จุด', frames: ['∙∙∙', '●∙∙', '∙●∙', '∙∙●', '∙∙∙'], color: '#ff6b35' },
39 { id: 'typing', name: 'กำลังพิมพ์', group: 'จุด', frames: [' ', '. ', '.. ', '...'], color: '#90a4ae' },
40 { id: 'bars', name: 'แท่งคลื่น', group: 'แท่ง', frames: ['▁▃▅', '▂▄▆', '▃▅▇', '▄▆█', '▃▅▇', '▂▄▆'], color: '#7c83fd' },
41 { id: 'grow', name: 'แท่งโต', group: 'แท่ง', frames: ['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█', '▇', '▆', '▅', '▄', '▃', '▂'], color: '#4db6ac' },
42 { id: 'progress', name: 'แถบเลื่อน', group: 'แท่ง', frames: ['[■□□□□]', '[□■□□□]', '[□□■□□]', '[□□□■□]', '[□□□□■]', '[□□□■□]', '[□□■□□]', '[□■□□□]'], color: '#64b5f6' },
43 { id: 'moon', name: 'ข้างขึ้นข้างแรม', group: 'สร้างสรรค์', frames: ['🌑', '🌒', '🌓', '🌔', '🌕', '🌖', '🌗', '🌘'], color: '#ffd54f' },
44 { id: 'clock', name: 'นาฬิกา', group: 'สร้างสรรค์', frames: ['🕛', '🕐', '🕑', '🕒', '🕓', '🕔', '🕕', '🕖', '🕗', '🕘', '🕙', '🕚'], color: '#ffd54f' },
45 { id: 'earth', name: 'โลกหมุน', group: 'สร้างสรรค์', frames: ['🌍', '🌎', '🌏'], color: '#4fc3f7' },
46 { id: 'pacman', name: 'แพคแมน', group: 'สร้างสรรค์', frames: ['ᗧ···', ' ᗧ··', ' ᗧ·', ' ᗧ', ' ᗤ ', ' ᗤ '], color: '#ffeb3b' },
47 { id: 'tuktuk', name: 'ตุ๊กตุ๊กวิ่ง', group: 'ไทย', frames: ['🛺 ', ' 🛺 ', ' 🛺 ', ' 🛺 ', ' 🛺', ' 🛺 ', ' 🛺 ', ' 🛺 '], color: '#26a69a' },
48 { id: 'lotus', name: 'ดอกบัวบาน', group: 'ไทย', frames: ['🌱', '🪷', '🌸', '🪷'], color: '#f06292' },
49 { id: 'coffee', name: 'กาแฟหยด', group: 'ไทย', frames: ['☕ ˚', '☕ 。', '☕ °', '☕ '], color: '#a1887f' },
50 { id: 'wai', name: 'สวัสดีครับ', group: 'ไทย', frames: ['🙏', '🙂', '🙏', '😊'], color: '#ffb74d' },
51]
52
53// คำกริยาไทยแทน "Sauteing", "Thinking" ของ Claude Code
54const THAI_WORDS = ['กำลังคิด', 'กำลังต้มยำ', 'กำลังตำส้มตำ', 'กำลังชงชาไทย', 'กำลังเคี่ยวโค้ด', 'กำลังปั้นข้าวเหนียว', 'กำลังนวดแป้ง', 'กำลังรีดบั๊ก', 'กำลังพับกระทง', 'กำลังจัดระเบียบ']
55
56let styleId = 'dots'
57let thaiWords = false
58let speedMs = 100
59let ticker: Timer | undefined
60let activeTurns = 0
61
62function current(): Style | undefined {
63 return STYLES.find(s => s.id === styleId)
64}
65
66// ==========================================================================
67// บทที่ 2: ทำให้ขยับ - เปิด timer เฉพาะช่วงที่ Claude ทำงาน
68// ==========================================================================
69function startTicker($: EngineInterface): void {
70 if (ticker || !current()) return
71 ticker = $.clock.every(speedMs, () => $.ui.invalidate('ui.render'))
72}
73
74function stopTicker(): void {
75 ticker?.cancel()
76 ticker = undefined
77}
78
79async function save($: EngineInterface): Promise<void> {
80 await $.store.set('spinner', { styleId, thaiWords, speedMs })
81}
82
83function previewLine(s: Style): string {
84 return `${s.id.padEnd(9)} ${s.name.padEnd(14)} ${s.frames.slice(0, 6).join(' ')}`
85}
86
87const HELP = `🌀 Spinner TH (Mod 35)
88========================================
89/spinner ดูทุกแบบ + แบบที่ใช้อยู่
90/spinner use <id> เปลี่ยนแบบ เช่น /spinner use tuktuk
91/spinner random สุ่มแบบ
92/spinner thai on|off ใช้คำกริยาไทยขำๆ แทนคำของ Claude
93/spinner speed <ms> ความเร็ว (60-400, ค่าเริ่มต้น 100)
94/spinner off กลับไปใช้ spinner เดิมของ Claude Code`
95
96export const register: Register = on => {
97 on('session.start', async ($, e, next) => {
98 const saved = (await $.store.get('spinner')) as { styleId?: string; thaiWords?: boolean; speedMs?: number } | undefined
99 styleId = saved?.styleId ?? 'dots'
100 thaiWords = saved?.thaiWords ?? false
101 speedMs = saved?.speedMs ?? 100
102 await $.command.register({ name: 'spinner', description: 'เปลี่ยน spinner ตอน Claude ทำงาน 20 แบบ (/spinner help)', argumentHint: 'use|random|thai|speed|off' })
103 return next(e)
104 })
105
106 on('turn.start', ($, e, next) => {
107 activeTurns++
108 startTicker($)
109 return next(e)
110 })
111
112 on('turn.complete', ($, e, next) => {
113 activeTurns = Math.max(0, activeTurns - 1)
114 if (activeTurns === 0) stopTicker()
115 return next(e)
116 })
117
118 // ==========================================================================
119 // บทที่ 3: วาด spinner ใหม่ - เลือกเฟรมจากเวลา เพื่อให้ทุกครั้งที่วาดได้เฟรมถัดไป
120 // ==========================================================================
121 on('ui.render', { component: 'Spinner' }, async ($, e, next) => {
122 const style = current()
123 if (!style) return next(e) // ปิดอยู่ = ให้ Claude Code วาดของเดิม
124 const { Box, Text } = $.ui.resolve(e)
125 const now = await $.clock.now()
126 const frame = style.frames[Math.floor(now / speedMs) % style.frames.length] ?? ''
127 // ข้อความพิเศษ (เช่น "รออนุญาต") ต้องแสดงตามเดิม คำกริยาไทยใช้แทนเฉพาะคำปกติ
128 const word = e.props.message ?? (thaiWords ? (THAI_WORDS[Math.floor(now / 4000) % THAI_WORDS.length] ?? e.props.word) : e.props.word)
129 return (
130 <Box gap={1}>
131 <Text color={style.color}>{frame}</Text>
132 <Text>
133 {word}
134 {e.props.suffix}
135 </Text>
136 </Box>
137 )
138 })
139
140 // ==========================================================================
141 // บทที่ 4: คำสั่ง /spinner
142 // ==========================================================================
143 on('command.run', { command: 'spinner' }, async ($, e) => {
144 const [sub = '', arg] = e.args.trim().split(/\s+/).filter(Boolean)
145 switch (sub) {
146 case 'use': {
147 const s = STYLES.find(x => x.id === arg)
148 if (!s) return { text: `❌ ไม่มีแบบ "${arg ?? ''}" ดูรายชื่อด้วย /spinner` }
149 styleId = s.id
150 await save($)
151 return { text: `🌀 เปลี่ยนเป็น ${s.name}: ${s.frames.join(' ')}` }
152 }
153 case 'random': {
154 const s = STYLES[Math.floor(Math.random() * STYLES.length)] ?? STYLES[0]!
155 styleId = s.id
156 await save($)
157 return { text: `🎲 ได้ ${s.name}: ${s.frames.join(' ')}` }
158 }
159 case 'thai':
160 thaiWords = arg === 'on'
161 await save($)
162 return { text: thaiWords ? `🇹🇭 ใช้คำไทย เช่น "${THAI_WORDS.slice(0, 3).join('", "')}"` : 'ใช้คำของ Claude Code ตามเดิม' }
163 case 'speed': {
164 const ms = Number(arg)
165 if (!Number.isInteger(ms) || ms < 60 || ms > 400) return { text: 'วิธีใช้: /spinner speed 120 (60-400 มิลลิวินาทีต่อเฟรม)' }
166 speedMs = ms
167 stopTicker()
168 if (activeTurns > 0) startTicker($)
169 await save($)
170 return { text: `⏱️ เฟรมละ ${ms} ms` }
171 }
172 case 'off':
173 styleId = 'off'
174 stopTicker()
175 await save($)
176 return { text: '↩️ กลับไปใช้ spinner เดิมของ Claude Code แล้ว' }
177 case 'help':
178 return { text: HELP }
179 default: {
180 const groups = [...new Set(STYLES.map(s => s.group))]
181 return {
182 text: [
183 `🌀 Spinner ที่ใช้อยู่: ${current()?.name ?? 'ของเดิม (ปิด)'}${thaiWords ? ' + คำไทย' : ''}`,
184 ...groups.flatMap(g => [`\n${g}`, ...STYLES.filter(s => s.group === g).map(previewLine)]),
185 '',
186 '/spinner use <id> เพื่อเปลี่ยน | /spinner help',
187 ].join('\n'),
188 }
189 }
190 }
191 })
192}
193
194/**
195 * ========================================================================
196 * การบ้าน / ต่อยอด
197 * ========================================================================
198 * 1. เปลี่ยนแบบตามสิ่งที่ Claude ทำ: e.props.mode เป็น 'thinking' ใช้ดอกบัว, 'tool-use' ใช้ตุ๊กตุ๊ก
199 * 2. ใส่สีไล่ระดับตามเวลาที่รอ (ยิ่งนานยิ่งแดง)
200 * 3. ให้ผู้ใช้สร้างแบบของตัวเอง: /spinner custom 🍜,🍲,🥘 แล้วเก็บใน $.store
201 */
202