From d06784958d73159b5e4abafe5114804662114ed7 Mon Sep 17 00:00:00 2001 From: Ryan Schanzenbacher Date: Thu, 9 Jul 2026 22:15:53 -0400 Subject: move ubersicht modules in tree --- .../lib/components/data/notifications.jsx | 189 +++++++++++++++++++++ 1 file changed, 189 insertions(+) create mode 100755 users/ryan/modules/simple-bar/simple-bar-source/lib/components/data/notifications.jsx (limited to 'users/ryan/modules/simple-bar/simple-bar-source/lib/components/data/notifications.jsx') 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 @@ +/** + * 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 ( + + ); +}); + +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 ( +
+ {state.map((app) => ( + + ))} +
+ ); +}); + +Widget.displayName = "Notifications"; -- cgit v1.3