แก้ง่วงสำหรับ Dev: มุกโปรแกรมเมอร์ ควิซ โจทย์โค้ดสั้นๆ Pomodoro และเตือนพัก ดื่มน้ำ พักสายตา ขณะเขียนโค้ดนานๆ

แก้ง่วงระหว่างเขียนโค้ด: มุกโปรแกรมเมอร์ ควิซความรู้ Dev โจทย์โค้ดสั้นๆ Pomodoro ที่นับถอยหลังใน status line และเตือนพักอัตโนมัติ (ดื่มน้ำ พักสายตา 20-20-20 ยืดเส้น) รวมถึงทักเมื่อทำงานดึกเกินไป
| คำสั่ง | ทำอะไร |
|---|---|
/awake joke | มุกโปรแกรมเมอร์ |
/awake quiz / answer <a-d> | ควิซความรู้ Dev พร้อมนับคะแนน |
/awake challenge | โจทย์โค้ดสั้นๆ พร้อมคำใบ้ |
/awake pomodoro [โฟกัส] [พัก] | เริ่ม Pomodoro (ค่าเริ่มต้น 25/5 นาที) |
/awake pomodoro stop | หยุด |
/awake remind on [นาที] / off | เตือนพักอัตโนมัติ (ค่าเริ่มต้น 50 นาที) |
/awake status | สถานะทั้งหมด |
/awake quote / quote ai | คำคมของคนจริง (อ้างที่มาถูก) / ให้ Claude แต่งคำคมใหม่ |
/awake quote on [นาที] / quote off | คำคมหมุนเวียนในแถบสถานะ (ไม่แย่งที่ Pomodoro) |
/plugin install dev-awake-th --marketplace tksoft11/claude-code-mods-th
ต้องตั้ง CLAUDE_CODE_ENABLE_FUNCTION_HOOKS=1 ก่อนเปิด claude (ดู README หลัก)
เปิด hooks/register.tsx แล้วอ่านตามบทที่เขียนไว้ในคอมเมนต์:
การเตือนพักและ Pomodoro ทำงานเฉพาะตอนเปิด Claude Code อยู่
hooks/register.tsx 273 lines1/**
2 * ============================================================================
3 * MOD 31: Dev Awake TH - แก้ง่วงสำหรับ Dev (ฉบับสอนทำ)
4 * ============================================================================
5 * แรงบันดาลใจจาก: เทคนิค Pomodoro และกฎ 20-20-20 ถนอมสายตา
6 *
7 * หน้าที่ของ Mod นี้:
8 * 1. มุกโปรแกรมเมอร์, ควิซความรู้ Dev, โจทย์โค้ดสั้นๆ ให้สมองตื่น
9 * 2. Pomodoro: จับเวลาโฟกัส/พัก แสดงนับถอยหลังใน status line
10 * 3. เตือนพักอัตโนมัติ: ดื่มน้ำ, พักสายตา 20-20-20, ลุกยืดเส้น (หมุนเวียน)
11 * 4. ทักเมื่อทำงานดึกเกินไป
12 * 5. คำคมหมุนเวียนในแถบสถานะ (ของคนจริง อ้างที่มาถูก) หรือให้ Claude แต่งคำคมใหม่
13 */
14
15import type { EngineInterface, Register, Timer } from 'claude-code'
16
17// คำคมที่รู้ที่มาแน่ชัด (หลีกเลี่ยงคำคมที่มักถูกอ้างผิดคน)
18const QUOTES = [
19 'Talk is cheap. Show me the code. — Linus Torvalds',
20 'Premature optimization is the root of all evil. — Donald Knuth',
21 'Simplicity is prerequisite for reliability. — Edsger W. Dijkstra',
22 'Programs must be written for people to read. — Abelson & Sussman',
23 'Make it work, make it right, make it fast. — Kent Beck',
24 'Any fool can write code that a computer can understand. Good programmers write code that humans can understand. — Martin Fowler',
25 'There are only two hard things in Computer Science: cache invalidation and naming things. — Phil Karlton',
26 'Testing shows the presence, not the absence of bugs. — Edsger W. Dijkstra',
27 'The most dangerous phrase in the language is "we\'ve always done it this way". — Grace Hopper',
28 'Weeks of coding can save you hours of planning. — นิรนาม',
29 'ช้าๆ ได้พร้าเล่มงาม — สุภาษิตไทย',
30 'น้ำหยดลงหินทุกวัน หินมันยังกร่อน — สุภาษิตไทย',
31 'ไม่มีใครแก่เกินเรียน — สำนวนไทย',
32 'ทำดีได้ดี ทำโค้ดดีได้รีวิวผ่าน — ดัดแปลงจากสุภาษิต',
33]
34let quoteTimer: Timer | undefined
35
36// ==========================================================================
37// บทที่ 1: คลังเนื้อหา - เพิ่มของทีมคุณเองได้
38// ==========================================================================
39const JOKES = [
40 'Dev: "บนเครื่องผมรันได้นะ"\nหัวหน้า: "งั้นเราส่งเครื่องคุณให้ลูกค้าเลยไหม"',
41 'ทำไมโปรแกรมเมอร์ชอบโหมดมืด?\nเพราะแสงดึงดูดบั๊ก 🐛',
42 'แก้บั๊ก 1 ตัว เกิดใหม่ 3 ตัว\nนี่ไม่ใช่บั๊ก นี่คือ feature การขยายพันธุ์',
43 'Git commit ล่าสุด: "แก้นิดเดียว"\nไฟล์ที่เปลี่ยน: 147',
44 'มี 10 ประเภทของคนบนโลก\nคนที่เข้าใจเลขฐานสอง กับคนที่ไม่เข้าใจ',
45 'เขียนโค้ดเสร็จ 90% แล้ว\nเหลืออีก 90% ที่เหลือ',
46 'ลูกค้า: "ขอแก้นิดเดียวนะ"\nDev: *เปิด Jira เตรียมใจ*',
47 'ตั้งชื่อตัวแปรยากกว่าเขียนโค้ดทั้งฟังก์ชัน\n`data2_final_จริงๆ_v3`',
48 'คอมเมนต์ในโค้ด: "อย่าแตะส่วนนี้"\nมือ: *แตะ*',
49 'Deploy วันศุกร์เย็น = ได้ทำงานวันเสาร์ฟรี',
50 'Stack Overflow ล่ม = วันหยุดราชการของ Dev',
51 'กาแฟคืออะไร? คือตัวแปลงคาเฟอีนเป็นโค้ด ☕→💻',
52]
53
54interface Quiz {
55 q: string
56 choices: string[]
57 answer: number // index ของคำตอบที่ถูก
58 why: string
59}
60const QUIZZES: Quiz[] = [
61 { q: 'HTTP status ไหนหมายถึง "ไม่พบหน้า"?', choices: ['200', '301', '404', '500'], answer: 2, why: '404 Not Found' },
62 { q: 'คำสั่ง git ไหนใช้ดูประวัติ commit?', choices: ['git status', 'git log', 'git diff', 'git show-branch'], answer: 1, why: 'git log แสดงประวัติ commit' },
63 { q: 'ใน JavaScript `typeof null` ได้อะไร?', choices: ['"null"', '"undefined"', '"object"', '"number"'], answer: 2, why: 'บั๊กประวัติศาสตร์ของ JS ที่แก้ไม่ได้แล้ว' },
64 { q: 'Big-O ของการค้นหาแบบ binary search?', choices: ['O(1)', 'O(log n)', 'O(n)', 'O(n log n)'], answer: 1, why: 'แบ่งครึ่งทุกรอบ' },
65 { q: 'SQL คำสั่งไหนใช้ลบข้อมูลบางแถว?', choices: ['DROP', 'TRUNCATE', 'DELETE', 'REMOVE'], answer: 2, why: 'DELETE ... WHERE ลบเฉพาะแถวที่ตรงเงื่อนไข' },
66 { q: 'พอร์ตเริ่มต้นของ HTTPS?', choices: ['80', '443', '8080', '22'], answer: 1, why: 'HTTPS ใช้ 443, HTTP ใช้ 80' },
67 { q: 'CSS ไหนจัดของให้อยู่กลางทั้งแนวตั้งและแนวนอนใน flex?', choices: ['align-items + justify-content: center', 'text-align: center', 'margin: auto 0', 'float: center'], answer: 0, why: 'flex ใช้ align-items (แกนขวาง) และ justify-content (แกนหลัก)' },
68 { q: 'REST: method ไหนควร idempotent?', choices: ['POST', 'PUT', 'PATCH (เสมอ)', 'ไม่มีเลย'], answer: 1, why: 'PUT ส่งซ้ำได้ผลเหมือนเดิม' },
69]
70
71const CHALLENGES = [
72 { title: 'FizzBuzz ภาษาไทย', task: 'พิมพ์ 1-30 ถ้าหาร 3 ลงตัวพิมพ์ "ฟิซ" หาร 5 ลงตัวพิมพ์ "บัซ" หารทั้งคู่พิมพ์ "ฟิซบัซ"', hint: 'เช็คหาร 15 ก่อน' },
73 { title: 'กลับคำ', task: 'เขียนฟังก์ชันกลับลำดับคำในประโยค "สวัสดี ชาว โลก" -> "โลก ชาว สวัสดี"', hint: 'split(" ") แล้ว reverse()' },
74 { title: 'นับสระ', task: 'นับจำนวนสระภาษาอังกฤษ (a e i o u) ในข้อความ', hint: 'ใช้ regex /[aeiou]/gi' },
75 { title: 'Palindrome', task: 'เช็คว่าคำอ่านกลับหลังได้เหมือนเดิมไหม เช่น "level"', hint: 'เทียบกับ s.split("").reverse().join("")' },
76 { title: 'หาเลขซ้ำ', task: 'จาก array [3, 1, 4, 1, 5, 9, 2, 6, 5] หาเลขที่ซ้ำทั้งหมด', hint: 'ใช้ Set เก็บที่เคยเห็น' },
77]
78
79const REMINDERS = [
80 '💧 ดื่มน้ำสักแก้ว สมองขาดน้ำแล้วง่วง',
81 '👀 กฎ 20-20-20: มองไกล 20 ฟุต (6 เมตร) นาน 20 วินาที',
82 '🧘 ลุกยืดเส้น หมุนไหล่ 10 รอบ แล้วค่อยกลับมา',
83 '🌬️ หายใจลึกๆ 5 ครั้ง ช้าๆ',
84 '🚶 เดินไปเติมน้ำหรือเข้าห้องน้ำ ให้ขาได้ขยับ',
85]
86
87// ==========================================================================
88// บทที่ 2: state ของ Mod
89// ==========================================================================
90let quiz: Quiz | undefined
91let score = { right: 0, total: 0 }
92let pomodoro: { phase: 'focus' | 'break'; endsAt: number; focusMin: number; breakMin: number; rounds: number } | undefined
93let pomoTimer: Timer | undefined
94let reminderTimer: Timer | undefined
95let reminderIndex = 0
96let lateWarnedOn = ''
97
98const pick = <T,>(list: T[]): T => list[Math.floor(Math.random() * list.length)] as T
99
100function mmss(ms: number): string {
101 const s = Math.max(0, Math.round(ms / 1000))
102 return `${Math.floor(s / 60)}:${String(s % 60).padStart(2, '0')}`
103}
104
105// ==========================================================================
106// บทที่ 3: Pomodoro - tick ทุก 1 วินาที อัปเดต status line และสลับโฟกัส/พัก
107// ==========================================================================
108function stopPomodoro($: EngineInterface): void {
109 pomoTimer?.cancel()
110 pomoTimer = undefined
111 pomodoro = undefined
112 $.ui.status(undefined)
113}
114
115function startPomodoro($: EngineInterface, now: number, focusMin: number, breakMin: number): void {
116 stopPomodoro($)
117 pomodoro = { phase: 'focus', endsAt: now + focusMin * 60_000, focusMin, breakMin, rounds: 0 }
118 pomoTimer = $.clock.every(1000, () => void tickPomodoro($))
119}
120
121async function tickPomodoro($: EngineInterface): Promise<void> {
122 if (!pomodoro) return
123 const now = await $.clock.now()
124 const left = pomodoro.endsAt - now
125 if (left <= 0) {
126 if (pomodoro.phase === 'focus') {
127 pomodoro.rounds++
128 pomodoro.phase = 'break'
129 pomodoro.endsAt = now + pomodoro.breakMin * 60_000
130 $.ui.toast(`🍅 ครบ ${pomodoro.focusMin} นาทีแล้ว (รอบที่ ${pomodoro.rounds}) พัก ${pomodoro.breakMin} นาที\n${pick(REMINDERS)}`, { timeoutMs: 15_000 })
131 } else {
132 pomodoro.phase = 'focus'
133 pomodoro.endsAt = now + pomodoro.focusMin * 60_000
134 $.ui.toast('🍅 หมดเวลาพัก กลับมาโฟกัสกันต่อ!')
135 }
136 }
137 $.ui.status(`${pomodoro.phase === 'focus' ? '🍅 โฟกัส' : '☕ พัก'} ${mmss(pomodoro.endsAt - now)}`)
138}
139
140const HELP = `☕ Dev Awake TH (Mod 31)
141========================================
142/awake joke มุกโปรแกรมเมอร์
143/awake quiz | answer <a-d> ควิซความรู้ Dev
144/awake challenge โจทย์โค้ดสั้นๆ (ให้ Claude ช่วยตรวจได้)
145/awake pomodoro [โฟกัส] [พัก] เริ่ม Pomodoro (นาที, ค่าเริ่มต้น 25 5) | pomodoro stop
146/awake remind on [นาที] | off เตือนพักอัตโนมัติ (ค่าเริ่มต้นทุก 50 นาที)
147/awake quote คำคมหนึ่งข้อ | quote ai ให้ Claude แต่งใหม่
148/awake quote on [นาที] | off คำคมหมุนเวียนในแถบสถานะ (ค่าเริ่มต้นทุก 10 นาที)
149/awake status`
150
151export const register: Register = on => {
152 on('session.start', async ($, e, next) => {
153 score = { right: 0, total: 0 }
154 const saved = (await $.store.get('remind')) as { minutes?: number } | undefined
155 if (saved?.minutes) reminderTimer = $.clock.every(saved.minutes * 60_000, () => $.ui.toast(REMINDERS[reminderIndex++ % REMINDERS.length] ?? ''))
156 const q = (await $.store.get('quote')) as { minutes?: number } | undefined
157 if (q?.minutes) quoteTimer = $.clock.every(q.minutes * 60_000, () => {
158 if (!pomodoro) $.ui.status(`💬 ${pick(QUOTES)}`)
159 })
160 await $.command.register({ name: 'awake', description: 'แก้ง่วง: มุก ควิซ โจทย์ Pomodoro เตือนพัก (/awake help)', argumentHint: 'joke|quiz|challenge|pomodoro|remind' })
161 return next(e)
162 })
163
164 // ==========================================================================
165 // บทที่ 4: ทักเมื่อทำงานดึก (หลัง 23:00 หรือก่อนตี 5) วันละครั้ง
166 // ==========================================================================
167 on('prompt.submit', async ($, e, next) => {
168 const d = new Date(await $.clock.now())
169 const today = d.toDateString()
170 if ((d.getHours() >= 23 || d.getHours() < 5) && lateWarnedOn !== today) {
171 lateWarnedOn = today
172 $.ui.toast('🌙 ดึกแล้วนะ โค้ดที่เขียนตอนง่วงมักต้องแก้พรุ่งนี้ ลอง commit แล้วไปนอนไหม')
173 }
174 return next(e)
175 })
176
177 // ==========================================================================
178 // บทที่ 5: คำสั่ง /awake
179 // ==========================================================================
180 on('command.run', { command: 'awake' }, async ($, e) => {
181 const [sub = 'help', a1, a2] = e.args.trim().split(/\s+/).filter(Boolean)
182 const now = await $.clock.now()
183 switch (sub) {
184 case 'joke':
185 return { text: `😂 ${pick(JOKES)}` }
186 case 'quiz': {
187 quiz = pick(QUIZZES)
188 return { text: [`🧠 ${quiz.q}`, ...quiz.choices.map((c, i) => ` ${'abcd'[i]}) ${c}`), '', 'ตอบด้วย /awake answer a'].join('\n') }
189 }
190 case 'answer': {
191 if (!quiz) return { text: 'ยังไม่มีคำถาม /awake quiz' }
192 const i = 'abcd'.indexOf((a1 ?? '').toLowerCase())
193 if (i < 0) return { text: 'ตอบ a, b, c หรือ d' }
194 const correct = i === quiz.answer
195 score = { right: score.right + (correct ? 1 : 0), total: score.total + 1 }
196 const text = `${correct ? '✅ ถูกต้อง!' : `❌ ยังไม่ใช่ คำตอบคือ ${'abcd'[quiz.answer]}) ${quiz.choices[quiz.answer]}`}\n💡 ${quiz.why}\nคะแนน ${score.right}/${score.total}`
197 quiz = undefined
198 return { text }
199 }
200 case 'challenge': {
201 const c = pick(CHALLENGES)
202 return { text: `🧩 ${c.title}\n${c.task}\n\n💡 คำใบ้: ${c.hint}\nเขียนเสร็จแล้วพิมพ์ให้ Claude ตรวจได้เลย เช่น "ตรวจโค้ด FizzBuzz นี้ให้หน่อย"` }
203 }
204 case 'pomodoro': {
205 if (a1 === 'stop') {
206 const rounds = pomodoro?.rounds ?? 0
207 stopPomodoro($)
208 return { text: `⏹️ หยุด Pomodoro (ทำไป ${rounds} รอบ)` }
209 }
210 const focus = Math.min(90, Math.max(1, Number(a1) || 25))
211 const rest = Math.min(30, Math.max(1, Number(a2) || 5))
212 startPomodoro($, now, focus, rest)
213 return { text: `🍅 เริ่มโฟกัส ${focus} นาที แล้วพัก ${rest} นาที นับถอยหลังอยู่ใน status line` }
214 }
215 case 'remind': {
216 reminderTimer?.cancel()
217 reminderTimer = undefined
218 if (a1 === 'off') {
219 await $.store.delete('remind')
220 return { text: '🔕 ปิดการเตือนพักแล้ว' }
221 }
222 const minutes = Math.min(180, Math.max(10, Number(a2) || 50))
223 reminderTimer = $.clock.every(minutes * 60_000, () => $.ui.toast(REMINDERS[reminderIndex++ % REMINDERS.length] ?? ''))
224 await $.store.set('remind', { minutes })
225 return { text: `🔔 จะเตือนพักทุก ${minutes} นาที (ดื่มน้ำ / พักสายตา / ยืดเส้น สลับกัน)` }
226 }
227 case 'quote': {
228 if (a1 === 'off') {
229 quoteTimer?.cancel()
230 quoteTimer = undefined
231 if (!pomodoro) $.ui.status(undefined)
232 await $.store.delete('quote')
233 return { text: 'ปิดคำคมในแถบสถานะแล้ว' }
234 }
235 if (a1 === 'on') {
236 const minutes = Math.max(1, Math.min(120, Number(a2) || 10))
237 quoteTimer?.cancel()
238 // Pomodoro ใช้แถบสถานะอยู่ก็ไม่แย่ง
239 const show = () => {
240 if (!pomodoro) $.ui.status(`💬 ${pick(QUOTES)}`)
241 }
242 show()
243 quoteTimer = $.clock.every(minutes * 60_000, show)
244 await $.store.set('quote', { minutes })
245 return { text: `💬 จะเปลี่ยนคำคมในแถบสถานะทุก ${minutes} นาที` }
246 }
247 if (a1 === 'ai') {
248 const system = 'แต่งคำคมสั้นสำหรับนักพัฒนาซอฟต์แวร์ 1 ประโยค ภาษาไทยที่เป็นธรรมชาติ คมคาย มีอารมณ์ขันเล็กน้อย ไม่เกิน 80 ตัวอักษร ห้ามอ้างว่าเป็นคำพูดของบุคคลจริง ตอบเฉพาะคำคม'
249 for (const model of ['haiku', await $.session.model()]) {
250 const r = await $.model.complete({ model, system, prompt: `หัวข้อสุ่ม: ${pick(['บั๊ก', 'การตั้งชื่อตัวแปร', 'deadline', 'code review', 'กาแฟ', 'เทสต์', 'refactor', 'production'])}`, maxTokens: 120 }).catch(() => undefined)
251 if (r?.isAnswered) return { text: `💬 ${r.text.trim().replace(/^["“]|["”]$/g, '')} — แต่งโดย Claude` }
252 }
253 return { text: `💬 ${pick(QUOTES)}` }
254 }
255 return { text: `💬 ${pick(QUOTES)}` }
256 }
257 case 'status':
258 return { text: `☕ Pomodoro: ${pomodoro ? `${pomodoro.phase === 'focus' ? 'โฟกัส' : 'พัก'} เหลือ ${mmss(pomodoro.endsAt - now)} (รอบ ${pomodoro.rounds})` : 'ปิด'} | เตือนพัก: ${reminderTimer ? 'เปิด' : 'ปิด'} | ควิซ ${score.right}/${score.total}` }
259 default:
260 return { text: HELP }
261 }
262 })
263}
264
265/**
266 * ========================================================================
267 * การบ้าน / ต่อยอด
268 * ========================================================================
269 * 1. ให้ Claude สร้างควิซใหม่จากโค้ดในโปรเจกต์คุณเอง ($.model.complete)
270 * 2. ส่งเตือนพักไปมือถือ/นาฬิกา (ต่อกับ Mod 22)
271 * 3. เก็บสถิติ Pomodoro รายวันใน $.store แล้วสรุปตอนเย็น
272 */
273