1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
|
/**
* Notifications Widget
*
* Displays badge counts from running applications with active notifications.
* Uses macOS lsappinfo to query app status labels (dock badges).
*
* Requirements:
* - macOS (lsappinfo is macOS-specific)
* - No special permissions required
*
* @module notifications
*/
import * as Uebersicht from "uebersicht";
import * as AppIcons from "../../app-icons.js";
import { SuspenseIcon } from "../icons/icon.jsx";
import useWidgetRefresh from "../../hooks/use-widget-refresh";
import useServerSocket from "../../hooks/use-server-socket";
import { useSimpleBarContext } from "../simple-bar-context.jsx";
import * as Utils from "../../utils";
export { notificationsStyles as styles } from "../../styles/components/data/notifications";
const { React } = Uebersicht;
const DEFAULT_REFRESH_FREQUENCY = 10000;
// Shell command to get notification badges from running apps
const COMMAND = `lsappinfo -all list 2>/dev/null | perl -0777 -pe 's/---/\\n---\\n/g' | perl -ne '
if (/"CFBundleName"="([^"]+)"/) { $app = $1; }
if (/"StatusLabel"=\\{ "label"="([^"]*)"/) { $badge = $1; }
if (/"LSBundlePath"="([^"]+)"/) { $path = $1; }
if (/^---$/ || eof) {
print "$app|$badge|$path\\n" if ($app && $badge);
($app, $badge, $path) = ("", "", "");
}
'`;
/**
* Parses shell command output into notification objects.
* @param {string} output - Raw command output from lsappinfo
* @returns {Array<{name: string, badge: string, bundlePath: string}>} Array of notification objects
*/
function parseNotifications(output) {
if (!output || !output.trim()) return [];
return output
.trim()
.split("\n")
.filter((line) => line.includes("|"))
.map((line) => {
const [name, badge, bundlePath] = line.split("|");
return { name, badge, bundlePath };
})
.filter((item) => item.name && item.badge);
}
/**
* Single app notification pill component.
* @component
* @param {Object} props - Component props
* @param {Object} props.app - Application notification data
* @param {string} props.app.name - Application name
* @param {string} props.app.badge - Badge count
* @param {string} props.app.bundlePath - Path to application bundle
* @returns {JSX.Element} Notification pill button
*/
const NotificationPill = React.memo(({ app }) => {
/**
* Opens the application when pill is clicked.
* @param {React.MouseEvent} e - Click event
*/
const openApp = async (e) => {
Utils.clickEffect(e);
// Escape special shell characters to prevent injection
const safePath = app.bundlePath.replace(/["$`\\]/g, "\\$&");
await Uebersicht.run(`open "${safePath}"`);
};
// Get the SVG icon for this app, or use Default
const Icon = AppIcons.apps[app.name] || AppIcons.apps.Default;
return (
<button
className="notification-pill"
onClick={openApp}
title={`${app.name}: ${app.badge} notification${app.badge !== "1" ? "s" : ""}`}
aria-label={`Open ${app.name} - ${app.badge} notification${app.badge !== "1" ? "s" : ""}`}
>
<SuspenseIcon>
<Icon className="notification-pill__icon" />
</SuspenseIcon>
<span className="notification-pill__badge">{app.badge}</span>
</button>
);
});
NotificationPill.displayName = "NotificationPill";
/**
* Notification Badges widget component.
* Shows each app with notifications as an inline pill.
* @component
* @returns {JSX.Element|null} The notifications widget component or null if no notifications
*/
export const Widget = React.memo(() => {
const { displayIndex, settings } = useSimpleBarContext();
const { widgets, notificationsWidgetOptions } = settings;
const { notificationsWidget } = widgets;
const { refreshFrequency, showOnDisplay, excludedApps } = notificationsWidgetOptions;
// Determine if the widget should be visible based on display settings
const visible =
Utils.isVisibleOnDisplay(displayIndex, showOnDisplay) &&
notificationsWidget;
// Calculate the refresh frequency for the widget
const refresh = React.useMemo(
() =>
Utils.getRefreshFrequency(refreshFrequency, DEFAULT_REFRESH_FREQUENCY),
[refreshFrequency],
);
const [state, setState] = React.useState([]);
const [loading, setLoading] = React.useState(visible);
/**
* Resets the widget state.
*/
const resetWidget = React.useCallback(() => {
setState([]);
setLoading(false);
}, []);
// Build the exclusion list from the comma-separated setting
const exclusionList = React.useMemo(
() =>
excludedApps
? excludedApps
.split(",")
.map((s) => s.trim().toLowerCase())
.filter(Boolean)
: [],
[excludedApps],
);
/**
* Fetches notification badges and updates the state.
*/
const getNotifications = React.useCallback(async () => {
if (!visible) return;
try {
const output = await Utils.cachedRun(COMMAND, refresh);
const notifications = parseNotifications(output).filter(
(item) => !exclusionList.includes(item.name.toLowerCase()),
);
setState(notifications);
} catch (error) {
// eslint-disable-next-line no-console
console.error("Error fetching notifications:", error);
setState([]);
}
setLoading(false);
}, [visible, refresh, exclusionList]);
// Server socket for real-time updates
useServerSocket(
"notifications",
visible,
getNotifications,
resetWidget,
setLoading,
);
// Refresh the widget at the specified interval
useWidgetRefresh(visible, getNotifications, refresh);
// Don't render anything if loading or no notifications
if (loading || !state.length) return null;
return (
<div className="notifications">
{state.map((app) => (
<NotificationPill key={app.bundlePath} app={app} />
))}
</div>
);
});
Widget.displayName = "Notifications";
|