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