Skip to content

Commit 59d8028

Browse files
mswiszczclaude
andcommitted
feat(badge): implement sound playback and border state management
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 63aafdc commit 59d8028

File tree

1 file changed

+80
-5
lines changed

1 file changed

+80
-5
lines changed

frontend/app/store/badge.ts

Lines changed: 80 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,61 @@ export type TabBadgesEnv = WaveEnvSubset<{
3030
const BadgeMap = new Map<string, PrimitiveAtom<Badge>>();
3131
const TabBadgeAtomCache = new Map<string, Atom<Badge[]>>();
3232

33+
type BlockBorder = {
34+
color: string;
35+
};
36+
37+
const BorderMap = new Map<string, PrimitiveAtom<BlockBorder | null>>();
38+
39+
function getBorderAtom(oref: string): PrimitiveAtom<BlockBorder | null> {
40+
if (oref == null) {
41+
return NullAtom as PrimitiveAtom<BlockBorder | null>;
42+
}
43+
let rtn = BorderMap.get(oref);
44+
if (rtn == null) {
45+
rtn = atom(null) as PrimitiveAtom<BlockBorder | null>;
46+
BorderMap.set(oref, rtn);
47+
}
48+
return rtn;
49+
}
50+
51+
function getBlockBorderAtom(blockId: string): Atom<BlockBorder | null> {
52+
if (blockId == null) {
53+
return NullAtom as Atom<BlockBorder | null>;
54+
}
55+
const oref = WOS.makeORef("block", blockId);
56+
return getBorderAtom(oref);
57+
}
58+
59+
const BUILTIN_SOUND_PRESETS = new Set(["chime", "ping", "gentle"]);
60+
61+
function playBadgeSound(sound: string): void {
62+
if (!sound) {
63+
return;
64+
}
65+
if (sound === "system") {
66+
fireAndForget(() => RpcApi.ElectronSystemBellCommand(TabRpcClient, { route: "electron" }));
67+
return;
68+
}
69+
let audioPath: string;
70+
if (BUILTIN_SOUND_PRESETS.has(sound)) {
71+
audioPath = `/sounds/${sound}.mp3`;
72+
} else {
73+
// Custom sound from ~/.waveterm/sounds/
74+
const electronApi = (window as any).api as ElectronApi;
75+
const configDir = electronApi?.getConfigDir?.();
76+
if (!configDir) {
77+
console.warn(`[badge] cannot resolve config dir for custom sound: ${sound}`);
78+
return;
79+
}
80+
audioPath = `file://${configDir}/sounds/${sound}`;
81+
}
82+
const audio = new Audio(audioPath);
83+
audio.play().catch((e) => {
84+
console.warn(`[badge] failed to play sound "${sound}":`, e);
85+
});
86+
}
87+
3388
function publishBadgeEvent(eventData: WaveEvent, env?: BadgeEnv) {
3489
if (env != null) {
3590
fireAndForget(() => env.rpc.EventPublishCommand(TabRpcClient, eventData));
@@ -57,6 +112,11 @@ function clearBadgesForBlockOnFocus(blockId: string, env?: BadgeEnv) {
57112
if (badge != null && !badge.pidlinked) {
58113
clearBadgeInternal(oref, env);
59114
}
115+
// Always clear border on focus
116+
const borderAtom = BorderMap.get(oref);
117+
if (borderAtom != null && globalStore.get(borderAtom) != null) {
118+
globalStore.set(borderAtom, null);
119+
}
60120
}
61121

62122
function clearBadgesForTabOnFocus(tabId: string, env?: BadgeEnv) {
@@ -199,6 +259,9 @@ function setupBadgesSubscription() {
199259
for (const atom of BadgeMap.values()) {
200260
globalStore.set(atom, null);
201261
}
262+
for (const atom of BorderMap.values()) {
263+
globalStore.set(atom, null);
264+
}
202265
return;
203266
}
204267
if (data?.oref == null) {
@@ -214,14 +277,25 @@ function setupBadgesSubscription() {
214277
}
215278
if (data.clear) {
216279
globalStore.set(curAtom, null);
280+
const borderAtom = getBorderAtom(data.oref);
281+
globalStore.set(borderAtom, null);
217282
return;
218283
}
219-
if (data.badge == null) {
220-
return;
284+
if (data.badge != null) {
285+
const existing = globalStore.get(curAtom);
286+
if (existing == null || cmpBadge(data.badge, existing) > 0) {
287+
globalStore.set(curAtom, data.badge);
288+
}
289+
}
290+
// Play sound if requested
291+
if (data.sound) {
292+
playBadgeSound(data.sound);
221293
}
222-
const existing = globalStore.get(curAtom);
223-
if (existing == null || cmpBadge(data.badge, existing) > 0) {
224-
globalStore.set(curAtom, data.badge);
294+
// Set border highlight if requested
295+
if (data.border) {
296+
const borderColor = data.bordercolor || data.badge?.color || "#fbbf24";
297+
const borderAtom = getBorderAtom(data.oref);
298+
globalStore.set(borderAtom, { color: borderColor });
225299
}
226300
},
227301
});
@@ -258,6 +332,7 @@ export {
258332
clearBadgesForTabOnFocus,
259333
getBadgeAtom,
260334
getBlockBadgeAtom,
335+
getBlockBorderAtom,
261336
getTabBadgeAtom,
262337
loadBadges,
263338
setBadge,

0 commit comments

Comments
 (0)