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);
230
Submitted on 7/11/2026