Clock
Classic Clock Widget with stopwatch and timer
Code Snippet
1// Initial State
2let mode = "clock"; // Modes: "clock", "stopwatch", "timer"
3
4// Stopwatch state
5let swStartTime = 0;
6let swElapsed = 0;
7let swRunning = false;
8
9// Timer state
10let timerDuration = 5 * 60; // default 5 minutes
11let timerRemaining = timerDuration;
12let timerRunning = false;
13let timerEndTime = 0;
14
15// Formatting Utilities
16function pad(num) {
17 return num.toString().padStart(2, '0');
18}
19
20function formatTime(ms) {
21 const totalSeconds = Math.floor(ms / 1000);
22 const minutes = Math.floor(totalSeconds / 60);
23 const seconds = totalSeconds % 60;
24 const millis = Math.floor((ms % 1000) / 10);
25 return `${pad(minutes)}:${pad(seconds)}.${pad(millis)}`;
26}
27
28function formatTimer(sec) {
29 const m = Math.floor(sec / 60);
30 const s = sec % 60;
31 return `${pad(m)}:${pad(s)}`;
32}
33
34// UI Rendering Function
35async function render() {
36 let content = [];
37
38 // Top Navigation Tabs
39 const tabs = {
40 type: "row",
41 justify: "center",
42 gap: "md",
43 components: [
44 { type: "button", id: "cw_tab_clock", text: "Clock", variant: mode === "clock" ? "primary" : "ghost", size: "sm" },
45 { type: "button", id: "cw_tab_sw", text: "Stopwatch", variant: mode === "stopwatch" ? "primary" : "ghost", size: "sm" },
46 { type: "button", id: "cw_tab_timer", text: "Timer", variant: mode === "timer" ? "primary" : "ghost", size: "sm" },
47 ]
48 };
49
50 content.push(tabs);
51 content.push({ type: "divider" });
52
53 // Mode-specific UI
54 if (mode === "clock") {
55 const now = new Date();
56 const timeString = now.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' });
57 const dateString = now.toLocaleDateString([], { weekday: 'long', month: 'long', day: 'numeric' });
58
59 content.push({
60 type: "column",
61 align: "center",
62 gap: "sm",
63 components: [
64 { type: "text", text: timeString, size: "xl", weight: "bold", color: "accent" },
65 { type: "text", text: dateString, size: "sm", color: "muted" }
66 ]
67 });
68 } else if (mode === "stopwatch") {
69 let display = "00:00.00";
70 if (swRunning) {
71 display = formatTime(swElapsed + (Date.now() - swStartTime));
72 } else {
73 display = formatTime(swElapsed);
74 }
75
76 content.push({
77 type: "column",
78 align: "center",
79 gap: "md",
80 components: [
81 { type: "text", text: display, size: "xl", weight: "bold", color: "default" },
82 {
83 type: "row",
84 justify: "center",
85 gap: "md",
86 components: [
87 { type: "button", id: "cw_sw_toggle", text: swRunning ? "Stop" : "Start", variant: swRunning ? "danger" : "success" },
88 { type: "button", id: "cw_sw_reset", text: "Reset", variant: "ghost", disabled: swRunning }
89 ]
90 }
91 ]
92 });
93 } else if (mode === "timer") {
94 let display = formatTimer(timerRemaining);
95 if (timerRunning) {
96 const left = Math.max(0, Math.ceil((timerEndTime - Date.now()) / 1000));
97 display = formatTimer(left);
98 }
99
100 content.push({
101 type: "column",
102 align: "center",
103 gap: "md",
104 components: [
105 { type: "text", text: display, size: "xl", weight: "bold", color: timerRemaining <= 10 && timerRemaining > 0 ? "warning" : "default" },
106 {
107 type: "row",
108 justify: "center",
109 gap: "md",
110 components: [
111 { type: "button", id: "cw_timer_toggle", text: timerRunning ? "Pause" : "Start", variant: timerRunning ? "danger" : "success" },
112 { type: "button", id: "cw_timer_reset", text: "Reset", variant: "ghost" }
113 ]
114 },
115 (!timerRunning ? {
116 type: "row",
117 justify: "center",
118 align: "center",
119 gap: "sm",
120 components: [
121 { type: "input", id: "cw_timer_input_m", placeholder: "Min", value: Math.floor(timerDuration/60).toString(), size: "sm" },
122 { type: "text", text: ":", weight: "bold" },
123 { type: "input", id: "cw_timer_input_s", placeholder: "Sec", value: (timerDuration%60).toString(), size: "sm" },
124 { type: "button", id: "cw_timer_set", text: "Set", variant: "ghost", size: "md" }
125 ]
126 } : null)
127 ].filter(Boolean)
128 });
129 }
130
131 // Push the updated tree to the widget API
132 await widget.update([{
133 type: "column",
134 bg: "card",
135 rounded: true,
136 padding: "lg",
137 gap: "lg",
138 components: content
139 }]);
140}
141
142// 1. Initial render
143await widget.show([{ type: "text", text: "Loading clock..." }], "bottom-right", { theme: "dark" });
144await render();
145
146// 2. Set up asynchronous listeners
147widget.on("cw_tab_clock", "click", async () => { mode = "clock"; await render(); });
148widget.on("cw_tab_sw", "click", async () => { mode = "stopwatch"; await render(); });
149widget.on("cw_tab_timer", "click", async () => { mode = "timer"; await render(); });
150
151// Stopwatch controls
152widget.on("cw_sw_toggle", "click", async () => {
153 if (swRunning) {
154 swRunning = false;
155 swElapsed += Date.now() - swStartTime;
156 } else {
157 swRunning = true;
158 swStartTime = Date.now();
159 }
160 await render();
161});
162
163widget.on("cw_sw_reset", "click", async () => {
164 if (!swRunning) {
165 swElapsed = 0;
166 await render();
167 }
168});
169
170// Timer controls
171widget.on("cw_timer_toggle", "click", async () => {
172 if (timerRunning) {
173 timerRunning = false;
174 timerRemaining = Math.max(0, Math.ceil((timerEndTime - Date.now()) / 1000));
175 } else {
176 if (timerRemaining > 0) {
177 timerRunning = true;
178 timerEndTime = Date.now() + (timerRemaining * 1000);
179 }
180 }
181 await render();
182});
183
184widget.on("cw_timer_reset", "click", async () => {
185 timerRunning = false;
186 timerRemaining = timerDuration;
187 await render();
188});
189
190widget.on("cw_timer_set", "click", async () => {
191 const valM = await widget.getValue("cw_timer_input_m");
192 const valS = await widget.getValue("cw_timer_input_s");
193
194 // Parse the inputs, defaulting to 0 if left empty
195 const mins = parseInt(valM, 10) || 0;
196 const secs = parseInt(valS, 10) || 0;
197
198 if (mins >= 0 && secs >= 0 && (mins > 0 || secs > 0)) {
199 timerDuration = (mins * 60) + secs;
200 timerRemaining = timerDuration;
201 notify.show(`Timer set to ${mins}m ${secs}s.`, "success");
202 await render();
203 } else {
204 notify.show("Please enter a valid time.", "error");
205 }
206});
207
208// 3. Main Update Loop (Ticks every 100ms for stopwatch accuracy)
209const timerInterval = setInterval(async () => {
210 if (mode === "clock" || mode === "stopwatch") {
211 await render();
212 } else if (mode === "timer" && timerRunning) {
213 const left = Math.max(0, Math.ceil((timerEndTime - Date.now()) / 1000));
214 if (left <= 0) {
215 timerRunning = false;
216 timerRemaining = 0;
217 notify.show("Timer finished!", "success");
218 } else {
219 timerRemaining = left;
220 }
221 await render();
222 }
223}, 100);
224
225// 4. Shut down the script cleanly when the user closes the Widget UI!
226await widget.waitForEvent("__widget_closed__", "close");
227
228// Critical: Stop the background loop so it doesn't keep updating a dead widget!
229clearInterval(timerInterval);
230Submitted on 7/11/2026