diff options
Diffstat (limited to 'users/ryan/modules/simple-bar/simple-bar-source/lib/components/data')
29 files changed, 4431 insertions, 0 deletions
diff --git a/users/ryan/modules/simple-bar/simple-bar-source/lib/components/data/battery.jsx b/users/ryan/modules/simple-bar/simple-bar-source/lib/components/data/battery.jsx new file mode 100755 index 0000000..1f08af6 --- /dev/null +++ b/users/ryan/modules/simple-bar/simple-bar-source/lib/components/data/battery.jsx | |||
| @@ -0,0 +1,183 @@ | |||
| 1 | import * as Uebersicht from "uebersicht"; | ||
| 2 | import * as DataWidget from "./data-widget.jsx"; | ||
| 3 | import * as DataWidgetLoader from "./data-widget-loader.jsx"; | ||
| 4 | import * as Icons from "../icons/icons.jsx"; | ||
| 5 | import { SuspenseIcon } from "../icons/icon.jsx"; | ||
| 6 | import useWidgetRefresh from "../../hooks/use-widget-refresh"; | ||
| 7 | import useServerSocket from "../../hooks/use-server-socket"; | ||
| 8 | import { useSimpleBarContext } from "../simple-bar-context.jsx"; | ||
| 9 | import * as Utils from "../../utils"; | ||
| 10 | |||
| 11 | export { batteryStyles as styles } from "../../styles/components/data/battery"; | ||
| 12 | |||
| 13 | const { React } = Uebersicht; | ||
| 14 | |||
| 15 | const DEFAULT_REFRESH_FREQUENCY = 10000; | ||
| 16 | |||
| 17 | /** | ||
| 18 | * Battery widget component | ||
| 19 | * @returns {JSX.Element|null} The battery widget component | ||
| 20 | */ | ||
| 21 | export const Widget = React.memo(() => { | ||
| 22 | const { displayIndex, settings, pushMissive } = useSimpleBarContext(); | ||
| 23 | const { widgets, batteryWidgetOptions } = settings; | ||
| 24 | const { batteryWidget } = widgets; | ||
| 25 | const { | ||
| 26 | refreshFrequency, | ||
| 27 | toggleCaffeinateOnClick, | ||
| 28 | caffeinateOption, | ||
| 29 | disableCaffeinateInvertedBackground, | ||
| 30 | showOnDisplay, | ||
| 31 | showIcon, | ||
| 32 | } = batteryWidgetOptions; | ||
| 33 | |||
| 34 | // Determine if the widget should be visible based on display settings | ||
| 35 | const visible = | ||
| 36 | Utils.isVisibleOnDisplay(displayIndex, showOnDisplay) && batteryWidget; | ||
| 37 | |||
| 38 | // Calculate the refresh frequency for the widget | ||
| 39 | const refresh = React.useMemo( | ||
| 40 | () => | ||
| 41 | Utils.getRefreshFrequency(refreshFrequency, DEFAULT_REFRESH_FREQUENCY), | ||
| 42 | [refreshFrequency], | ||
| 43 | ); | ||
| 44 | |||
| 45 | const [state, setState] = React.useState(); | ||
| 46 | const [loading, setLoading] = React.useState(visible); | ||
| 47 | |||
| 48 | // Reset the widget state | ||
| 49 | const resetWidget = () => { | ||
| 50 | setState(undefined); | ||
| 51 | setLoading(false); | ||
| 52 | }; | ||
| 53 | |||
| 54 | /** | ||
| 55 | * Fetch battery information and update the state | ||
| 56 | */ | ||
| 57 | const getBattery = React.useCallback(async () => { | ||
| 58 | if (!visible) return; | ||
| 59 | // Fetch battery information and parse the results | ||
| 60 | const [system, percentage, status, caffeinate, lowPowerMode] = | ||
| 61 | await Promise.all([ | ||
| 62 | Utils.getSystem(), | ||
| 63 | Utils.cachedRun( | ||
| 64 | `pmset -g batt | grep -Eo '[0-9]+%' | head -1 | tr -d '%'`, | ||
| 65 | refresh, | ||
| 66 | ), | ||
| 67 | Utils.cachedRun( | ||
| 68 | `pmset -g batt | head -1 | grep -q 'AC Power' && echo 'AC' || echo 'Batt'`, | ||
| 69 | refresh, | ||
| 70 | ), | ||
| 71 | Uebersicht.run(`pgrep caffeinate -d`), | ||
| 72 | Utils.cachedRun( | ||
| 73 | `pmset -g | awk '/lowpowermode|powermode/ {print $2; exit}'`, | ||
| 74 | refresh, | ||
| 75 | ), | ||
| 76 | ]); | ||
| 77 | setState({ | ||
| 78 | system, | ||
| 79 | percentage: parseInt(percentage, 10), | ||
| 80 | charging: Utils.cleanupOutput(status) === "AC", | ||
| 81 | caffeinate: Utils.cleanupOutput(caffeinate), | ||
| 82 | lowPowerMode: Utils.cleanupOutput(lowPowerMode) === "1", | ||
| 83 | }); | ||
| 84 | setLoading(false); | ||
| 85 | }, [visible, refresh]); | ||
| 86 | |||
| 87 | // Use server socket to fetch battery data | ||
| 88 | useServerSocket("battery", visible, getBattery, resetWidget, setLoading); | ||
| 89 | // Refresh the widget at the specified interval | ||
| 90 | useWidgetRefresh(visible, getBattery, refresh); | ||
| 91 | |||
| 92 | if (loading) return <DataWidgetLoader.Widget className="battery" />; | ||
| 93 | if (!state) return null; | ||
| 94 | |||
| 95 | const { system, percentage, charging, caffeinate, lowPowerMode } = state; | ||
| 96 | const isLowBattery = !charging && percentage < 20; | ||
| 97 | |||
| 98 | const classes = Utils.classNames("battery", { | ||
| 99 | "battery--low": isLowBattery, | ||
| 100 | "battery--low-power-mode": lowPowerMode, | ||
| 101 | "battery--caffeinate": | ||
| 102 | !disableCaffeinateInvertedBackground && caffeinate.length > 0, | ||
| 103 | }); | ||
| 104 | |||
| 105 | const transformValue = getTransform(percentage); | ||
| 106 | |||
| 107 | /** | ||
| 108 | * Handle click event to toggle caffeinate mode | ||
| 109 | * @param {React.MouseEvent} e - The click event | ||
| 110 | */ | ||
| 111 | const onClick = async (e) => { | ||
| 112 | Utils.clickEffect(e); | ||
| 113 | await toggleCaffeinate(system, caffeinate, caffeinateOption, pushMissive); | ||
| 114 | getBattery(); | ||
| 115 | }; | ||
| 116 | |||
| 117 | const onClickProp = toggleCaffeinateOnClick ? { onClick } : {}; | ||
| 118 | |||
| 119 | const Icon = () => ( | ||
| 120 | <div className="battery__icon"> | ||
| 121 | <div className="battery__icon-inner"> | ||
| 122 | <div | ||
| 123 | className="battery__icon-filler" | ||
| 124 | style={{ transform: transformValue }} | ||
| 125 | /> | ||
| 126 | {charging && ( | ||
| 127 | <SuspenseIcon> | ||
| 128 | <Icons.Charging className="battery__charging-icon" /> | ||
| 129 | </SuspenseIcon> | ||
| 130 | )} | ||
| 131 | </div> | ||
| 132 | </div> | ||
| 133 | ); | ||
| 134 | |||
| 135 | return ( | ||
| 136 | <DataWidget.Widget | ||
| 137 | classes={classes} | ||
| 138 | Icon={showIcon ? Icon : null} | ||
| 139 | disableSlider | ||
| 140 | {...onClickProp} | ||
| 141 | > | ||
| 142 | {caffeinate.length > 0 && ( | ||
| 143 | <SuspenseIcon> | ||
| 144 | <Icons.Coffee className="battery__caffeinate-icon" /> | ||
| 145 | </SuspenseIcon> | ||
| 146 | )} | ||
| 147 | {percentage}% | ||
| 148 | </DataWidget.Widget> | ||
| 149 | ); | ||
| 150 | }); | ||
| 151 | |||
| 152 | Widget.displayName = "Battery"; | ||
| 153 | |||
| 154 | /** | ||
| 155 | * Get the transform value for the battery icon based on the percentage | ||
| 156 | * @param {number} value - The battery percentage | ||
| 157 | * @returns {string} The transform value | ||
| 158 | */ | ||
| 159 | function getTransform(value) { | ||
| 160 | let transform = `0.${value}`; | ||
| 161 | if (value === 100) transform = "1"; | ||
| 162 | if (value < 10) transform = `0.0${value}`; | ||
| 163 | return `scaleX(${transform})`; | ||
| 164 | } | ||
| 165 | |||
| 166 | /** | ||
| 167 | * Toggle caffeinate mode on or off | ||
| 168 | * @param {string} system - The system architecture | ||
| 169 | * @param {string} caffeinate - The current caffeinate state | ||
| 170 | * @param {string} option - The caffeinate option | ||
| 171 | * @param {function} pushMissive - Function to push notifications | ||
| 172 | */ | ||
| 173 | async function toggleCaffeinate(system, caffeinate, option, pushMissive) { | ||
| 174 | const command = | ||
| 175 | system === "x86_64" ? "caffeinate" : "arch -arch arm64 caffeinate"; | ||
| 176 | if (caffeinate.length === 0) { | ||
| 177 | Uebersicht.run(`${command} ${option} &`); | ||
| 178 | Utils.notification("Enabling caffeinate...", pushMissive); | ||
| 179 | } else { | ||
| 180 | await Uebersicht.run("pkill -f caffeinate"); | ||
| 181 | Utils.notification("Disabling caffeinate...", pushMissive); | ||
| 182 | } | ||
| 183 | } | ||
diff --git a/users/ryan/modules/simple-bar/simple-bar-source/lib/components/data/browser-track.jsx b/users/ryan/modules/simple-bar/simple-bar-source/lib/components/data/browser-track.jsx new file mode 100755 index 0000000..d320e4a --- /dev/null +++ b/users/ryan/modules/simple-bar/simple-bar-source/lib/components/data/browser-track.jsx | |||
| @@ -0,0 +1,147 @@ | |||
| 1 | import * as Uebersicht from "uebersicht"; | ||
| 2 | import * as DataWidget from "./data-widget.jsx"; | ||
| 3 | import * as DataWidgetLoader from "./data-widget-loader.jsx"; | ||
| 4 | import * as Icons from "../icons/icons.jsx"; | ||
| 5 | import { SuspenseIcon } from "../icons/icon.jsx"; | ||
| 6 | import useWidgetRefresh from "../../hooks/use-widget-refresh"; | ||
| 7 | import useServerSocket from "../../hooks/use-server-socket"; | ||
| 8 | import { useSimpleBarContext } from "../simple-bar-context.jsx"; | ||
| 9 | import * as Utils from "../../utils"; | ||
| 10 | |||
| 11 | export { browserTrackStyles as styles } from "../../styles/components/data/browser-track"; | ||
| 12 | |||
| 13 | const { React } = Uebersicht; | ||
| 14 | |||
| 15 | const DEFAULT_REFRESH_FREQUENCY = 10000; | ||
| 16 | |||
| 17 | /** | ||
| 18 | * BrowserTrack Widget component | ||
| 19 | * @returns {JSX.Element|null} The BrowserTrack widget | ||
| 20 | */ | ||
| 21 | export const Widget = React.memo(() => { | ||
| 22 | const { displayIndex, settings } = useSimpleBarContext(); | ||
| 23 | const { widgets, browserTrackWidgetOptions } = settings; | ||
| 24 | const { browserTrackWidget } = widgets; | ||
| 25 | const { refreshFrequency, showSpecter, showOnDisplay, showIcon } = | ||
| 26 | browserTrackWidgetOptions; | ||
| 27 | const visible = | ||
| 28 | Utils.isVisibleOnDisplay(displayIndex, showOnDisplay) && browserTrackWidget; | ||
| 29 | |||
| 30 | const refresh = React.useMemo( | ||
| 31 | () => | ||
| 32 | Utils.getRefreshFrequency(refreshFrequency, DEFAULT_REFRESH_FREQUENCY), | ||
| 33 | [refreshFrequency], | ||
| 34 | ); | ||
| 35 | |||
| 36 | const ref = React.useRef(); | ||
| 37 | |||
| 38 | const [state, setState] = React.useState(); | ||
| 39 | const [loading, setLoading] = React.useState(visible); | ||
| 40 | |||
| 41 | /** | ||
| 42 | * Resets the widget state | ||
| 43 | */ | ||
| 44 | const resetWidget = () => { | ||
| 45 | setState(undefined); | ||
| 46 | setLoading(false); | ||
| 47 | }; | ||
| 48 | |||
| 49 | /** | ||
| 50 | * Fetches the current browser track information | ||
| 51 | */ | ||
| 52 | const getBrowserTrack = React.useCallback(async () => { | ||
| 53 | if (!visible) return; | ||
| 54 | const [firefoxStatus, firefoxDevStatus] = await Promise.all([ | ||
| 55 | Utils.cachedRun( | ||
| 56 | `pgrep -xq 'Firefox' && echo "true" || echo "false"`, | ||
| 57 | refresh, | ||
| 58 | ), | ||
| 59 | Utils.cachedRun( | ||
| 60 | `pgrep -xq 'Firefox Developer Edition' && echo "true" || echo "false"`, | ||
| 61 | refresh, | ||
| 62 | ), | ||
| 63 | ]); | ||
| 64 | const isFirefoxDevRunning = | ||
| 65 | Utils.cleanupOutput(firefoxDevStatus) === "true"; | ||
| 66 | const isFirefoxRunning = Utils.cleanupOutput(firefoxStatus) === "true"; | ||
| 67 | const scriptNamePrefix = isFirefoxDevRunning | ||
| 68 | ? "firefox-dev" | ||
| 69 | : isFirefoxRunning | ||
| 70 | ? "firefox" | ||
| 71 | : "browser"; | ||
| 72 | |||
| 73 | const [browserTrackOutput, spotifyStatus] = await Promise.all([ | ||
| 74 | Utils.cachedRun( | ||
| 75 | `osascript ./simple-bar/lib/scripts/${scriptNamePrefix}-audio.applescript 2>&1`, | ||
| 76 | refresh, | ||
| 77 | ), | ||
| 78 | Utils.cachedRun( | ||
| 79 | `pgrep -fq '[S]potify Helper' && echo "true" || echo "false"`, | ||
| 80 | refresh, | ||
| 81 | ), | ||
| 82 | ]); | ||
| 83 | const browserTrack = JSON.parse(browserTrackOutput); | ||
| 84 | setState({ | ||
| 85 | ...browserTrack, | ||
| 86 | isSpotifyRunning: Utils.cleanupOutput(spotifyStatus) === "true", | ||
| 87 | }); | ||
| 88 | setLoading(false); | ||
| 89 | }, [visible, refresh]); | ||
| 90 | |||
| 91 | useServerSocket( | ||
| 92 | "browser-track", | ||
| 93 | visible, | ||
| 94 | getBrowserTrack, | ||
| 95 | resetWidget, | ||
| 96 | setLoading, | ||
| 97 | ); | ||
| 98 | useWidgetRefresh(visible, getBrowserTrack, refresh); | ||
| 99 | |||
| 100 | if (loading) return <DataWidgetLoader.Widget className="browser-track" />; | ||
| 101 | if (!state) return null; | ||
| 102 | const { browser, title, isSpotifyRunning } = state; | ||
| 103 | |||
| 104 | if (!browser?.length || !title?.length || isSpotifyRunning) return null; | ||
| 105 | |||
| 106 | /** | ||
| 107 | * Icon component for displaying browser and playing icons | ||
| 108 | * @returns {JSX.Element} The icon component | ||
| 109 | */ | ||
| 110 | const Icon = () => { | ||
| 111 | const BrowserIcon = getIcon(browser); | ||
| 112 | return ( | ||
| 113 | <div className="browser-track__icons"> | ||
| 114 | <SuspenseIcon> | ||
| 115 | <BrowserIcon /> | ||
| 116 | <Icons.Playing /> | ||
| 117 | </SuspenseIcon> | ||
| 118 | </div> | ||
| 119 | ); | ||
| 120 | }; | ||
| 121 | |||
| 122 | return ( | ||
| 123 | <DataWidget.Widget | ||
| 124 | ref={ref} | ||
| 125 | classes="browser-track" | ||
| 126 | Icon={showIcon ? Icon : null} | ||
| 127 | showSpecter={showSpecter} | ||
| 128 | > | ||
| 129 | {title} | ||
| 130 | </DataWidget.Widget> | ||
| 131 | ); | ||
| 132 | }); | ||
| 133 | |||
| 134 | Widget.displayName = "BrowserTrack"; | ||
| 135 | |||
| 136 | /** | ||
| 137 | * Returns the appropriate icon component based on the browser name | ||
| 138 | * @param {string} browser - The name of the browser | ||
| 139 | * @returns {JSX.Element} The icon component for the browser | ||
| 140 | */ | ||
| 141 | function getIcon(browser) { | ||
| 142 | if (browser === "chrome") return Icons.GoogleChrome; | ||
| 143 | if (browser === "brave") return Icons.BraveBrowser; | ||
| 144 | if (browser === "safari") return Icons.Safari; | ||
| 145 | if (browser === "firefox") return Icons.Firefox; | ||
| 146 | return Icons.Default; | ||
| 147 | } | ||
diff --git a/users/ryan/modules/simple-bar/simple-bar-source/lib/components/data/cpu.jsx b/users/ryan/modules/simple-bar/simple-bar-source/lib/components/data/cpu.jsx new file mode 100755 index 0000000..378b3c9 --- /dev/null +++ b/users/ryan/modules/simple-bar/simple-bar-source/lib/components/data/cpu.jsx | |||
| @@ -0,0 +1,153 @@ | |||
| 1 | import * as Uebersicht from "uebersicht"; | ||
| 2 | import * as DataWidget from "./data-widget.jsx"; | ||
| 3 | import * as DataWidgetLoader from "./data-widget-loader.jsx"; | ||
| 4 | import Graph from "./graph.jsx"; | ||
| 5 | import * as Icons from "../icons/icons.jsx"; | ||
| 6 | import useWidgetRefresh from "../../hooks/use-widget-refresh"; | ||
| 7 | import useServerSocket from "../../hooks/use-server-socket"; | ||
| 8 | import { useSimpleBarContext } from "../simple-bar-context.jsx"; | ||
| 9 | import * as Utils from "../../utils"; | ||
| 10 | |||
| 11 | export { cpuStyles as styles } from "../../styles/components/data/cpu"; | ||
| 12 | |||
| 13 | const { React } = Uebersicht; | ||
| 14 | |||
| 15 | const DEFAULT_REFRESH_FREQUENCY = 2000; | ||
| 16 | const GRAPH_LENGTH = 50; | ||
| 17 | |||
| 18 | /** | ||
| 19 | * CPU Widget component | ||
| 20 | * @returns {JSX.Element|null} The CPU widget | ||
| 21 | */ | ||
| 22 | export const Widget = React.memo(() => { | ||
| 23 | const { displayIndex, settings } = useSimpleBarContext(); | ||
| 24 | const { widgets, cpuWidgetOptions } = settings; | ||
| 25 | const { cpuWidget } = widgets; | ||
| 26 | const { | ||
| 27 | refreshFrequency, | ||
| 28 | showOnDisplay, | ||
| 29 | displayAsGraph, | ||
| 30 | cpuMonitorApp, | ||
| 31 | showIcon, | ||
| 32 | cpuUsageThreshold, | ||
| 33 | } = cpuWidgetOptions; | ||
| 34 | |||
| 35 | // Determine if the widget should be visible based on display settings | ||
| 36 | const visible = | ||
| 37 | Utils.isVisibleOnDisplay(displayIndex, showOnDisplay) && cpuWidget; | ||
| 38 | |||
| 39 | // Set the refresh frequency for the widget | ||
| 40 | const refresh = React.useMemo( | ||
| 41 | () => | ||
| 42 | Utils.getRefreshFrequency(refreshFrequency, DEFAULT_REFRESH_FREQUENCY), | ||
| 43 | [refreshFrequency], | ||
| 44 | ); | ||
| 45 | |||
| 46 | const [graph, setGraph] = React.useState([]); | ||
| 47 | const [state, setState] = React.useState(); | ||
| 48 | const [loading, setLoading] = React.useState(visible); | ||
| 49 | |||
| 50 | /** | ||
| 51 | * Reset the widget state | ||
| 52 | */ | ||
| 53 | const resetWidget = () => { | ||
| 54 | setState(undefined); | ||
| 55 | setLoading(false); | ||
| 56 | setGraph([]); | ||
| 57 | }; | ||
| 58 | |||
| 59 | /** | ||
| 60 | * Fetch CPU usage data | ||
| 61 | */ | ||
| 62 | const getCpu = React.useCallback(async () => { | ||
| 63 | if (!visible) return; | ||
| 64 | try { | ||
| 65 | const usage = await Utils.cachedRun( | ||
| 66 | `top -l 2 | awk '/CPU usage/ && NR > 10 {gsub(/%/, "", $7); print int(100 - $7); exit}'`, | ||
| 67 | refresh, | ||
| 68 | ); | ||
| 69 | const formattedUsage = { usage: parseInt(usage, 10) }; | ||
| 70 | setState(formattedUsage); | ||
| 71 | if (displayAsGraph) { | ||
| 72 | Utils.addToGraphHistory(formattedUsage, setGraph, GRAPH_LENGTH); | ||
| 73 | } | ||
| 74 | setLoading(false); | ||
| 75 | } catch { | ||
| 76 | setTimeout(getCpu, 1000); | ||
| 77 | } | ||
| 78 | }, [displayAsGraph, setGraph, visible, refresh]); | ||
| 79 | |||
| 80 | // Use server socket to fetch CPU data | ||
| 81 | useServerSocket("cpu", visible, getCpu, resetWidget, setLoading); | ||
| 82 | // Refresh the widget at the specified interval | ||
| 83 | useWidgetRefresh(visible, getCpu, refresh); | ||
| 84 | |||
| 85 | if (loading) return <DataWidgetLoader.Widget className="cpu" />; | ||
| 86 | if (!state) return null; | ||
| 87 | |||
| 88 | const { usage } = state; | ||
| 89 | const threshold = Number(cpuUsageThreshold) || 0; | ||
| 90 | const usageValue = Number(usage) || 0; | ||
| 91 | |||
| 92 | if (threshold > 0 && usageValue < threshold) return null; | ||
| 93 | |||
| 94 | // Handle click event to open CPU monitor app | ||
| 95 | const onClick = | ||
| 96 | cpuMonitorApp === "None" | ||
| 97 | ? undefined | ||
| 98 | : (e) => { | ||
| 99 | Utils.clickEffect(e); | ||
| 100 | openCpuUsageApp(cpuMonitorApp); | ||
| 101 | }; | ||
| 102 | |||
| 103 | if (displayAsGraph) { | ||
| 104 | return ( | ||
| 105 | <DataWidget.Widget | ||
| 106 | classes="cpu cpu--graph" | ||
| 107 | onClick={onClick} | ||
| 108 | disableSlider | ||
| 109 | > | ||
| 110 | <Graph | ||
| 111 | className="cpu__graph" | ||
| 112 | caption={{ | ||
| 113 | usage: { | ||
| 114 | value: `${usage}%`, | ||
| 115 | icon: showIcon ? Icons.CPU : null, | ||
| 116 | color: "var(--yellow)", | ||
| 117 | }, | ||
| 118 | }} | ||
| 119 | values={graph} | ||
| 120 | maxLength={GRAPH_LENGTH} | ||
| 121 | maxValue={100} | ||
| 122 | /> | ||
| 123 | </DataWidget.Widget> | ||
| 124 | ); | ||
| 125 | } | ||
| 126 | |||
| 127 | return ( | ||
| 128 | <DataWidget.Widget | ||
| 129 | classes="cpu" | ||
| 130 | Icon={showIcon ? Icons.CPU : null} | ||
| 131 | onClick={onClick} | ||
| 132 | > | ||
| 133 | <span className="cpu__usage">{usage}%</span> | ||
| 134 | </DataWidget.Widget> | ||
| 135 | ); | ||
| 136 | }); | ||
| 137 | |||
| 138 | Widget.displayName = "Cpu"; | ||
| 139 | |||
| 140 | /** | ||
| 141 | * Open the specified CPU usage monitoring application | ||
| 142 | * @param {string} app - The name of the application to open | ||
| 143 | */ | ||
| 144 | function openCpuUsageApp(app) { | ||
| 145 | switch (app) { | ||
| 146 | case "Activity Monitor": | ||
| 147 | Uebersicht.run(`open -a "Activity Monitor"`); | ||
| 148 | break; | ||
| 149 | case "Top": | ||
| 150 | Utils.runInUserTerminal("top"); | ||
| 151 | break; | ||
| 152 | } | ||
| 153 | } | ||
diff --git a/users/ryan/modules/simple-bar/simple-bar-source/lib/components/data/crypto.jsx b/users/ryan/modules/simple-bar/simple-bar-source/lib/components/data/crypto.jsx new file mode 100755 index 0000000..3faf4ea --- /dev/null +++ b/users/ryan/modules/simple-bar/simple-bar-source/lib/components/data/crypto.jsx | |||
| @@ -0,0 +1,160 @@ | |||
| 1 | import * as Uebersicht from "uebersicht"; | ||
| 2 | import * as DataWidget from "./data-widget.jsx"; | ||
| 3 | import * as DataWidgetLoader from "./data-widget-loader.jsx"; | ||
| 4 | import * as Icons from "../icons/icons.jsx"; | ||
| 5 | import useWidgetRefresh from "../../hooks/use-widget-refresh"; | ||
| 6 | import useServerSocket from "../../hooks/use-server-socket"; | ||
| 7 | import { useSimpleBarContext } from "../simple-bar-context.jsx"; | ||
| 8 | import * as Utils from "../../utils"; | ||
| 9 | |||
| 10 | export { cryptoStyles as styles } from "../../styles/components/data/crypto"; | ||
| 11 | |||
| 12 | const { React } = Uebersicht; | ||
| 13 | |||
| 14 | const DEFAULT_REFRESH_FREQUENCY = 5 * 60 * 1000; // Default refresh frequency set to 5 minutes | ||
| 15 | |||
| 16 | /** | ||
| 17 | * Crypto Widget Component | ||
| 18 | * @returns {JSX.Element|null} The Crypto Widget component | ||
| 19 | */ | ||
| 20 | export const Widget = React.memo(() => { | ||
| 21 | const { displayIndex, settings, pushMissive } = useSimpleBarContext(); | ||
| 22 | const { widgets, cryptoWidgetOptions } = settings; | ||
| 23 | const { cryptoWidget } = widgets; | ||
| 24 | const { | ||
| 25 | refreshFrequency, | ||
| 26 | denomination, | ||
| 27 | identifiers, | ||
| 28 | precision, | ||
| 29 | showOnDisplay, | ||
| 30 | showIcon, | ||
| 31 | } = cryptoWidgetOptions; | ||
| 32 | |||
| 33 | // Calculate the refresh frequency using the provided or default value | ||
| 34 | const refresh = React.useMemo( | ||
| 35 | () => | ||
| 36 | Utils.getRefreshFrequency(refreshFrequency, DEFAULT_REFRESH_FREQUENCY), | ||
| 37 | [refreshFrequency], | ||
| 38 | ); | ||
| 39 | |||
| 40 | // Determine if the widget should be visible on the current display | ||
| 41 | const visible = | ||
| 42 | Utils.isVisibleOnDisplay(displayIndex, showOnDisplay) && cryptoWidget; | ||
| 43 | |||
| 44 | const ref = React.useRef(); | ||
| 45 | const denominatorToken = getDenominatorToken(denomination); | ||
| 46 | |||
| 47 | // Memoize cleanedUpIdentifiers to prevent recreation on every render | ||
| 48 | const cleanedUpIdentifiers = React.useMemo( | ||
| 49 | () => identifiers.replace(/ /g, ""), | ||
| 50 | [identifiers], | ||
| 51 | ); | ||
| 52 | |||
| 53 | // Memoize enumeratedIdentifiers to prevent recreation on every render | ||
| 54 | const enumeratedIdentifiers = React.useMemo( | ||
| 55 | () => cleanedUpIdentifiers.replace(/ /g, "").split(","), | ||
| 56 | [cleanedUpIdentifiers], | ||
| 57 | ); | ||
| 58 | |||
| 59 | const [state, setState] = React.useState(); | ||
| 60 | const [loading, setLoading] = React.useState(visible); | ||
| 61 | |||
| 62 | // Reset the widget state | ||
| 63 | const resetWidget = () => { | ||
| 64 | setState(undefined); | ||
| 65 | setLoading(false); | ||
| 66 | }; | ||
| 67 | |||
| 68 | /** | ||
| 69 | * Fetches cryptocurrency prices from the CoinGecko API | ||
| 70 | */ | ||
| 71 | const getCrypto = React.useCallback(async () => { | ||
| 72 | if (!visible) return; | ||
| 73 | const response = await fetch( | ||
| 74 | `https://api.coingecko.com/api/v3/simple/price?ids=${cleanedUpIdentifiers}&vs_currencies=${denomination}`, | ||
| 75 | ); | ||
| 76 | const result = await response.json(); | ||
| 77 | |||
| 78 | const prices = enumeratedIdentifiers.map((id) => { | ||
| 79 | const value = parseFloat(result[id][denomination]).toFixed(precision); | ||
| 80 | return `${denominatorToken}${value}`; | ||
| 81 | }); | ||
| 82 | setState(prices); | ||
| 83 | setLoading(false); | ||
| 84 | }, [ | ||
| 85 | visible, | ||
| 86 | cleanedUpIdentifiers, | ||
| 87 | denomination, | ||
| 88 | enumeratedIdentifiers, | ||
| 89 | precision, | ||
| 90 | denominatorToken, | ||
| 91 | ]); | ||
| 92 | |||
| 93 | useServerSocket("crypto", visible, getCrypto, resetWidget, setLoading); | ||
| 94 | useWidgetRefresh(visible, getCrypto, refresh); | ||
| 95 | |||
| 96 | /** | ||
| 97 | * Refreshes the cryptocurrency prices | ||
| 98 | * @param {Event} e - The event object | ||
| 99 | */ | ||
| 100 | const refreshCrypto = (e) => { | ||
| 101 | Utils.clickEffect(e); | ||
| 102 | setLoading(true); | ||
| 103 | getCrypto(); | ||
| 104 | Utils.notification("Refreshing price from coingecko.com...", pushMissive); | ||
| 105 | }; | ||
| 106 | |||
| 107 | if (loading) return <DataWidgetLoader.Widget className="crypto" />; | ||
| 108 | if (!state) return null; | ||
| 109 | |||
| 110 | const classes = Utils.classNames("crypto"); | ||
| 111 | |||
| 112 | return enumeratedIdentifiers.map((id, i) => ( | ||
| 113 | <DataWidget.Widget | ||
| 114 | key={id} | ||
| 115 | classes={classes} | ||
| 116 | ref={ref} | ||
| 117 | Icon={showIcon ? getIcon(id) : null} | ||
| 118 | href={`https://coingecko.com/en/coins/${id}`} | ||
| 119 | onClick={(e) => openCrypto(e, pushMissive)} | ||
| 120 | onRightClick={refreshCrypto} | ||
| 121 | > | ||
| 122 | {state[i]} | ||
| 123 | </DataWidget.Widget> | ||
| 124 | )); | ||
| 125 | }); | ||
| 126 | |||
| 127 | Widget.displayName = "Crypto"; | ||
| 128 | |||
| 129 | /** | ||
| 130 | * Returns the icon component for a given cryptocurrency identifier | ||
| 131 | * @param {string} identifier - The cryptocurrency identifier | ||
| 132 | * @returns {JSX.Element} The icon component | ||
| 133 | */ | ||
| 134 | function getIcon(identifier) { | ||
| 135 | if (identifier === "celo") return Icons.Celo; | ||
| 136 | if (identifier === "ethereum") return Icons.Ethereum; | ||
| 137 | if (identifier === "bitcoin") return Icons.Bitcoin; | ||
| 138 | return Icons.Moon; | ||
| 139 | } | ||
| 140 | |||
| 141 | /** | ||
| 142 | * Returns the symbol for a given denomination | ||
| 143 | * @param {string} denomination - The denomination (e.g., "usd", "eur") | ||
| 144 | * @returns {string} The symbol for the denomination | ||
| 145 | */ | ||
| 146 | function getDenominatorToken(denomination) { | ||
| 147 | if (denomination === "usd") return "$"; | ||
| 148 | if (denomination === "eur") return "€"; | ||
| 149 | return ""; | ||
| 150 | } | ||
| 151 | |||
| 152 | /** | ||
| 153 | * Opens the cryptocurrency price chart | ||
| 154 | * @param {Event} e - The event object | ||
| 155 | * @param {Function} pushMissive - The function to push a notification | ||
| 156 | */ | ||
| 157 | function openCrypto(e, pushMissive) { | ||
| 158 | Utils.clickEffect(e); | ||
| 159 | Utils.notification("Opening price chart from coingecko.com...", pushMissive); | ||
| 160 | } | ||
diff --git a/users/ryan/modules/simple-bar/simple-bar-source/lib/components/data/data-widget-loader.jsx b/users/ryan/modules/simple-bar/simple-bar-source/lib/components/data/data-widget-loader.jsx new file mode 100755 index 0000000..4115024 --- /dev/null +++ b/users/ryan/modules/simple-bar/simple-bar-source/lib/components/data/data-widget-loader.jsx | |||
| @@ -0,0 +1,37 @@ | |||
| 1 | import * as Uebersicht from "uebersicht"; | ||
| 2 | import * as Utils from "../../utils"; | ||
| 3 | |||
| 4 | export { dataWidgetLoaderStyles as styles } from "../../styles/components/data/data-widget-loader"; | ||
| 5 | |||
| 6 | const { React } = Uebersicht; | ||
| 7 | |||
| 8 | /** | ||
| 9 | * A memoized React component that renders a data widget loader. | ||
| 10 | * | ||
| 11 | * @param {Object} props - The properties object. | ||
| 12 | * @param {number} [props.width=14] - The width of the loader. | ||
| 13 | * @param {number} [props.height=14] - The height of the loader. | ||
| 14 | * @param {string} [props.className] - Additional class names for the loader. | ||
| 15 | * @param {Object} [props.style] - Additional styles for the loader. | ||
| 16 | * @returns {JSX.Element} The rendered data widget loader component. | ||
| 17 | */ | ||
| 18 | export const Widget = React.memo( | ||
| 19 | ({ width = 14, height = 14, className, style }) => { | ||
| 20 | // Generate class names using the Utils.classNames function | ||
| 21 | const classes = Utils.classNames("data-widget-loader", "data-widget", { | ||
| 22 | [className]: className, | ||
| 23 | }); | ||
| 24 | |||
| 25 | // Return the JSX for the data widget loader | ||
| 26 | return ( | ||
| 27 | <div className={classes} style={style}> | ||
| 28 | <div | ||
| 29 | className="data-widget-loader__inner" | ||
| 30 | style={{ width, height, flex: `0 0 ${width || height}px` }} | ||
| 31 | /> | ||
| 32 | </div> | ||
| 33 | ); | ||
| 34 | }, | ||
| 35 | ); | ||
| 36 | |||
| 37 | Widget.displayName = "DataWidgetLoader"; | ||
diff --git a/users/ryan/modules/simple-bar/simple-bar-source/lib/components/data/data-widget.jsx b/users/ryan/modules/simple-bar/simple-bar-source/lib/components/data/data-widget.jsx new file mode 100755 index 0000000..f5555a1 --- /dev/null +++ b/users/ryan/modules/simple-bar/simple-bar-source/lib/components/data/data-widget.jsx | |||
| @@ -0,0 +1,136 @@ | |||
| 1 | import * as Uebersicht from "uebersicht"; | ||
| 2 | import * as Specter from "./specter.jsx"; | ||
| 3 | import * as Utils from "../../utils"; | ||
| 4 | import { SuspenseIcon } from "../icons/icon.jsx"; | ||
| 5 | export { dataWidgetStyles as styles } from "../../styles/components/data/data-widget"; | ||
| 6 | |||
| 7 | const { React } = Uebersicht; | ||
| 8 | |||
| 9 | /** | ||
| 10 | * Widget component that renders a clickable data widget with optional icon and specter. | ||
| 11 | * @param {Object} props - The properties object. | ||
| 12 | * @param {React.Component} props.Icon - The icon component to display. | ||
| 13 | * @param {string} props.classes - Additional classes for the widget. | ||
| 14 | * @param {string} props.href - The URL to link to. | ||
| 15 | * @param {function} props.onClick - The click event handler. | ||
| 16 | * @param {function} props.onRightClick - The right-click event handler. | ||
| 17 | * @param {function} props.onMiddleClick - The middle-click event handler. | ||
| 18 | * @param {Object} props.style - The style object. | ||
| 19 | * @param {boolean} props.disableSlider - Flag to disable the slider effect. | ||
| 20 | * @param {boolean} props.showSpecter - Flag to show the specter widget. | ||
| 21 | * @param {boolean} props.useDivForClick - Render a div for click handling instead of a button. | ||
| 22 | * @param {React.ReactNode} props.children - The child elements to render inside the widget. | ||
| 23 | * @returns {React.ReactElement} The rendered widget component. | ||
| 24 | */ | ||
| 25 | export function Widget({ | ||
| 26 | Icon, | ||
| 27 | classes, | ||
| 28 | href, | ||
| 29 | onClick, | ||
| 30 | onRightClick, | ||
| 31 | onMiddleClick, | ||
| 32 | style, | ||
| 33 | disableSlider, | ||
| 34 | showSpecter, | ||
| 35 | useDivForClick, | ||
| 36 | children, | ||
| 37 | }) { | ||
| 38 | const ref = React.useRef(); | ||
| 39 | |||
| 40 | let Tag = "div"; | ||
| 41 | if (href) Tag = "a"; | ||
| 42 | if (onClick && !href && !useDivForClick) Tag = "button"; | ||
| 43 | |||
| 44 | const renderDivButton = Tag === "div" && Boolean(onClick); | ||
| 45 | |||
| 46 | const dataWidgetClasses = Utils.classNames("data-widget", classes, { | ||
| 47 | "data-widget--clickable": href || onClick, | ||
| 48 | }); | ||
| 49 | |||
| 50 | /** | ||
| 51 | * Handles the click event, determining the appropriate action based on the event. | ||
| 52 | * @param {MouseEvent} e - The mouse event. | ||
| 53 | */ | ||
| 54 | const onClickProp = (e) => { | ||
| 55 | const { metaKey } = e; | ||
| 56 | const action = metaKey || isMiddleClick(e) ? onMiddleClick : onClick; | ||
| 57 | if (action) action(e); | ||
| 58 | }; | ||
| 59 | |||
| 60 | /** | ||
| 61 | * Handles the mouse enter event to start the sliding effect. | ||
| 62 | */ | ||
| 63 | const onMouseEnter = () => { | ||
| 64 | Utils.startSliding( | ||
| 65 | ref.current, | ||
| 66 | ".data-widget__inner", | ||
| 67 | ".data-widget__slider", | ||
| 68 | ); | ||
| 69 | }; | ||
| 70 | |||
| 71 | /** | ||
| 72 | * Handles the mouse leave event to stop the sliding effect. | ||
| 73 | */ | ||
| 74 | const onMouseLeave = () => { | ||
| 75 | Utils.stopSliding(ref.current, ".data-widget__slider"); | ||
| 76 | }; | ||
| 77 | |||
| 78 | /** | ||
| 79 | * Handles keyboard interaction for div-based clickable widgets. | ||
| 80 | * @param {KeyboardEvent} e - The keyboard event. | ||
| 81 | */ | ||
| 82 | const onKeyDown = (e) => { | ||
| 83 | if (e.key !== "Enter" && e.key !== " ") return; | ||
| 84 | e.preventDefault(); | ||
| 85 | onClickProp(e); | ||
| 86 | }; | ||
| 87 | |||
| 88 | return ( | ||
| 89 | <Tag | ||
| 90 | ref={ref} | ||
| 91 | className={dataWidgetClasses} | ||
| 92 | href={href} | ||
| 93 | onClick={onClickProp} | ||
| 94 | role={renderDivButton ? "button" : undefined} | ||
| 95 | tabIndex={renderDivButton ? 0 : undefined} | ||
| 96 | onKeyDown={renderDivButton ? onKeyDown : undefined} | ||
| 97 | onContextMenu={onRightClick || undefined} | ||
| 98 | onMouseEnter={!disableSlider ? onMouseEnter : undefined} | ||
| 99 | onMouseLeave={!disableSlider ? onMouseLeave : undefined} | ||
| 100 | style={style} | ||
| 101 | > | ||
| 102 | {Icon && ( | ||
| 103 | <SuspenseIcon> | ||
| 104 | <Icon /> | ||
| 105 | </SuspenseIcon> | ||
| 106 | )} | ||
| 107 | {showSpecter && <Specter.Widget />} | ||
| 108 | <Inner disableSlider={disableSlider}>{children}</Inner> | ||
| 109 | </Tag> | ||
| 110 | ); | ||
| 111 | } | ||
| 112 | |||
| 113 | /** | ||
| 114 | * Inner component that optionally wraps children with sliding effect elements. | ||
| 115 | * @param {Object} props - The properties object. | ||
| 116 | * @param {boolean} props.disableSlider - Flag to disable the slider effect. | ||
| 117 | * @param {React.ReactNode} props.children - The child elements to render inside the inner component. | ||
| 118 | * @returns {React.ReactElement} The rendered inner component. | ||
| 119 | */ | ||
| 120 | function Inner({ disableSlider, children }) { | ||
| 121 | if (disableSlider) return children; | ||
| 122 | return ( | ||
| 123 | <div className="data-widget__inner"> | ||
| 124 | <div className="data-widget__slider">{children}</div> | ||
| 125 | </div> | ||
| 126 | ); | ||
| 127 | } | ||
| 128 | |||
| 129 | /** | ||
| 130 | * Checks if the event is a middle-click. | ||
| 131 | * @param {MouseEvent} e - The mouse event. | ||
| 132 | * @returns {boolean} True if the event is a middle-click, false otherwise. | ||
| 133 | */ | ||
| 134 | function isMiddleClick(e) { | ||
| 135 | return e.button === 1 || e["button&2"] === 1; | ||
| 136 | } | ||
diff --git a/users/ryan/modules/simple-bar/simple-bar-source/lib/components/data/date-display.jsx b/users/ryan/modules/simple-bar/simple-bar-source/lib/components/data/date-display.jsx new file mode 100755 index 0000000..af4c058 --- /dev/null +++ b/users/ryan/modules/simple-bar/simple-bar-source/lib/components/data/date-display.jsx | |||
| @@ -0,0 +1,118 @@ | |||
| 1 | import * as Uebersicht from "uebersicht"; | ||
| 2 | import * as DataWidget from "./data-widget.jsx"; | ||
| 3 | import * as DataWidgetLoader from "./data-widget-loader.jsx"; | ||
| 4 | import * as Icons from "../icons/icons.jsx"; | ||
| 5 | import * as Utils from "../../utils"; | ||
| 6 | import useWidgetRefresh from "../../hooks/use-widget-refresh"; | ||
| 7 | import useServerSocket from "../../hooks/use-server-socket"; | ||
| 8 | import { useSimpleBarContext } from "../simple-bar-context.jsx"; | ||
| 9 | |||
| 10 | export { dateStyles as styles } from "../../styles/components/data/date-display"; | ||
| 11 | |||
| 12 | const { React } = Uebersicht; | ||
| 13 | |||
| 14 | const DEFAULT_REFRESH_FREQUENCY = 30000; | ||
| 15 | |||
| 16 | /** | ||
| 17 | * Date display widget component. | ||
| 18 | * @returns {JSX.Element} The date display widget. | ||
| 19 | */ | ||
| 20 | export const Widget = React.memo(() => { | ||
| 21 | const { displayIndex, settings } = useSimpleBarContext(); | ||
| 22 | const { widgets, dateWidgetOptions } = settings; | ||
| 23 | const { dateWidget } = widgets; | ||
| 24 | const { | ||
| 25 | refreshFrequency, | ||
| 26 | shortDateFormat, | ||
| 27 | locale, | ||
| 28 | calendarApp, | ||
| 29 | showOnDisplay, | ||
| 30 | showIcon, | ||
| 31 | } = dateWidgetOptions; | ||
| 32 | |||
| 33 | // Determine if the widget should be visible based on display settings | ||
| 34 | const visible = | ||
| 35 | Utils.isVisibleOnDisplay(displayIndex, showOnDisplay) && dateWidget; | ||
| 36 | |||
| 37 | // Calculate the refresh frequency for the widget | ||
| 38 | const refresh = React.useMemo( | ||
| 39 | () => | ||
| 40 | Utils.getRefreshFrequency(refreshFrequency, DEFAULT_REFRESH_FREQUENCY), | ||
| 41 | [refreshFrequency], | ||
| 42 | ); | ||
| 43 | |||
| 44 | const [state, setState] = React.useState(); | ||
| 45 | const [loading, setLoading] = React.useState(visible); | ||
| 46 | |||
| 47 | const formatOptions = shortDateFormat ? "short" : "long"; | ||
| 48 | |||
| 49 | // Memoize the date format options | ||
| 50 | const options = React.useMemo( | ||
| 51 | () => ({ | ||
| 52 | weekday: formatOptions, | ||
| 53 | month: formatOptions, | ||
| 54 | day: "numeric", | ||
| 55 | }), | ||
| 56 | [formatOptions], | ||
| 57 | ); | ||
| 58 | |||
| 59 | // Ensure locale is valid, default to "en-UK" if not | ||
| 60 | const _locale = locale.length > 4 ? locale : "en-UK"; | ||
| 61 | |||
| 62 | /** | ||
| 63 | * Reset the widget state. | ||
| 64 | */ | ||
| 65 | const resetWidget = () => { | ||
| 66 | setState(undefined); | ||
| 67 | setLoading(false); | ||
| 68 | }; | ||
| 69 | |||
| 70 | /** | ||
| 71 | * Get the current date and update the state. | ||
| 72 | */ | ||
| 73 | const getDate = React.useCallback(() => { | ||
| 74 | if (!visible) return; | ||
| 75 | const now = new Date().toLocaleDateString(_locale, options); | ||
| 76 | setState({ now }); | ||
| 77 | setLoading(false); | ||
| 78 | }, [_locale, options, visible]); | ||
| 79 | |||
| 80 | // Use server socket to get date updates | ||
| 81 | useServerSocket("date-display", visible, getDate, resetWidget, setLoading); | ||
| 82 | // Refresh the widget at the specified interval | ||
| 83 | useWidgetRefresh(visible, getDate, refresh); | ||
| 84 | |||
| 85 | if (loading) return <DataWidgetLoader.Widget className="date-display" />; | ||
| 86 | if (!state) return null; | ||
| 87 | const { now } = state; | ||
| 88 | |||
| 89 | /** | ||
| 90 | * Handle click event to open the calendar application. | ||
| 91 | * @param {Event} e - The click event. | ||
| 92 | */ | ||
| 93 | const onClick = (e) => { | ||
| 94 | Utils.clickEffect(e); | ||
| 95 | openCalendarApp(calendarApp); | ||
| 96 | }; | ||
| 97 | |||
| 98 | return ( | ||
| 99 | <DataWidget.Widget | ||
| 100 | classes="date-display" | ||
| 101 | Icon={showIcon ? Icons.Date : null} | ||
| 102 | onClick={onClick} | ||
| 103 | > | ||
| 104 | {now} | ||
| 105 | </DataWidget.Widget> | ||
| 106 | ); | ||
| 107 | }); | ||
| 108 | |||
| 109 | Widget.displayName = "DateDisplay"; | ||
| 110 | |||
| 111 | /** | ||
| 112 | * Open the specified calendar application. | ||
| 113 | * @param {string} calendarApp - The name of the calendar application to open. | ||
| 114 | */ | ||
| 115 | function openCalendarApp(calendarApp) { | ||
| 116 | const appName = calendarApp || "Calendar"; | ||
| 117 | Uebersicht.run(`open -a "${appName}"`); | ||
| 118 | } | ||
diff --git a/users/ryan/modules/simple-bar/simple-bar-source/lib/components/data/github.jsx b/users/ryan/modules/simple-bar/simple-bar-source/lib/components/data/github.jsx new file mode 100755 index 0000000..89308b5 --- /dev/null +++ b/users/ryan/modules/simple-bar/simple-bar-source/lib/components/data/github.jsx | |||
| @@ -0,0 +1,105 @@ | |||
| 1 | import * as Uebersicht from "uebersicht"; | ||
| 2 | import * as DataWidget from "./data-widget.jsx"; | ||
| 3 | import * as DataWidgetLoader from "./data-widget-loader.jsx"; | ||
| 4 | import * as Icons from "../icons/icons.jsx"; | ||
| 5 | import useWidgetRefresh from "../../hooks/use-widget-refresh.js"; | ||
| 6 | import useServerSocket from "../../hooks/use-server-socket.js"; | ||
| 7 | import { useSimpleBarContext } from "../simple-bar-context.jsx"; | ||
| 8 | import * as Utils from "../../utils.js"; | ||
| 9 | |||
| 10 | export { githubStyles as styles } from "../../styles/components/data/github.js"; | ||
| 11 | |||
| 12 | const { React } = Uebersicht; | ||
| 13 | |||
| 14 | const DEFAULT_REFRESH_FREQUENCY = 1000 * 60 * 10; | ||
| 15 | |||
| 16 | /** | ||
| 17 | * GitHub notification widget component | ||
| 18 | * @returns {JSX.Element|null} The GitHub notification widget component | ||
| 19 | */ | ||
| 20 | export const Widget = React.memo(() => { | ||
| 21 | const { displayIndex, settings } = useSimpleBarContext(); | ||
| 22 | const { widgets, githubWidgetOptions } = settings; | ||
| 23 | const { githubWidget } = widgets; | ||
| 24 | const { | ||
| 25 | refreshFrequency, | ||
| 26 | showOnDisplay, | ||
| 27 | hideWhenNoNotification, | ||
| 28 | notificationUrl, | ||
| 29 | ghBinaryPath, | ||
| 30 | showIcon, | ||
| 31 | } = githubWidgetOptions; | ||
| 32 | |||
| 33 | const isDisabled = React.useRef(false); | ||
| 34 | |||
| 35 | // Calculate the refresh frequency for the widget | ||
| 36 | const refresh = React.useMemo( | ||
| 37 | () => | ||
| 38 | Utils.getRefreshFrequency(refreshFrequency, DEFAULT_REFRESH_FREQUENCY), | ||
| 39 | [refreshFrequency], | ||
| 40 | ); | ||
| 41 | |||
| 42 | // Determine if the widget should be visible | ||
| 43 | const visible = | ||
| 44 | Utils.isVisibleOnDisplay(displayIndex, showOnDisplay) && githubWidget; | ||
| 45 | |||
| 46 | const [state, setState] = React.useState(); | ||
| 47 | const [loading, setLoading] = React.useState(visible); | ||
| 48 | |||
| 49 | /** | ||
| 50 | * Reset the widget state | ||
| 51 | */ | ||
| 52 | const resetWidget = () => { | ||
| 53 | setState(undefined); | ||
| 54 | setLoading(false); | ||
| 55 | }; | ||
| 56 | |||
| 57 | /** | ||
| 58 | * Fetch GPU data and update the widget state | ||
| 59 | */ | ||
| 60 | const getGitHub = React.useCallback(async () => { | ||
| 61 | if (!visible) return; | ||
| 62 | setLoading(true); | ||
| 63 | const result = await Utils.cachedRun( | ||
| 64 | `${ghBinaryPath} api notifications`, | ||
| 65 | refresh, | ||
| 66 | ); | ||
| 67 | if (!visible || isDisabled.current) { | ||
| 68 | return setLoading(false); | ||
| 69 | } | ||
| 70 | const json = JSON.parse(result); | ||
| 71 | const count = json.length; | ||
| 72 | setState({ count: count > 99 ? "99+" : count }); | ||
| 73 | setLoading(false); | ||
| 74 | }, [ghBinaryPath, visible, refresh]); | ||
| 75 | |||
| 76 | // Update the disabled state based on visibility | ||
| 77 | React.useEffect(() => { | ||
| 78 | isDisabled.current = !visible; | ||
| 79 | }, [visible]); | ||
| 80 | |||
| 81 | // Use server socket to fetch GPU data | ||
| 82 | useServerSocket("github", visible, getGitHub, resetWidget, setLoading); | ||
| 83 | // Use widget refresh hook to periodically refresh the widget | ||
| 84 | useWidgetRefresh(visible, getGitHub, refresh); | ||
| 85 | |||
| 86 | if (loading) return <DataWidgetLoader.Widget className="github" />; | ||
| 87 | if (!state) return null; | ||
| 88 | |||
| 89 | const { count } = state; | ||
| 90 | |||
| 91 | if (hideWhenNoNotification && count === 0) return null; | ||
| 92 | |||
| 93 | return ( | ||
| 94 | <DataWidget.Widget | ||
| 95 | classes="github" | ||
| 96 | href={notificationUrl} | ||
| 97 | Icon={showIcon ? Icons.GitHub : null} | ||
| 98 | onRightClick={getGitHub} | ||
| 99 | > | ||
| 100 | <span className="github__count">{count}</span> | ||
| 101 | </DataWidget.Widget> | ||
| 102 | ); | ||
| 103 | }); | ||
| 104 | |||
| 105 | Widget.displayName = "GitHub"; | ||
diff --git a/users/ryan/modules/simple-bar/simple-bar-source/lib/components/data/gpu.jsx b/users/ryan/modules/simple-bar/simple-bar-source/lib/components/data/gpu.jsx new file mode 100755 index 0000000..6ac1dab --- /dev/null +++ b/users/ryan/modules/simple-bar/simple-bar-source/lib/components/data/gpu.jsx | |||
| @@ -0,0 +1,128 @@ | |||
| 1 | import * as Uebersicht from "uebersicht"; | ||
| 2 | import * as DataWidget from "./data-widget.jsx"; | ||
| 3 | import * as DataWidgetLoader from "./data-widget-loader.jsx"; | ||
| 4 | import Graph from "./graph.jsx"; | ||
| 5 | import * as Icons from "../icons/icons.jsx"; | ||
| 6 | import useWidgetRefresh from "../../hooks/use-widget-refresh.js"; | ||
| 7 | import useServerSocket from "../../hooks/use-server-socket.js"; | ||
| 8 | import { useSimpleBarContext } from "../simple-bar-context.jsx"; | ||
| 9 | import * as Utils from "../../utils.js"; | ||
| 10 | |||
| 11 | export { gpuStyles as styles } from "../../styles/components/data/gpu.js"; | ||
| 12 | |||
| 13 | const { React } = Uebersicht; | ||
| 14 | |||
| 15 | const DEFAULT_REFRESH_FREQUENCY = 2000; | ||
| 16 | const GRAPH_LENGTH = 40; | ||
| 17 | |||
| 18 | /** | ||
| 19 | * GPU Widget component | ||
| 20 | * @returns {JSX.Element|null} The GPU widget component | ||
| 21 | */ | ||
| 22 | export const Widget = React.memo(() => { | ||
| 23 | const { displayIndex, settings } = useSimpleBarContext(); | ||
| 24 | const { widgets, gpuWidgetOptions } = settings; | ||
| 25 | const { gpuWidget } = widgets; | ||
| 26 | const { | ||
| 27 | refreshFrequency, | ||
| 28 | showOnDisplay, | ||
| 29 | displayAsGraph, | ||
| 30 | gpuMacmonBinaryPath, | ||
| 31 | showIcon, | ||
| 32 | } = gpuWidgetOptions; | ||
| 33 | |||
| 34 | const isDisabled = React.useRef(false); | ||
| 35 | |||
| 36 | // Calculate the refresh frequency for the widget | ||
| 37 | const refresh = React.useMemo( | ||
| 38 | () => | ||
| 39 | Utils.getRefreshFrequency(refreshFrequency, DEFAULT_REFRESH_FREQUENCY), | ||
| 40 | [refreshFrequency], | ||
| 41 | ); | ||
| 42 | |||
| 43 | // Determine if the widget should be visible | ||
| 44 | const visible = | ||
| 45 | Utils.isVisibleOnDisplay(displayIndex, showOnDisplay) && gpuWidget; | ||
| 46 | |||
| 47 | const [graph, setGraph] = React.useState([]); | ||
| 48 | const [state, setState] = React.useState(); | ||
| 49 | const [loading, setLoading] = React.useState(visible); | ||
| 50 | |||
| 51 | /** | ||
| 52 | * Reset the widget state | ||
| 53 | */ | ||
| 54 | const resetWidget = () => { | ||
| 55 | setState(undefined); | ||
| 56 | setLoading(false); | ||
| 57 | setGraph([]); | ||
| 58 | }; | ||
| 59 | |||
| 60 | /** | ||
| 61 | * Fetch GPU data and update the widget state | ||
| 62 | */ | ||
| 63 | const getGpu = React.useCallback(async () => { | ||
| 64 | if (!visible) return; | ||
| 65 | try { | ||
| 66 | const result = await Utils.cachedRun( | ||
| 67 | `${gpuMacmonBinaryPath} pipe -s 1`, | ||
| 68 | refresh, | ||
| 69 | ); | ||
| 70 | if (!visible || isDisabled.current) { | ||
| 71 | return; | ||
| 72 | } | ||
| 73 | const json = JSON.parse(result); | ||
| 74 | const { gpu_usage } = json; | ||
| 75 | const formattedUsage = { usage: Math.round(gpu_usage[1] * 100) }; | ||
| 76 | setState(formattedUsage); | ||
| 77 | if (displayAsGraph) { | ||
| 78 | Utils.addToGraphHistory(formattedUsage, setGraph, GRAPH_LENGTH); | ||
| 79 | } | ||
| 80 | setLoading(false); | ||
| 81 | } catch { | ||
| 82 | setTimeout(getGpu, 1000); | ||
| 83 | } | ||
| 84 | }, [displayAsGraph, gpuMacmonBinaryPath, visible, refresh]); | ||
| 85 | |||
| 86 | // Update the disabled state based on visibility | ||
| 87 | React.useEffect(() => { | ||
| 88 | isDisabled.current = !visible; | ||
| 89 | }, [visible]); | ||
| 90 | |||
| 91 | // Use server socket to fetch GPU data | ||
| 92 | useServerSocket("gpu", visible, getGpu, resetWidget, setLoading); | ||
| 93 | // Use widget refresh hook to periodically refresh the widget | ||
| 94 | useWidgetRefresh(visible, getGpu, refresh); | ||
| 95 | |||
| 96 | if (loading) return <DataWidgetLoader.Widget className="cpu" />; | ||
| 97 | if (!state) return null; | ||
| 98 | |||
| 99 | const { usage } = state; | ||
| 100 | |||
| 101 | if (displayAsGraph) { | ||
| 102 | return ( | ||
| 103 | <DataWidget.Widget classes="gpu gpu--graph" disableSlider> | ||
| 104 | <Graph | ||
| 105 | className="gpu__graph" | ||
| 106 | caption={{ | ||
| 107 | usage: { | ||
| 108 | value: `${usage}%`, | ||
| 109 | icon: showIcon ? Icons.CPU : null, | ||
| 110 | color: "var(--cyan)", | ||
| 111 | }, | ||
| 112 | }} | ||
| 113 | values={graph} | ||
| 114 | maxLength={GRAPH_LENGTH} | ||
| 115 | maxValue={100} | ||
| 116 | /> | ||
| 117 | </DataWidget.Widget> | ||
| 118 | ); | ||
| 119 | } | ||
| 120 | |||
| 121 | return ( | ||
| 122 | <DataWidget.Widget classes="cpu" Icon={showIcon ? Icons.CPU : null}> | ||
| 123 | <span className="cpu__usage">{usage}%</span> | ||
| 124 | </DataWidget.Widget> | ||
| 125 | ); | ||
| 126 | }); | ||
| 127 | |||
| 128 | Widget.displayName = "Gpu"; | ||
diff --git a/users/ryan/modules/simple-bar/simple-bar-source/lib/components/data/graph.jsx b/users/ryan/modules/simple-bar/simple-bar-source/lib/components/data/graph.jsx new file mode 100755 index 0000000..008e68e --- /dev/null +++ b/users/ryan/modules/simple-bar/simple-bar-source/lib/components/data/graph.jsx | |||
| @@ -0,0 +1,87 @@ | |||
| 1 | import * as Uebersicht from "uebersicht"; | ||
| 2 | import * as Utils from "../../utils.js"; | ||
| 3 | import { SuspenseIcon } from "../icons/icon.jsx"; | ||
| 4 | |||
| 5 | const { React } = Uebersicht; | ||
| 6 | |||
| 7 | export { graphStyles as styles } from "../../styles/components/data/graph"; | ||
| 8 | |||
| 9 | /** | ||
| 10 | * Graph component to display a bar graph with captions and values. | ||
| 11 | * | ||
| 12 | * @param {Object} props - The properties object. | ||
| 13 | * @param {string} props.className - Additional class names for the graph container. | ||
| 14 | * @param {Object} props.caption - Caption data containing value, icon, and color for each key. | ||
| 15 | * @param {Array} props.values - Array of value objects to be displayed as bars. | ||
| 16 | * @param {number} props.maxLength - Maximum length of the graph. | ||
| 17 | * @param {number} [props.maxValue] - Maximum value for scaling the bars. | ||
| 18 | * @returns {JSX.Element|null} The rendered graph component or null if no caption is provided. | ||
| 19 | */ | ||
| 20 | export default function Graph({ | ||
| 21 | className, | ||
| 22 | caption, | ||
| 23 | values = [], | ||
| 24 | maxLength, | ||
| 25 | maxValue, | ||
| 26 | }) { | ||
| 27 | // Return null if no caption is provided | ||
| 28 | if (!caption) { | ||
| 29 | return null; | ||
| 30 | } | ||
| 31 | |||
| 32 | const captionKeys = Object.keys(caption); | ||
| 33 | |||
| 34 | // Calculate maxValue if not provided | ||
| 35 | if (maxValue === undefined) { | ||
| 36 | const allValues = values.map((value) => Object.values(value)).flat(); | ||
| 37 | maxValue = Math.max(...allValues); | ||
| 38 | } | ||
| 39 | |||
| 40 | const classes = Utils.classNames("graph", className); | ||
| 41 | |||
| 42 | return ( | ||
| 43 | <div className={classes}> | ||
| 44 | <div className="graph__bars"> | ||
| 45 | {values.map((value) => { | ||
| 46 | const keys = Object.keys(value); | ||
| 47 | return keys.map((key) => { | ||
| 48 | const height = (value[key] / maxValue) * 100; | ||
| 49 | const { color: backgroundColor } = caption[key]; | ||
| 50 | return ( | ||
| 51 | <div | ||
| 52 | key={key} | ||
| 53 | className="graph__bar" | ||
| 54 | style={{ | ||
| 55 | flex: `0 0 calc(100% / ${maxLength * captionKeys.length})`, | ||
| 56 | height: `${height}%`, | ||
| 57 | backgroundColor, | ||
| 58 | }} | ||
| 59 | /> | ||
| 60 | ); | ||
| 61 | }); | ||
| 62 | })} | ||
| 63 | </div> | ||
| 64 | <div className="graph__data"> | ||
| 65 | {captionKeys.map((key) => { | ||
| 66 | const { value, icon: Icon, color } = caption[key]; | ||
| 67 | return ( | ||
| 68 | <div key={key} className="graph__data-item"> | ||
| 69 | {Icon && ( | ||
| 70 | <SuspenseIcon> | ||
| 71 | <Icon | ||
| 72 | className="graph__data-item-icon" | ||
| 73 | style={{ fill: color }} | ||
| 74 | /> | ||
| 75 | </SuspenseIcon> | ||
| 76 | )} | ||
| 77 | <span | ||
| 78 | className="graph__data-item-value" | ||
| 79 | dangerouslySetInnerHTML={{ __html: value }} | ||
| 80 | /> | ||
| 81 | </div> | ||
| 82 | ); | ||
| 83 | })} | ||
| 84 | </div> | ||
| 85 | </div> | ||
| 86 | ); | ||
| 87 | } | ||
diff --git a/users/ryan/modules/simple-bar/simple-bar-source/lib/components/data/keyboard.jsx b/users/ryan/modules/simple-bar/simple-bar-source/lib/components/data/keyboard.jsx new file mode 100755 index 0000000..d6e04f2 --- /dev/null +++ b/users/ryan/modules/simple-bar/simple-bar-source/lib/components/data/keyboard.jsx | |||
| @@ -0,0 +1,108 @@ | |||
| 1 | import * as Uebersicht from "uebersicht"; | ||
| 2 | import * as DataWidget from "./data-widget.jsx"; | ||
| 3 | import * as DataWidgetLoader from "./data-widget-loader.jsx"; | ||
| 4 | import * as Icons from "../icons/icons.jsx"; | ||
| 5 | import useWidgetRefresh from "../../hooks/use-widget-refresh"; | ||
| 6 | import useServerSocket from "../../hooks/use-server-socket"; | ||
| 7 | import { useSimpleBarContext } from "../simple-bar-context.jsx"; | ||
| 8 | import * as Utils from "../../utils"; | ||
| 9 | |||
| 10 | export { keyboardStyles as styles } from "../../styles/components/data/keyboard"; | ||
| 11 | |||
| 12 | const { React } = Uebersicht; | ||
| 13 | |||
| 14 | const DEFAULT_REFRESH_FREQUENCY = 20000; | ||
| 15 | |||
| 16 | /** | ||
| 17 | * Keyboard widget component. | ||
| 18 | * @returns {JSX.Element|null} The rendered widget or null if not visible. | ||
| 19 | */ | ||
| 20 | export const Widget = React.memo(() => { | ||
| 21 | const { displayIndex, settings } = useSimpleBarContext(); | ||
| 22 | const { widgets, keyboardWidgetOptions } = settings; | ||
| 23 | const { keyboardWidget } = widgets; | ||
| 24 | const { refreshFrequency, showOnDisplay, showIcon, keyboardMaxLength } = | ||
| 25 | keyboardWidgetOptions; | ||
| 26 | |||
| 27 | // Determine the refresh frequency for the widget | ||
| 28 | const refresh = React.useMemo( | ||
| 29 | () => | ||
| 30 | Utils.getRefreshFrequency(refreshFrequency, DEFAULT_REFRESH_FREQUENCY), | ||
| 31 | [refreshFrequency], | ||
| 32 | ); | ||
| 33 | |||
| 34 | // Determine if the widget should be visible based on display settings | ||
| 35 | const visible = | ||
| 36 | Utils.isVisibleOnDisplay(displayIndex, showOnDisplay) && keyboardWidget; | ||
| 37 | |||
| 38 | const [state, setState] = React.useState(); | ||
| 39 | const [loading, setLoading] = React.useState(visible); | ||
| 40 | |||
| 41 | /** | ||
| 42 | * Resets the widget state. | ||
| 43 | */ | ||
| 44 | const resetWidget = () => { | ||
| 45 | setState(undefined); | ||
| 46 | setLoading(false); | ||
| 47 | }; | ||
| 48 | |||
| 49 | /** | ||
| 50 | * Fetches the current keyboard layout or input mode. | ||
| 51 | */ | ||
| 52 | const getKeyboard = React.useCallback(async () => { | ||
| 53 | if (!visible) return; | ||
| 54 | const keyboard = await Utils.cachedRun( | ||
| 55 | `defaults read ~/Library/Preferences/com.apple.HIToolbox.plist AppleSelectedInputSources | awk '/KeyboardLayout Name/ {$1=$2=$3=""; print $0}'`, | ||
| 56 | refresh, | ||
| 57 | ); | ||
| 58 | const layout = Utils.cleanupOutput(keyboard) | ||
| 59 | .replace(";", "") | ||
| 60 | .replaceAll('"', ""); | ||
| 61 | if (layout.length) { | ||
| 62 | setState({ keyboard: layout }); | ||
| 63 | setLoading(false); | ||
| 64 | return; | ||
| 65 | } | ||
| 66 | |||
| 67 | const inputMode = await Utils.cachedRun( | ||
| 68 | `defaults read ~/Library/Preferences/com.apple.HIToolbox.plist AppleSelectedInputSources | awk '/"Input Mode" =/ {$1=$2=$3=""; print $0}'`, | ||
| 69 | refresh, | ||
| 70 | ); | ||
| 71 | const cleanedInputMode = Utils.cleanupOutput(inputMode) | ||
| 72 | .replace(/"com.apple.inputmethod.(.*)"/, "$1") | ||
| 73 | .replace(";", ""); | ||
| 74 | |||
| 75 | if (!cleanedInputMode.length) return setLoading(false); | ||
| 76 | |||
| 77 | const splitedInputMode = cleanedInputMode.split("."); | ||
| 78 | const inputModeName = splitedInputMode[splitedInputMode.length - 1]; | ||
| 79 | setState({ keyboard: inputModeName }); | ||
| 80 | setLoading(false); | ||
| 81 | }, [visible, refresh]); | ||
| 82 | |||
| 83 | // Use server socket to listen for keyboard events | ||
| 84 | useServerSocket("keyboard", visible, getKeyboard, resetWidget, setLoading); | ||
| 85 | // Refresh the widget at the specified interval | ||
| 86 | useWidgetRefresh(visible, getKeyboard, refresh); | ||
| 87 | |||
| 88 | if (loading) return <DataWidgetLoader.Widget className="keyboard" />; | ||
| 89 | if (!state) return null; | ||
| 90 | const { keyboard } = state; | ||
| 91 | |||
| 92 | const maxLength = Number(keyboardMaxLength) || 0; | ||
| 93 | const displayKeyboard = | ||
| 94 | maxLength > 0 ? keyboard.slice(0, maxLength) : keyboard; | ||
| 95 | |||
| 96 | if (!displayKeyboard?.length) return null; | ||
| 97 | |||
| 98 | return ( | ||
| 99 | <DataWidget.Widget | ||
| 100 | classes="keyboard" | ||
| 101 | Icon={showIcon ? Icons.Keyboard : null} | ||
| 102 | > | ||
| 103 | {displayKeyboard} | ||
| 104 | </DataWidget.Widget> | ||
| 105 | ); | ||
| 106 | }); | ||
| 107 | |||
| 108 | Widget.displayName = "Keyboard"; | ||
diff --git a/users/ryan/modules/simple-bar/simple-bar-source/lib/components/data/memory.jsx b/users/ryan/modules/simple-bar/simple-bar-source/lib/components/data/memory.jsx new file mode 100755 index 0000000..e4361db --- /dev/null +++ b/users/ryan/modules/simple-bar/simple-bar-source/lib/components/data/memory.jsx | |||
| @@ -0,0 +1,134 @@ | |||
| 1 | import * as Uebersicht from "uebersicht"; | ||
| 2 | import * as DataWidget from "./data-widget.jsx"; | ||
| 3 | import * as DataWidgetLoader from "./data-widget-loader.jsx"; | ||
| 4 | import useWidgetRefresh from "../../hooks/use-widget-refresh"; | ||
| 5 | import useServerSocket from "../../hooks/use-server-socket"; | ||
| 6 | import { useSimpleBarContext } from "../simple-bar-context.jsx"; | ||
| 7 | import * as Utils from "../../utils"; | ||
| 8 | |||
| 9 | export { memoryStyles as styles } from "../../styles/components/data/memory"; | ||
| 10 | |||
| 11 | const { React } = Uebersicht; | ||
| 12 | |||
| 13 | const DEFAULT_REFRESH_FREQUENCY = 4000; | ||
| 14 | |||
| 15 | /** | ||
| 16 | * Memory Widget component | ||
| 17 | * @returns {JSX.Element|null} The memory widget component | ||
| 18 | */ | ||
| 19 | export const Widget = () => { | ||
| 20 | const { displayIndex, settings } = useSimpleBarContext(); | ||
| 21 | const { widgets, memoryWidgetOptions } = settings; | ||
| 22 | const { memoryWidget } = widgets; | ||
| 23 | const { | ||
| 24 | refreshFrequency, | ||
| 25 | showOnDisplay, | ||
| 26 | memoryMonitorApp, | ||
| 27 | showIcon, | ||
| 28 | memoryUsageThreshold, | ||
| 29 | } = memoryWidgetOptions; | ||
| 30 | |||
| 31 | // Determine the refresh frequency for the widget | ||
| 32 | const refresh = React.useMemo( | ||
| 33 | () => | ||
| 34 | Utils.getRefreshFrequency(refreshFrequency, DEFAULT_REFRESH_FREQUENCY), | ||
| 35 | [refreshFrequency], | ||
| 36 | ); | ||
| 37 | |||
| 38 | // Determine if the widget should be visible | ||
| 39 | const visible = | ||
| 40 | Utils.isVisibleOnDisplay(displayIndex, showOnDisplay) && memoryWidget; | ||
| 41 | |||
| 42 | const [state, setState] = Uebersicht.React.useState(); | ||
| 43 | const [loading, setLoading] = Uebersicht.React.useState(visible); | ||
| 44 | |||
| 45 | /** | ||
| 46 | * Reset the widget state | ||
| 47 | */ | ||
| 48 | const resetWidget = () => { | ||
| 49 | setState(undefined); | ||
| 50 | setLoading(false); | ||
| 51 | }; | ||
| 52 | |||
| 53 | /** | ||
| 54 | * Fetch memory usage data | ||
| 55 | */ | ||
| 56 | const getMemory = React.useCallback(async () => { | ||
| 57 | const output = await Utils.cachedRun( | ||
| 58 | 'vm_stat | awk \'BEGIN {page_size=4096} /page size of/ {page_size=$8} /Pages free/ {free=$3} /Pages inactive/ {inactive=$3} /Pages speculative/ {spec=$4} /Pages active/ {active=$3} /Pages wired/ {wired=$4} END {gsub(/\\./, "", free); gsub(/\\./, "", inactive); gsub(/\\./, "", spec); gsub(/\\./, "", active); gsub(/\\./, "", wired); available=free+inactive+spec; total=available+active+wired; printf "%.0f", (available/total)*100}\'', | ||
| 59 | refresh, | ||
| 60 | ); | ||
| 61 | const free = parseInt(Utils.cleanupOutput(output), 10); | ||
| 62 | setState({ free }); | ||
| 63 | setLoading(false); | ||
| 64 | }, [setLoading, setState, refresh]); | ||
| 65 | |||
| 66 | // Use server socket to get memory data | ||
| 67 | useServerSocket("memory", visible, getMemory, resetWidget, setLoading); | ||
| 68 | // Refresh the widget at the specified interval | ||
| 69 | useWidgetRefresh(visible, getMemory, refresh); | ||
| 70 | |||
| 71 | if (loading) return <DataWidgetLoader.Widget className="memory" />; | ||
| 72 | if (!state) return null; | ||
| 73 | |||
| 74 | const { free } = state; | ||
| 75 | const used = 100 - free; | ||
| 76 | const threshold = Number(memoryUsageThreshold) || 0; | ||
| 77 | |||
| 78 | if (threshold > 0 && used < threshold) return null; | ||
| 79 | |||
| 80 | // Handle click event to open memory usage app | ||
| 81 | const onClick = | ||
| 82 | memoryMonitorApp === "None" | ||
| 83 | ? undefined | ||
| 84 | : (e) => { | ||
| 85 | Utils.clickEffect(e); | ||
| 86 | openMemoryUsageApp(memoryMonitorApp); | ||
| 87 | }; | ||
| 88 | |||
| 89 | /** | ||
| 90 | * Pie chart component for memory usage | ||
| 91 | * @returns {JSX.Element} The pie chart component | ||
| 92 | */ | ||
| 93 | const Pie = () => { | ||
| 94 | return ( | ||
| 95 | <div | ||
| 96 | className="memory__pie" | ||
| 97 | style={{ | ||
| 98 | backgroundImage: `conic-gradient(var(--pie-color) ${used}%, var(--main-alt) ${used}% 100%)`, | ||
| 99 | }} | ||
| 100 | /> | ||
| 101 | ); | ||
| 102 | }; | ||
| 103 | |||
| 104 | const classes = Utils.classNames("memory", { | ||
| 105 | "memory--low": used <= 30, | ||
| 106 | "memory--medium": used > 30 && used <= 70, | ||
| 107 | "memory--high": used > 70, | ||
| 108 | }); | ||
| 109 | |||
| 110 | return ( | ||
| 111 | <DataWidget.Widget | ||
| 112 | classes={classes} | ||
| 113 | Icon={showIcon ? Pie : null} | ||
| 114 | onClick={onClick} | ||
| 115 | > | ||
| 116 | <div className="memory__content">{used}%</div> | ||
| 117 | </DataWidget.Widget> | ||
| 118 | ); | ||
| 119 | }; | ||
| 120 | |||
| 121 | /** | ||
| 122 | * Open the specified memory usage application | ||
| 123 | * @param {string} app - The name of the application to open | ||
| 124 | */ | ||
| 125 | function openMemoryUsageApp(app) { | ||
| 126 | switch (app) { | ||
| 127 | case "Activity Monitor": | ||
| 128 | Uebersicht.run(`open -a "Activity Monitor"`); | ||
| 129 | break; | ||
| 130 | case "Top": | ||
| 131 | Utils.runInUserTerminal("top"); | ||
| 132 | break; | ||
| 133 | } | ||
| 134 | } | ||
diff --git a/users/ryan/modules/simple-bar/simple-bar-source/lib/components/data/mic.jsx b/users/ryan/modules/simple-bar/simple-bar-source/lib/components/data/mic.jsx new file mode 100755 index 0000000..5c071a8 --- /dev/null +++ b/users/ryan/modules/simple-bar/simple-bar-source/lib/components/data/mic.jsx | |||
| @@ -0,0 +1,152 @@ | |||
| 1 | import * as Uebersicht from "uebersicht"; | ||
| 2 | import * as DataWidget from "./data-widget.jsx"; | ||
| 3 | import * as DataWidgetLoader from "./data-widget-loader.jsx"; | ||
| 4 | import * as Icons from "../icons/icons.jsx"; | ||
| 5 | import { SuspenseIcon } from "../icons/icon.jsx"; | ||
| 6 | import useWidgetRefresh from "../../hooks/use-widget-refresh"; | ||
| 7 | import useServerSocket from "../../hooks/use-server-socket"; | ||
| 8 | import * as Utils from "../../utils"; | ||
| 9 | import { useSimpleBarContext } from "../simple-bar-context.jsx"; | ||
| 10 | |||
| 11 | const { React } = Uebersicht; | ||
| 12 | |||
| 13 | export { micStyles as styles } from "../../styles/components/data/mic"; | ||
| 14 | |||
| 15 | const DEFAULT_REFRESH_FREQUENCY = 20000; | ||
| 16 | |||
| 17 | /** | ||
| 18 | * Mic widget component. | ||
| 19 | * @returns {JSX.Element} The rendered mic widget. | ||
| 20 | */ | ||
| 21 | export const Widget = React.memo(() => { | ||
| 22 | const { displayIndex, settings } = useSimpleBarContext(); | ||
| 23 | const { widgets, micWidgetOptions } = settings; | ||
| 24 | const { micWidget } = widgets; | ||
| 25 | const { refreshFrequency, showOnDisplay, showIcon } = micWidgetOptions; | ||
| 26 | |||
| 27 | // Determine the refresh frequency for the widget. | ||
| 28 | const refresh = React.useMemo( | ||
| 29 | () => | ||
| 30 | Utils.getRefreshFrequency(refreshFrequency, DEFAULT_REFRESH_FREQUENCY), | ||
| 31 | [refreshFrequency], | ||
| 32 | ); | ||
| 33 | |||
| 34 | // Determine if the widget should be visible. | ||
| 35 | const visible = | ||
| 36 | Utils.isVisibleOnDisplay(displayIndex, showOnDisplay) && micWidget; | ||
| 37 | |||
| 38 | const [state, setState] = React.useState(); | ||
| 39 | const [loading, setLoading] = React.useState(visible); | ||
| 40 | const { volume: _volume } = state || {}; | ||
| 41 | const [volume, setVolume] = React.useState(_volume && parseInt(_volume, 10)); | ||
| 42 | const [dragging, setDragging] = React.useState(false); | ||
| 43 | |||
| 44 | /** | ||
| 45 | * Reset the widget state. | ||
| 46 | */ | ||
| 47 | const resetWidget = () => { | ||
| 48 | setState(undefined); | ||
| 49 | setLoading(false); | ||
| 50 | }; | ||
| 51 | |||
| 52 | /** | ||
| 53 | * Fetch the current microphone volume. | ||
| 54 | */ | ||
| 55 | const getMic = React.useCallback(async () => { | ||
| 56 | if (!visible) return; | ||
| 57 | const volume = await Utils.cachedRun( | ||
| 58 | `osascript -e 'input volume of (get volume settings)'`, | ||
| 59 | refresh, | ||
| 60 | ); | ||
| 61 | setState({ volume: Utils.cleanupOutput(volume) }); | ||
| 62 | setLoading(false); | ||
| 63 | }, [visible, refresh]); | ||
| 64 | |||
| 65 | // Use server socket to get mic data. | ||
| 66 | useServerSocket("mic", visible, getMic, resetWidget, setLoading); | ||
| 67 | // Refresh the widget periodically. | ||
| 68 | useWidgetRefresh(visible, getMic, refresh); | ||
| 69 | |||
| 70 | // Update the mic volume when dragging state changes. | ||
| 71 | React.useEffect(() => { | ||
| 72 | if (!dragging) setMic(volume); | ||
| 73 | }, [dragging, volume]); | ||
| 74 | |||
| 75 | // Update the volume state when the fetched volume changes. | ||
| 76 | React.useEffect(() => { | ||
| 77 | setVolume((currentVolume) => { | ||
| 78 | if (_volume && currentVolume !== parseInt(_volume, 10)) { | ||
| 79 | return parseInt(_volume, 10); | ||
| 80 | } | ||
| 81 | return currentVolume; | ||
| 82 | }); | ||
| 83 | }, [_volume]); | ||
| 84 | |||
| 85 | if (loading) return <DataWidgetLoader.Widget className="mic" />; | ||
| 86 | if (!state || volume === undefined || _volume === "missing value") | ||
| 87 | return null; | ||
| 88 | |||
| 89 | const Icon = !volume ? Icons.MicOff : Icons.MicOn; | ||
| 90 | |||
| 91 | /** | ||
| 92 | * Handle volume change event. | ||
| 93 | * @param {React.ChangeEvent<HTMLInputElement>} e - The change event. | ||
| 94 | */ | ||
| 95 | const onChange = (e) => { | ||
| 96 | const value = parseInt(e.target.value, 10); | ||
| 97 | setVolume(value); | ||
| 98 | }; | ||
| 99 | |||
| 100 | /** | ||
| 101 | * Handle mouse down event on the slider. | ||
| 102 | */ | ||
| 103 | const onMouseDown = () => setDragging(true); | ||
| 104 | |||
| 105 | /** | ||
| 106 | * Handle mouse up event on the slider. | ||
| 107 | */ | ||
| 108 | const onMouseUp = () => setDragging(false); | ||
| 109 | |||
| 110 | const formattedVolume = `${volume.toString().padStart(2, "0")}%`; | ||
| 111 | |||
| 112 | const classes = Utils.classNames("mic", { | ||
| 113 | "mic--dragging": dragging, | ||
| 114 | }); | ||
| 115 | |||
| 116 | return ( | ||
| 117 | <DataWidget.Widget classes={classes} disableSlider> | ||
| 118 | <div className="mic__display"> | ||
| 119 | {showIcon && ( | ||
| 120 | <SuspenseIcon> | ||
| 121 | <Icon /> | ||
| 122 | </SuspenseIcon> | ||
| 123 | )} | ||
| 124 | <span className="mic__value">{formattedVolume}</span> | ||
| 125 | </div> | ||
| 126 | <div className="mic__slider-container"> | ||
| 127 | <input | ||
| 128 | type="range" | ||
| 129 | min="0" | ||
| 130 | max="100" | ||
| 131 | step="1" | ||
| 132 | value={volume} | ||
| 133 | className="mic__slider" | ||
| 134 | onMouseDown={onMouseDown} | ||
| 135 | onMouseUp={onMouseUp} | ||
| 136 | onChange={onChange} | ||
| 137 | /> | ||
| 138 | </div> | ||
| 139 | </DataWidget.Widget> | ||
| 140 | ); | ||
| 141 | }); | ||
| 142 | |||
| 143 | Widget.displayName = "Mic"; | ||
| 144 | |||
| 145 | /** | ||
| 146 | * Set the microphone volume. | ||
| 147 | * @param {number} volume - The volume level to set. | ||
| 148 | */ | ||
| 149 | function setMic(volume) { | ||
| 150 | if (volume === undefined) return; | ||
| 151 | Uebersicht.run(`osascript -e 'set volume input volume ${volume}'`); | ||
| 152 | } | ||
diff --git a/users/ryan/modules/simple-bar/simple-bar-source/lib/components/data/mpd.jsx b/users/ryan/modules/simple-bar/simple-bar-source/lib/components/data/mpd.jsx new file mode 100755 index 0000000..53449b1 --- /dev/null +++ b/users/ryan/modules/simple-bar/simple-bar-source/lib/components/data/mpd.jsx | |||
| @@ -0,0 +1,228 @@ | |||
| 1 | import * as Uebersicht from "uebersicht"; | ||
| 2 | import * as DataWidget from "./data-widget.jsx"; | ||
| 3 | import * as DataWidgetLoader from "./data-widget-loader.jsx"; | ||
| 4 | import * as Icons from "../icons/icons.jsx"; | ||
| 5 | import useWidgetRefresh from "../../hooks/use-widget-refresh"; | ||
| 6 | import useServerSocket from "../../hooks/use-server-socket"; | ||
| 7 | import { useSimpleBarContext } from "../simple-bar-context.jsx"; | ||
| 8 | import * as Utils from "../../utils"; | ||
| 9 | |||
| 10 | export { mpdStyles as styles } from "../../styles/components/data/mpd"; | ||
| 11 | |||
| 12 | const { React } = Uebersicht; | ||
| 13 | |||
| 14 | const DEFAULT_REFRESH_FREQUENCY = 10000; | ||
| 15 | |||
| 16 | /** | ||
| 17 | * MPD Widget component | ||
| 18 | * @returns {JSX.Element|null} The MPD widget | ||
| 19 | */ | ||
| 20 | export const Widget = React.memo(() => { | ||
| 21 | const ref = React.useRef(); | ||
| 22 | const { displayIndex, settings } = useSimpleBarContext(); | ||
| 23 | const { widgets, mpdWidgetOptions } = settings; | ||
| 24 | const { mpdWidget } = widgets; | ||
| 25 | const { | ||
| 26 | refreshFrequency, | ||
| 27 | showSpecter, | ||
| 28 | mpdBinaryPath, | ||
| 29 | mpdHost, | ||
| 30 | mpdPort, | ||
| 31 | mpdFormatString, | ||
| 32 | showOnDisplay, | ||
| 33 | showIcon, | ||
| 34 | } = mpdWidgetOptions; | ||
| 35 | |||
| 36 | // Determine the refresh frequency for the widget | ||
| 37 | const refresh = React.useMemo( | ||
| 38 | () => | ||
| 39 | Utils.getRefreshFrequency(refreshFrequency, DEFAULT_REFRESH_FREQUENCY), | ||
| 40 | [refreshFrequency], | ||
| 41 | ); | ||
| 42 | |||
| 43 | // Determine if the widget should be visible on the current display | ||
| 44 | const visible = | ||
| 45 | Utils.isVisibleOnDisplay(displayIndex, showOnDisplay) && mpdWidget; | ||
| 46 | |||
| 47 | const [state, setState] = React.useState(); | ||
| 48 | const [loading, setLoading] = React.useState(visible); | ||
| 49 | const [isMpdActive, setIsMpdActive] = React.useState(false); | ||
| 50 | const { volume: _volume } = state || {}; | ||
| 51 | const defaultVolume = _volume && parseInt(_volume); | ||
| 52 | const [volume, setVolume] = React.useState(defaultVolume); | ||
| 53 | const [dragging, setDragging] = React.useState(false); | ||
| 54 | |||
| 55 | /** | ||
| 56 | * Reset the widget state | ||
| 57 | */ | ||
| 58 | const resetWidget = () => { | ||
| 59 | setState(undefined); | ||
| 60 | setLoading(false); | ||
| 61 | setIsMpdActive(false); | ||
| 62 | }; | ||
| 63 | |||
| 64 | /** | ||
| 65 | * Fetch MPD data | ||
| 66 | */ | ||
| 67 | const getMpd = React.useCallback(async () => { | ||
| 68 | if (!visible) return; | ||
| 69 | |||
| 70 | try { | ||
| 71 | const mpdProcess = await Utils.cachedRun( | ||
| 72 | `pgrep -x mpd > /dev/null && echo "true" || echo "false"`, | ||
| 73 | refresh, | ||
| 74 | ); | ||
| 75 | |||
| 76 | if (Utils.cleanupOutput(mpdProcess) === "false") { | ||
| 77 | setLoading(false); | ||
| 78 | setIsMpdActive(false); | ||
| 79 | return; | ||
| 80 | } | ||
| 81 | |||
| 82 | const [playerState, trackInfo, volumeState] = await Promise.all([ | ||
| 83 | Utils.cachedRun( | ||
| 84 | `${mpdBinaryPath} --host ${mpdHost} --port ${mpdPort} | head -n 2 | tail -n 1 | awk '{print substr($1,2,length($1)-2)}' 2>/dev/null || echo "stopped"`, | ||
| 85 | refresh, | ||
| 86 | ), | ||
| 87 | Utils.cachedRun( | ||
| 88 | `${mpdBinaryPath} --host ${mpdHost} --port ${mpdPort} --format "${mpdFormatString}" | head -n 1`, | ||
| 89 | refresh, | ||
| 90 | ), | ||
| 91 | Utils.cachedRun( | ||
| 92 | `${mpdBinaryPath} --host ${mpdHost} --port ${mpdPort} volume | sed -e 's/volume:[ ]*//g' -e 's/%//g'`, | ||
| 93 | refresh, | ||
| 94 | ), | ||
| 95 | ]); | ||
| 96 | if (Utils.cleanupOutput(trackInfo) === "") { | ||
| 97 | setLoading(false); | ||
| 98 | setIsMpdActive(false); | ||
| 99 | return; | ||
| 100 | } | ||
| 101 | setState({ | ||
| 102 | playerState: Utils.cleanupOutput(playerState), | ||
| 103 | trackInfo: Utils.cleanupOutput(trackInfo), | ||
| 104 | volume: Utils.cleanupOutput(volumeState), | ||
| 105 | }); | ||
| 106 | setIsMpdActive(true); | ||
| 107 | setLoading(false); | ||
| 108 | } catch { | ||
| 109 | setLoading(false); | ||
| 110 | setIsMpdActive(false); | ||
| 111 | } | ||
| 112 | }, [visible, mpdBinaryPath, mpdHost, mpdPort, mpdFormatString, refresh]); | ||
| 113 | |||
| 114 | // Update the volume when dragging ends | ||
| 115 | React.useEffect(() => { | ||
| 116 | if (!dragging) setSound(mpdBinaryPath, mpdHost, mpdPort, volume); | ||
| 117 | }, [dragging, mpdBinaryPath, mpdHost, mpdPort, volume]); | ||
| 118 | |||
| 119 | // Set the volume state when dragging ends | ||
| 120 | React.useEffect(() => { | ||
| 121 | if (!dragging) setVolume(volume); | ||
| 122 | }, [dragging, volume]); | ||
| 123 | |||
| 124 | useServerSocket("mpd", visible, getMpd, resetWidget, setLoading); | ||
| 125 | useWidgetRefresh(visible, getMpd, refresh); | ||
| 126 | |||
| 127 | if (loading) return <DataWidgetLoader.Widget className="mpd" />; | ||
| 128 | if (!state || !isMpdActive) return null; | ||
| 129 | const { playerState, trackInfo } = state; | ||
| 130 | |||
| 131 | if (!trackInfo.length) return null; | ||
| 132 | |||
| 133 | const isPlaying = playerState === "playing"; | ||
| 134 | const Icon = isPlaying ? Icons.Playing : Icons.Paused; | ||
| 135 | |||
| 136 | /** | ||
| 137 | * Handle click event to toggle play/pause | ||
| 138 | * @param {React.MouseEvent} e - The click event | ||
| 139 | */ | ||
| 140 | const onClick = async (e) => { | ||
| 141 | Utils.clickEffect(e); | ||
| 142 | await togglePlay(mpdBinaryPath, mpdHost, mpdPort); | ||
| 143 | await getMpd(); | ||
| 144 | }; | ||
| 145 | |||
| 146 | /** | ||
| 147 | * Handle volume change event | ||
| 148 | * @param {React.ChangeEvent<HTMLInputElement>} e - The change event | ||
| 149 | */ | ||
| 150 | const onChange = (e) => { | ||
| 151 | const value = parseInt(e.target.value); | ||
| 152 | setVolume(value); | ||
| 153 | }; | ||
| 154 | |||
| 155 | const onMouseDown = () => setDragging(true); | ||
| 156 | |||
| 157 | const onMouseUp = () => setDragging(false); | ||
| 158 | |||
| 159 | const stopPropagation = (e) => e.stopPropagation(); | ||
| 160 | |||
| 161 | const onMouseEnter = () => | ||
| 162 | Utils.startSliding(ref.current, ".mpd__inner", ".mpd__info"); | ||
| 163 | const onMouseLeave = () => Utils.stopSliding(ref.current, ".mpd__slider"); | ||
| 164 | |||
| 165 | const classes = Utils.classNames("sound", "mpd", { | ||
| 166 | "mpd--playing": isPlaying, | ||
| 167 | "mpd--dragging": dragging, | ||
| 168 | }); | ||
| 169 | |||
| 170 | return ( | ||
| 171 | <DataWidget.Widget | ||
| 172 | classes={classes} | ||
| 173 | Icon={showIcon ? Icon : null} | ||
| 174 | onClick={onClick} | ||
| 175 | showSpecter={showSpecter && isPlaying} | ||
| 176 | disableSlider | ||
| 177 | > | ||
| 178 | <div | ||
| 179 | ref={ref} | ||
| 180 | className="mpd__outer" | ||
| 181 | onMouseEnter={onMouseEnter} | ||
| 182 | onMouseLeave={onMouseLeave} | ||
| 183 | > | ||
| 184 | <div className="mpd__inner"> | ||
| 185 | <div className="mpd__info">{trackInfo}</div> | ||
| 186 | </div> | ||
| 187 | <div className="mpd__slider-container" onClick={stopPropagation}> | ||
| 188 | <input | ||
| 189 | type="range" | ||
| 190 | min="0" | ||
| 191 | max="100" | ||
| 192 | step="1" | ||
| 193 | value={volume} | ||
| 194 | className="mpd__slider" | ||
| 195 | onMouseDown={onMouseDown} | ||
| 196 | onMouseUp={onMouseUp} | ||
| 197 | onChange={onChange} | ||
| 198 | /> | ||
| 199 | </div> | ||
| 200 | </div> | ||
| 201 | </DataWidget.Widget> | ||
| 202 | ); | ||
| 203 | }); | ||
| 204 | |||
| 205 | Widget.displayName = "Mpd"; | ||
| 206 | |||
| 207 | /** | ||
| 208 | * Toggle play/pause state of MPD | ||
| 209 | * @param {string} path - The path to the MPD binary | ||
| 210 | * @param {string} host - The MPD host | ||
| 211 | * @param {string} port - The MPD port | ||
| 212 | * @returns {Promise<void>} | ||
| 213 | */ | ||
| 214 | async function togglePlay(path, host, port) { | ||
| 215 | return Uebersicht.run(`${path} --host ${host} --port ${port} toggle`); | ||
| 216 | } | ||
| 217 | |||
| 218 | /** | ||
| 219 | * Set the volume of MPD | ||
| 220 | * @param {string} path - The path to the MPD binary | ||
| 221 | * @param {string} host - The MPD host | ||
| 222 | * @param {string} port - The MPD port | ||
| 223 | * @param {number} volume - The volume level to set | ||
| 224 | */ | ||
| 225 | function setSound(path, host, port, volume) { | ||
| 226 | if (!volume) return; | ||
| 227 | Uebersicht.run(`${path} --host ${host} --port ${port} volume ${volume}`); | ||
| 228 | } | ||
diff --git a/users/ryan/modules/simple-bar/simple-bar-source/lib/components/data/music.jsx b/users/ryan/modules/simple-bar/simple-bar-source/lib/components/data/music.jsx new file mode 100755 index 0000000..f0fabd6 --- /dev/null +++ b/users/ryan/modules/simple-bar/simple-bar-source/lib/components/data/music.jsx | |||
| @@ -0,0 +1,161 @@ | |||
| 1 | import * as Uebersicht from "uebersicht"; | ||
| 2 | import * as DataWidget from "./data-widget.jsx"; | ||
| 3 | import * as DataWidgetLoader from "./data-widget-loader.jsx"; | ||
| 4 | import * as Icons from "../icons/icons.jsx"; | ||
| 5 | import useWidgetRefresh from "../../hooks/use-widget-refresh"; | ||
| 6 | import useServerSocket from "../../hooks/use-server-socket"; | ||
| 7 | import { useSimpleBarContext } from "../simple-bar-context.jsx"; | ||
| 8 | import * as Utils from "../../utils"; | ||
| 9 | |||
| 10 | export { musicStyles as styles } from "../../styles/components/data/music"; | ||
| 11 | |||
| 12 | const { React } = Uebersicht; | ||
| 13 | |||
| 14 | const DEFAULT_REFRESH_FREQUENCY = 10000; | ||
| 15 | |||
| 16 | /** | ||
| 17 | * Music widget component. | ||
| 18 | * @returns {JSX.Element|null} The music widget. | ||
| 19 | */ | ||
| 20 | export const Widget = React.memo(() => { | ||
| 21 | const { displayIndex, settings } = useSimpleBarContext(); | ||
| 22 | const { widgets, musicWidgetOptions } = settings; | ||
| 23 | const { musicWidget } = widgets; | ||
| 24 | const { refreshFrequency, showSpecter, showOnDisplay, showIcon } = | ||
| 25 | musicWidgetOptions; | ||
| 26 | |||
| 27 | // Determine the refresh frequency for the widget | ||
| 28 | const refresh = React.useMemo( | ||
| 29 | () => | ||
| 30 | Utils.getRefreshFrequency(refreshFrequency, DEFAULT_REFRESH_FREQUENCY), | ||
| 31 | [refreshFrequency], | ||
| 32 | ); | ||
| 33 | |||
| 34 | // Determine if the widget should be visible on the current display | ||
| 35 | const visible = | ||
| 36 | Utils.isVisibleOnDisplay(displayIndex, showOnDisplay) && musicWidget; | ||
| 37 | |||
| 38 | const [state, setState] = React.useState(); | ||
| 39 | const [loading, setLoading] = React.useState(visible); | ||
| 40 | const [isMusicActive, setIsMusicActive] = React.useState(false); | ||
| 41 | |||
| 42 | /** | ||
| 43 | * Resets the widget state. | ||
| 44 | */ | ||
| 45 | const resetWidget = () => { | ||
| 46 | setState(undefined); | ||
| 47 | setLoading(false); | ||
| 48 | setIsMusicActive(false); | ||
| 49 | }; | ||
| 50 | |||
| 51 | /** | ||
| 52 | * Fetches the current music information. | ||
| 53 | */ | ||
| 54 | const getMusic = React.useCallback(async () => { | ||
| 55 | if (!visible) return; | ||
| 56 | const osVersion = await Utils.cachedRun(`sw_vers -productVersion`, refresh); | ||
| 57 | const processName = | ||
| 58 | Utils.cleanupOutput(osVersion) === "10.15" ? "iTunes" : "Music"; | ||
| 59 | const isRunning = await Utils.cachedRun( | ||
| 60 | `pgrep -xq "${processName}" && echo "true" || echo "false"`, | ||
| 61 | refresh, | ||
| 62 | ); | ||
| 63 | if (Utils.cleanupOutput(isRunning) === "false") { | ||
| 64 | setLoading(false); | ||
| 65 | setIsMusicActive(false); | ||
| 66 | return; | ||
| 67 | } | ||
| 68 | const output = await Utils.cachedRun( | ||
| 69 | `osascript -e 'tell application "${processName}"' -e 'set output to (player state as string) & "|" & (name of current track as string) & "|" & (artist of current track as string)' -e 'end tell' 2>/dev/null || echo "stopped|unknown track|unknown artist"`, | ||
| 70 | refresh, | ||
| 71 | ); | ||
| 72 | const [playerState, trackName, artistName] = | ||
| 73 | Utils.cleanupOutput(output).split("|"); | ||
| 74 | setState({ | ||
| 75 | playerState, | ||
| 76 | trackName, | ||
| 77 | artistName, | ||
| 78 | processName: Utils.cleanupOutput(processName), | ||
| 79 | }); | ||
| 80 | setIsMusicActive(true); | ||
| 81 | setLoading(false); | ||
| 82 | }, [visible, refresh]); | ||
| 83 | |||
| 84 | // Use server socket to listen for music events | ||
| 85 | useServerSocket("music", visible, getMusic, resetWidget, setLoading); | ||
| 86 | // Refresh the widget at the specified interval | ||
| 87 | useWidgetRefresh(visible, getMusic, refresh); | ||
| 88 | |||
| 89 | if (loading) return <DataWidgetLoader.Widget className="music" />; | ||
| 90 | if (!state || !isMusicActive) return null; | ||
| 91 | const { processName, playerState, trackName, artistName } = state; | ||
| 92 | |||
| 93 | if (!trackName.length) return null; | ||
| 94 | |||
| 95 | const isPlaying = playerState === "playing"; | ||
| 96 | const Icon = isPlaying ? Icons.Playing : Icons.Paused; | ||
| 97 | |||
| 98 | /** | ||
| 99 | * Handles click event to toggle play/pause. | ||
| 100 | * @param {React.MouseEvent} e - The click event. | ||
| 101 | */ | ||
| 102 | const onClick = (e) => { | ||
| 103 | Utils.clickEffect(e); | ||
| 104 | togglePlay(!isPlaying, processName); | ||
| 105 | getMusic(); | ||
| 106 | }; | ||
| 107 | |||
| 108 | /** | ||
| 109 | * Handles right-click event to skip to the next track. | ||
| 110 | * @param {React.MouseEvent} e - The right-click event. | ||
| 111 | */ | ||
| 112 | const onRightClick = (e) => { | ||
| 113 | Utils.clickEffect(e); | ||
| 114 | Uebersicht.run( | ||
| 115 | `osascript -e 'tell application "${processName}" to Next Track'`, | ||
| 116 | ); | ||
| 117 | getMusic(); | ||
| 118 | }; | ||
| 119 | |||
| 120 | /** | ||
| 121 | * Handles middle-click event to open the music application. | ||
| 122 | * @param {React.MouseEvent} e - The middle-click event. | ||
| 123 | */ | ||
| 124 | const onMiddleClick = (e) => { | ||
| 125 | Utils.clickEffect(e); | ||
| 126 | Uebersicht.run(`open -a '${processName}'`); | ||
| 127 | getMusic(); | ||
| 128 | }; | ||
| 129 | |||
| 130 | const classes = Utils.classNames("music", { | ||
| 131 | "music--playing": isPlaying, | ||
| 132 | }); | ||
| 133 | |||
| 134 | return ( | ||
| 135 | <DataWidget.Widget | ||
| 136 | classes={classes} | ||
| 137 | Icon={showIcon ? Icon : null} | ||
| 138 | onClick={onClick} | ||
| 139 | onRightClick={onRightClick} | ||
| 140 | onMiddleClick={onMiddleClick} | ||
| 141 | showSpecter={showSpecter && isPlaying} | ||
| 142 | > | ||
| 143 | {trackName} - {artistName} | ||
| 144 | </DataWidget.Widget> | ||
| 145 | ); | ||
| 146 | }); | ||
| 147 | |||
| 148 | Widget.displayName = "Music"; | ||
| 149 | |||
| 150 | /** | ||
| 151 | * Toggles play/pause state of the music application. | ||
| 152 | * @param {boolean} isPaused - Whether the music is paused. | ||
| 153 | * @param {string} processName - The name of the music application process. | ||
| 154 | */ | ||
| 155 | function togglePlay(isPaused, processName) { | ||
| 156 | if (isPaused) { | ||
| 157 | Uebersicht.run(`osascript -e 'tell application "${processName}" to play'`); | ||
| 158 | } else { | ||
| 159 | Uebersicht.run(`osascript -e 'tell application "${processName}" to pause'`); | ||
| 160 | } | ||
| 161 | } | ||
diff --git a/users/ryan/modules/simple-bar/simple-bar-source/lib/components/data/netstats.jsx b/users/ryan/modules/simple-bar/simple-bar-source/lib/components/data/netstats.jsx new file mode 100755 index 0000000..ef3d7be --- /dev/null +++ b/users/ryan/modules/simple-bar/simple-bar-source/lib/components/data/netstats.jsx | |||
| @@ -0,0 +1,182 @@ | |||
| 1 | import * as Uebersicht from "uebersicht"; | ||
| 2 | import * as DataWidget from "./data-widget.jsx"; | ||
| 3 | import * as DataWidgetLoader from "./data-widget-loader.jsx"; | ||
| 4 | import * as Icons from "../icons/icons.jsx"; | ||
| 5 | import { SuspenseIcon } from "../icons/icon.jsx"; | ||
| 6 | import Graph from "./graph.jsx"; | ||
| 7 | import useWidgetRefresh from "../../hooks/use-widget-refresh.js"; | ||
| 8 | import useServerSocket from "../../hooks/use-server-socket"; | ||
| 9 | import { useSimpleBarContext } from "../simple-bar-context.jsx"; | ||
| 10 | import * as Utils from "../../utils.js"; | ||
| 11 | |||
| 12 | export { netstatsStyles as styles } from "../../styles/components/data/netstats"; | ||
| 13 | |||
| 14 | const { React } = Uebersicht; | ||
| 15 | |||
| 16 | const DEFAULT_REFRESH_FREQUENCY = 2000; | ||
| 17 | const GRAPH_LENGTH = 30; | ||
| 18 | |||
| 19 | /** | ||
| 20 | * Netstats widget component. | ||
| 21 | * @returns {JSX.Element|null} The rendered component. | ||
| 22 | */ | ||
| 23 | export const Widget = React.memo(() => { | ||
| 24 | const { displayIndex, settings } = useSimpleBarContext(); | ||
| 25 | const { widgets, netstatsWidgetOptions } = settings; | ||
| 26 | const { netstatsWidget } = widgets; | ||
| 27 | const { | ||
| 28 | refreshFrequency, | ||
| 29 | showOnDisplay, | ||
| 30 | displayAsGraph, | ||
| 31 | showIcon, | ||
| 32 | netstatsThreshold, | ||
| 33 | } = netstatsWidgetOptions; | ||
| 34 | |||
| 35 | const isDisabled = React.useRef(false); | ||
| 36 | |||
| 37 | // Determine the refresh frequency for the widget | ||
| 38 | const refresh = React.useMemo( | ||
| 39 | () => | ||
| 40 | Utils.getRefreshFrequency(refreshFrequency, DEFAULT_REFRESH_FREQUENCY), | ||
| 41 | [refreshFrequency], | ||
| 42 | ); | ||
| 43 | |||
| 44 | // Determine if the widget should be visible | ||
| 45 | const visible = | ||
| 46 | Utils.isVisibleOnDisplay(displayIndex, showOnDisplay) && netstatsWidget; | ||
| 47 | |||
| 48 | const [graph, setGraph] = React.useState([]); | ||
| 49 | const [state, setState] = React.useState(); | ||
| 50 | const [loading, setLoading] = React.useState(visible); | ||
| 51 | |||
| 52 | /** | ||
| 53 | * Resets the widget state. | ||
| 54 | */ | ||
| 55 | const resetWidget = () => { | ||
| 56 | setState(undefined); | ||
| 57 | setLoading(false); | ||
| 58 | setGraph([]); | ||
| 59 | }; | ||
| 60 | |||
| 61 | /** | ||
| 62 | * Fetches network statistics. | ||
| 63 | */ | ||
| 64 | const getNetstats = React.useCallback(async () => { | ||
| 65 | if (!visible) return; | ||
| 66 | try { | ||
| 67 | const output = await Utils.cachedRun( | ||
| 68 | `bash ./simple-bar/lib/scripts/netstats.sh 2>&1`, | ||
| 69 | refresh, | ||
| 70 | ); | ||
| 71 | if (!visible || isDisabled.current) { | ||
| 72 | return; | ||
| 73 | } | ||
| 74 | const data = Utils.cleanupOutput(output); | ||
| 75 | const json = JSON.parse(data); | ||
| 76 | setState(json); | ||
| 77 | if (displayAsGraph) { | ||
| 78 | Utils.addToGraphHistory(json, setGraph, GRAPH_LENGTH); | ||
| 79 | } | ||
| 80 | setLoading(false); | ||
| 81 | } catch { | ||
| 82 | setTimeout(getNetstats, 1000); | ||
| 83 | } | ||
| 84 | }, [displayAsGraph, setGraph, visible, refresh]); | ||
| 85 | |||
| 86 | // Update the disabled state based on visibility | ||
| 87 | React.useEffect(() => { | ||
| 88 | isDisabled.current = !visible; | ||
| 89 | }, [visible]); | ||
| 90 | |||
| 91 | // Set up server socket and widget refresh hooks | ||
| 92 | useServerSocket("netstats", visible, getNetstats, resetWidget, setLoading); | ||
| 93 | useWidgetRefresh(visible, getNetstats, refresh); | ||
| 94 | |||
| 95 | if (loading) | ||
| 96 | return ( | ||
| 97 | <React.Fragment> | ||
| 98 | <DataWidgetLoader.Widget className="netstats" /> | ||
| 99 | {!displayAsGraph && <DataWidgetLoader.Widget className="netstats" />} | ||
| 100 | </React.Fragment> | ||
| 101 | ); | ||
| 102 | |||
| 103 | if (!state) { | ||
| 104 | return null; | ||
| 105 | } | ||
| 106 | |||
| 107 | const { download, upload } = state; | ||
| 108 | |||
| 109 | if (download === undefined || upload === undefined) { | ||
| 110 | return null; | ||
| 111 | } | ||
| 112 | |||
| 113 | const threshold = (Number(netstatsThreshold) || 0) * 1024; | ||
| 114 | const isBelowThreshold = | ||
| 115 | threshold > 0 && | ||
| 116 | Math.abs(download) < threshold && | ||
| 117 | Math.abs(upload) < threshold; | ||
| 118 | |||
| 119 | if (isBelowThreshold) { | ||
| 120 | return null; | ||
| 121 | } | ||
| 122 | |||
| 123 | const formattedDownload = Utils.formatBytes(download); | ||
| 124 | const formattedUpload = Utils.formatBytes(upload); | ||
| 125 | |||
| 126 | if (displayAsGraph) { | ||
| 127 | return ( | ||
| 128 | <DataWidget.Widget classes="netstats netstats--graph" disableSlider> | ||
| 129 | <Graph | ||
| 130 | className="netstats__graph" | ||
| 131 | caption={{ | ||
| 132 | download: { | ||
| 133 | value: formattedDownload, | ||
| 134 | icon: showIcon ? Icons.Download : null, | ||
| 135 | color: "var(--magenta)", | ||
| 136 | }, | ||
| 137 | upload: { | ||
| 138 | value: formattedUpload, | ||
| 139 | icon: showIcon ? Icons.Upload : null, | ||
| 140 | color: "var(--blue)", | ||
| 141 | }, | ||
| 142 | }} | ||
| 143 | values={graph} | ||
| 144 | maxLength={GRAPH_LENGTH} | ||
| 145 | /> | ||
| 146 | </DataWidget.Widget> | ||
| 147 | ); | ||
| 148 | } | ||
| 149 | |||
| 150 | return ( | ||
| 151 | <React.Fragment> | ||
| 152 | <DataWidget.Widget classes="netstats" disableSlider> | ||
| 153 | <div className="netstats__item"> | ||
| 154 | {showIcon && ( | ||
| 155 | <SuspenseIcon> | ||
| 156 | <Icons.Download className="netstats__icon netstats__icon--download" /> | ||
| 157 | </SuspenseIcon> | ||
| 158 | )} | ||
| 159 | <span | ||
| 160 | className="netstats__value" | ||
| 161 | dangerouslySetInnerHTML={{ __html: formattedDownload }} | ||
| 162 | /> | ||
| 163 | </div> | ||
| 164 | </DataWidget.Widget> | ||
| 165 | <DataWidget.Widget classes="netstats" disableSlider> | ||
| 166 | <div className="netstats__item"> | ||
| 167 | {showIcon && ( | ||
| 168 | <SuspenseIcon> | ||
| 169 | <Icons.Upload className="netstats__icon netstats__icon--upload" /> | ||
| 170 | </SuspenseIcon> | ||
| 171 | )} | ||
| 172 | <span | ||
| 173 | className="netstats__value" | ||
| 174 | dangerouslySetInnerHTML={{ __html: formattedUpload }} | ||
| 175 | /> | ||
| 176 | </div> | ||
| 177 | </DataWidget.Widget> | ||
| 178 | </React.Fragment> | ||
| 179 | ); | ||
| 180 | }); | ||
| 181 | |||
| 182 | Widget.displayName = "Netstats"; | ||
diff --git a/users/ryan/modules/simple-bar/simple-bar-source/lib/components/data/next-meeting.jsx b/users/ryan/modules/simple-bar/simple-bar-source/lib/components/data/next-meeting.jsx new file mode 100755 index 0000000..74f6bf8 --- /dev/null +++ b/users/ryan/modules/simple-bar/simple-bar-source/lib/components/data/next-meeting.jsx | |||
| @@ -0,0 +1,310 @@ | |||
| 1 | /** | ||
| 2 | * Next Meeting Widget | ||
| 3 | * | ||
| 4 | * Displays the next upcoming calendar event with time until start and optional join button. | ||
| 5 | * Uses icalBuddy for fast calendar access with proper recurring event support. | ||
| 6 | * | ||
| 7 | * Requirements: | ||
| 8 | * - macOS with Calendar.app or compatible calendar | ||
| 9 | * - icalBuddy installed (brew install ical-buddy) | ||
| 10 | * - Calendar access permissions granted | ||
| 11 | * | ||
| 12 | * @module next-meeting | ||
| 13 | */ | ||
| 14 | import * as Uebersicht from "uebersicht"; | ||
| 15 | import * as DataWidget from "./data-widget.jsx"; | ||
| 16 | import * as DataWidgetLoader from "./data-widget-loader.jsx"; | ||
| 17 | import * as Icons from "../icons/icons.jsx"; | ||
| 18 | import useWidgetRefresh from "../../hooks/use-widget-refresh"; | ||
| 19 | import useServerSocket from "../../hooks/use-server-socket"; | ||
| 20 | import { useSimpleBarContext } from "../simple-bar-context.jsx"; | ||
| 21 | import * as Utils from "../../utils"; | ||
| 22 | |||
| 23 | export { nextMeetingStyles as styles } from "../../styles/components/data/next-meeting"; | ||
| 24 | |||
| 25 | const { React } = Uebersicht; | ||
| 26 | |||
| 27 | const DEFAULT_REFRESH_FREQUENCY = 60000; // 1 minute | ||
| 28 | |||
| 29 | // Timing thresholds (in minutes) | ||
| 30 | const URGENT_THRESHOLD = 5; // Red pulsing when ≤5 min until meeting | ||
| 31 | const UPCOMING_THRESHOLD = 15; // Yellow when ≤15 min until meeting | ||
| 32 | |||
| 33 | // Uses icalBuddy for fast calendar access (handles recurring events properly) | ||
| 34 | // Note: Shell script automatically skips meetings that started more than 15 minutes ago | ||
| 35 | const BASE_COMMAND = `./simple-bar/lib/scripts/next-meeting.sh`; | ||
| 36 | |||
| 37 | /** | ||
| 38 | * Builds the shell command with optional custom icalBuddy path and look-ahead hours. | ||
| 39 | * @param {string} icalBuddyPath - Optional custom path to icalBuddy binary | ||
| 40 | * @param {number} lookAheadHours - Hours to look ahead for meetings (default: 12) | ||
| 41 | * @returns {string} Complete shell command | ||
| 42 | */ | ||
| 43 | function buildCommand(icalBuddyPath, lookAheadHours) { | ||
| 44 | const hours = lookAheadHours || 12; | ||
| 45 | if (icalBuddyPath && icalBuddyPath.trim()) { | ||
| 46 | // Escape special shell characters to prevent injection | ||
| 47 | const safePath = icalBuddyPath.replace(/["$`\\]/g, "\\$&"); | ||
| 48 | return `${BASE_COMMAND} "${safePath}" ${hours}`; | ||
| 49 | } | ||
| 50 | return `${BASE_COMMAND} "" ${hours}`; | ||
| 51 | } | ||
| 52 | |||
| 53 | // Patterns to extract meeting URLs from notes | ||
| 54 | const MEETING_URL_PATTERNS = [ | ||
| 55 | /https?:\/\/[\w-]*\.?zoom\.us\/[^\s<>"]+/gi, | ||
| 56 | /https?:\/\/teams\.microsoft\.com\/l\/meetup-join\/[^\s<>"]+/gi, | ||
| 57 | /https?:\/\/meet\.google\.com\/[^\s<>"]+/gi, | ||
| 58 | /https?:\/\/[\w-]*\.?webex\.com\/[^\s<>"]+/gi, | ||
| 59 | ]; | ||
| 60 | |||
| 61 | // SafeLinks pattern (Microsoft Outlook protection wrapper) | ||
| 62 | const SAFELINKS_PATTERN = | ||
| 63 | /https?:\/\/[\w.-]*safelinks\.protection\.outlook\.com\/[^\s<>">\]]+/gi; | ||
| 64 | |||
| 65 | /** | ||
| 66 | * Decode a URL that may be URL-encoded | ||
| 67 | * @param {string} url - Possibly encoded URL | ||
| 68 | * @returns {string} Decoded URL | ||
| 69 | */ | ||
| 70 | function decodeUrl(url) { | ||
| 71 | try { | ||
| 72 | return decodeURIComponent(url); | ||
| 73 | } catch { | ||
| 74 | return url; | ||
| 75 | } | ||
| 76 | } | ||
| 77 | |||
| 78 | /** | ||
| 79 | * Extract the real URL from a SafeLink wrapper | ||
| 80 | * @param {string} safeLink - SafeLink URL | ||
| 81 | * @returns {string} Extracted real URL or original | ||
| 82 | */ | ||
| 83 | function extractFromSafeLink(safeLink) { | ||
| 84 | const urlParam = safeLink.match(/[?&]url=([^&]+)/i); | ||
| 85 | if (urlParam && urlParam[1]) { | ||
| 86 | return decodeUrl(urlParam[1]); | ||
| 87 | } | ||
| 88 | return safeLink; | ||
| 89 | } | ||
| 90 | |||
| 91 | /** | ||
| 92 | * Extract meeting URL from event URL or notes | ||
| 93 | * @param {string} url - Event URL | ||
| 94 | * @param {string} notes - Event notes/description | ||
| 95 | * @returns {string|null} Meeting URL or null | ||
| 96 | */ | ||
| 97 | function extractMeetingUrl(url, notes) { | ||
| 98 | // First check direct URL | ||
| 99 | if (url && url.trim() && url !== "missing value") { | ||
| 100 | for (const pattern of MEETING_URL_PATTERNS) { | ||
| 101 | pattern.lastIndex = 0; | ||
| 102 | const match = url.match(pattern); | ||
| 103 | if (match) return match[0]; | ||
| 104 | } | ||
| 105 | if (url.startsWith("http")) return url; | ||
| 106 | } | ||
| 107 | |||
| 108 | // Check notes for meeting links | ||
| 109 | if (notes && notes.trim()) { | ||
| 110 | // First try direct meeting URLs | ||
| 111 | for (const pattern of MEETING_URL_PATTERNS) { | ||
| 112 | pattern.lastIndex = 0; | ||
| 113 | const match = notes.match(pattern); | ||
| 114 | if (match) return match[0]; | ||
| 115 | } | ||
| 116 | |||
| 117 | // Check for SafeLinks that contain meeting URLs | ||
| 118 | SAFELINKS_PATTERN.lastIndex = 0; | ||
| 119 | const safeLinks = notes.match(SAFELINKS_PATTERN); | ||
| 120 | if (safeLinks) { | ||
| 121 | for (const safeLink of safeLinks) { | ||
| 122 | const realUrl = extractFromSafeLink(safeLink); | ||
| 123 | for (const pattern of MEETING_URL_PATTERNS) { | ||
| 124 | pattern.lastIndex = 0; | ||
| 125 | if (pattern.test(realUrl)) { | ||
| 126 | return realUrl; | ||
| 127 | } | ||
| 128 | } | ||
| 129 | } | ||
| 130 | } | ||
| 131 | } | ||
| 132 | |||
| 133 | return null; | ||
| 134 | } | ||
| 135 | |||
| 136 | /** | ||
| 137 | * Format time until meeting | ||
| 138 | * @param {number} minutes - Minutes until meeting | ||
| 139 | * @returns {string} Formatted time string | ||
| 140 | */ | ||
| 141 | function formatTimeUntil(minutes) { | ||
| 142 | if (minutes < 0) return "now"; | ||
| 143 | if (minutes < 1) return "<1m"; | ||
| 144 | if (minutes < 60) return `${minutes}m`; | ||
| 145 | const hours = Math.floor(minutes / 60); | ||
| 146 | const mins = minutes % 60; | ||
| 147 | if (mins === 0) return `${hours}h`; | ||
| 148 | return `${hours}h ${mins}m`; | ||
| 149 | } | ||
| 150 | |||
| 151 | /** | ||
| 152 | * Parse meeting data from shell script output | ||
| 153 | * @param {string} output - Raw command output | ||
| 154 | * @returns {Object|null} Meeting object or null | ||
| 155 | */ | ||
| 156 | function parseMeetingData(output) { | ||
| 157 | if (!output || !output.trim()) return null; | ||
| 158 | |||
| 159 | const parts = output.trim().split("|"); | ||
| 160 | if (parts.length < 5) return null; | ||
| 161 | |||
| 162 | const [title, startTime, url, notes, minutesUntil] = parts; | ||
| 163 | if (!title) return null; | ||
| 164 | |||
| 165 | const meetingUrl = extractMeetingUrl(url, notes); | ||
| 166 | |||
| 167 | return { | ||
| 168 | title: title.trim(), | ||
| 169 | startTime: startTime, | ||
| 170 | url: meetingUrl, | ||
| 171 | minutesUntil: parseInt(minutesUntil, 10) || 0, | ||
| 172 | }; | ||
| 173 | } | ||
| 174 | |||
| 175 | /** | ||
| 176 | * Join button sub-component for meeting URLs. | ||
| 177 | * @component | ||
| 178 | * @param {Object} props - Component props | ||
| 179 | * @param {string} props.url - Meeting URL to open | ||
| 180 | * @returns {JSX.Element} Join button element | ||
| 181 | */ | ||
| 182 | const JoinButton = React.memo(({ url }) => { | ||
| 183 | /** | ||
| 184 | * Opens the meeting URL when clicked. | ||
| 185 | * @param {React.MouseEvent} e - Click event | ||
| 186 | */ | ||
| 187 | const handleJoin = async (e) => { | ||
| 188 | e.stopPropagation(); | ||
| 189 | Utils.clickEffect(e); | ||
| 190 | // Escape special shell characters to prevent injection | ||
| 191 | const safeUrl = url.replace(/["$`\\]/g, "\\$&"); | ||
| 192 | await Uebersicht.run(`open "${safeUrl}"`); | ||
| 193 | }; | ||
| 194 | |||
| 195 | return ( | ||
| 196 | <button | ||
| 197 | className="next-meeting__join" | ||
| 198 | onClick={handleJoin} | ||
| 199 | title="Join meeting" | ||
| 200 | aria-label="Join video meeting" | ||
| 201 | > | ||
| 202 | Join | ||
| 203 | </button> | ||
| 204 | ); | ||
| 205 | }); | ||
| 206 | |||
| 207 | JoinButton.displayName = "JoinButton"; | ||
| 208 | |||
| 209 | /** | ||
| 210 | * Next Meeting widget component | ||
| 211 | * Shows the next upcoming meeting with optional join button | ||
| 212 | * @returns {JSX.Element|null} The next meeting widget | ||
| 213 | */ | ||
| 214 | export const Widget = React.memo(() => { | ||
| 215 | const { displayIndex, settings } = useSimpleBarContext(); | ||
| 216 | const { widgets, nextMeetingWidgetOptions, dateWidgetOptions } = settings; | ||
| 217 | const { nextMeetingWidget } = widgets; | ||
| 218 | const { | ||
| 219 | refreshFrequency, | ||
| 220 | showOnDisplay, | ||
| 221 | showJoinButton, | ||
| 222 | showTimeOnly, | ||
| 223 | icalBuddyPath, | ||
| 224 | lookAheadHours, | ||
| 225 | } = nextMeetingWidgetOptions; | ||
| 226 | |||
| 227 | // Calculate the refresh frequency for the widget | ||
| 228 | const refresh = React.useMemo( | ||
| 229 | () => | ||
| 230 | Utils.getRefreshFrequency(refreshFrequency, DEFAULT_REFRESH_FREQUENCY), | ||
| 231 | [refreshFrequency], | ||
| 232 | ); | ||
| 233 | |||
| 234 | // Determine if the widget should be visible based on display settings | ||
| 235 | const visible = | ||
| 236 | Utils.isVisibleOnDisplay(displayIndex, showOnDisplay) && nextMeetingWidget; | ||
| 237 | |||
| 238 | const [state, setState] = React.useState(null); | ||
| 239 | const [loading, setLoading] = React.useState(visible); | ||
| 240 | |||
| 241 | /** | ||
| 242 | * Resets the widget state. | ||
| 243 | */ | ||
| 244 | const resetWidget = React.useCallback(() => { | ||
| 245 | setState(null); | ||
| 246 | setLoading(false); | ||
| 247 | }, []); | ||
| 248 | |||
| 249 | /** | ||
| 250 | * Fetches next meeting and updates the state. | ||
| 251 | */ | ||
| 252 | const getMeeting = React.useCallback(async () => { | ||
| 253 | if (!visible) return; | ||
| 254 | try { | ||
| 255 | const command = buildCommand(icalBuddyPath, lookAheadHours); | ||
| 256 | const output = await Uebersicht.run(command); | ||
| 257 | const meeting = parseMeetingData(output); | ||
| 258 | setState(meeting); | ||
| 259 | } catch (error) { | ||
| 260 | // eslint-disable-next-line no-console | ||
| 261 | console.error("Error fetching next meeting:", error); | ||
| 262 | setState(null); | ||
| 263 | } | ||
| 264 | setLoading(false); | ||
| 265 | }, [visible, icalBuddyPath, lookAheadHours]); | ||
| 266 | |||
| 267 | // Server socket for real-time updates | ||
| 268 | useServerSocket("next-meeting", visible, getMeeting, resetWidget, setLoading); | ||
| 269 | |||
| 270 | // Refresh the widget at the specified interval | ||
| 271 | useWidgetRefresh(visible, getMeeting, refresh); | ||
| 272 | |||
| 273 | if (loading) return <DataWidgetLoader.Widget className="next-meeting" />; | ||
| 274 | if (!state) return null; | ||
| 275 | |||
| 276 | const { title, minutesUntil, url } = state; | ||
| 277 | const timeDisplay = formatTimeUntil(minutesUntil); | ||
| 278 | const isUrgent = minutesUntil <= URGENT_THRESHOLD; // Red pulsing: ≤5 min or in progress (up to 15 min) | ||
| 279 | const isUpcoming = minutesUntil <= UPCOMING_THRESHOLD; // Yellow: ≤15 min (but not urgent/in-progress) | ||
| 280 | |||
| 281 | const widgetClasses = Utils.classNames("next-meeting", { | ||
| 282 | "next-meeting--urgent": isUrgent, | ||
| 283 | "next-meeting--upcoming": isUpcoming && !isUrgent, | ||
| 284 | }); | ||
| 285 | |||
| 286 | /** | ||
| 287 | * Handle click event to open the calendar application | ||
| 288 | * @param {Event} e - The click event | ||
| 289 | */ | ||
| 290 | const openCalendar = async (e) => { | ||
| 291 | Utils.clickEffect(e); | ||
| 292 | const calendarApp = dateWidgetOptions?.calendarApp || "Calendar"; | ||
| 293 | await Uebersicht.run(`open -a "${calendarApp}"`); | ||
| 294 | }; | ||
| 295 | |||
| 296 | return ( | ||
| 297 | <DataWidget.Widget | ||
| 298 | classes={widgetClasses} | ||
| 299 | Icon={Icons.Calendar} | ||
| 300 | onClick={openCalendar} | ||
| 301 | useDivForClick | ||
| 302 | > | ||
| 303 | {!showTimeOnly && <span className="next-meeting__title">{title}</span>} | ||
| 304 | <span className="next-meeting__time">{timeDisplay}</span> | ||
| 305 | {showJoinButton && url && <JoinButton url={url} />} | ||
| 306 | </DataWidget.Widget> | ||
| 307 | ); | ||
| 308 | }); | ||
| 309 | |||
| 310 | Widget.displayName = "NextMeeting"; | ||
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 | */ | ||
| 13 | import * as Uebersicht from "uebersicht"; | ||
| 14 | import * as AppIcons from "../../app-icons.js"; | ||
| 15 | import { SuspenseIcon } from "../icons/icon.jsx"; | ||
| 16 | import useWidgetRefresh from "../../hooks/use-widget-refresh"; | ||
| 17 | import useServerSocket from "../../hooks/use-server-socket"; | ||
| 18 | import { useSimpleBarContext } from "../simple-bar-context.jsx"; | ||
| 19 | import * as Utils from "../../utils"; | ||
| 20 | |||
| 21 | export { notificationsStyles as styles } from "../../styles/components/data/notifications"; | ||
| 22 | |||
| 23 | const { React } = Uebersicht; | ||
| 24 | |||
| 25 | const DEFAULT_REFRESH_FREQUENCY = 10000; | ||
| 26 | |||
| 27 | // Shell command to get notification badges from running apps | ||
| 28 | const 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 | */ | ||
| 43 | function 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 | */ | ||
| 67 | const 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 | |||
| 97 | NotificationPill.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 | */ | ||
| 105 | export 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 | |||
| 189 | Widget.displayName = "Notifications"; | ||
diff --git a/users/ryan/modules/simple-bar/simple-bar-source/lib/components/data/sound.jsx b/users/ryan/modules/simple-bar/simple-bar-source/lib/components/data/sound.jsx new file mode 100755 index 0000000..7301426 --- /dev/null +++ b/users/ryan/modules/simple-bar/simple-bar-source/lib/components/data/sound.jsx | |||
| @@ -0,0 +1,154 @@ | |||
| 1 | import * as Uebersicht from "uebersicht"; | ||
| 2 | import * as DataWidget from "./data-widget.jsx"; | ||
| 3 | import * as DataWidgetLoader from "./data-widget-loader.jsx"; | ||
| 4 | import * as Icons from "../icons/icons.jsx"; | ||
| 5 | import { SuspenseIcon } from "../icons/icon.jsx"; | ||
| 6 | import useWidgetRefresh from "../../hooks/use-widget-refresh"; | ||
| 7 | import useServerSocket from "../../hooks/use-server-socket"; | ||
| 8 | import { useSimpleBarContext } from "../simple-bar-context.jsx"; | ||
| 9 | import * as Utils from "../../utils"; | ||
| 10 | |||
| 11 | export { soundStyles as styles } from "../../styles/components/data/sound"; | ||
| 12 | |||
| 13 | const { React } = Uebersicht; | ||
| 14 | |||
| 15 | const DEFAULT_REFRESH_FREQUENCY = 20000; | ||
| 16 | |||
| 17 | /** | ||
| 18 | * Sound widget component. | ||
| 19 | * @returns {JSX.Element|null} The sound widget. | ||
| 20 | */ | ||
| 21 | export const Widget = React.memo(() => { | ||
| 22 | const { displayIndex, settings } = useSimpleBarContext(); | ||
| 23 | const { widgets, soundWidgetOptions } = settings; | ||
| 24 | const { soundWidget } = widgets; | ||
| 25 | const { refreshFrequency, showOnDisplay, showIcon } = soundWidgetOptions; | ||
| 26 | |||
| 27 | // Determine the refresh frequency for the widget. | ||
| 28 | const refresh = React.useMemo( | ||
| 29 | () => | ||
| 30 | Utils.getRefreshFrequency(refreshFrequency, DEFAULT_REFRESH_FREQUENCY), | ||
| 31 | [refreshFrequency], | ||
| 32 | ); | ||
| 33 | |||
| 34 | // Determine if the widget should be visible on the current display. | ||
| 35 | const visible = | ||
| 36 | Utils.isVisibleOnDisplay(displayIndex, showOnDisplay) && soundWidget; | ||
| 37 | |||
| 38 | const [state, setState] = React.useState(); | ||
| 39 | const [loading, setLoading] = React.useState(visible); | ||
| 40 | const { volume: _volume } = state || {}; | ||
| 41 | const [volume, setVolume] = React.useState(_volume && parseInt(_volume, 10)); | ||
| 42 | const [dragging, setDragging] = React.useState(false); | ||
| 43 | |||
| 44 | /** | ||
| 45 | * Reset the widget state. | ||
| 46 | */ | ||
| 47 | const resetWidget = () => { | ||
| 48 | setState(undefined); | ||
| 49 | setLoading(false); | ||
| 50 | }; | ||
| 51 | |||
| 52 | /** | ||
| 53 | * Fetch the current sound settings. | ||
| 54 | */ | ||
| 55 | const getSound = React.useCallback(async () => { | ||
| 56 | if (!visible) return; | ||
| 57 | const output = await Utils.cachedRun( | ||
| 58 | `osascript -e 'set v to get volume settings' -e 'output volume of v & output muted of v'`, | ||
| 59 | refresh, | ||
| 60 | ); | ||
| 61 | const parts = Utils.cleanupOutput(output).split(","); | ||
| 62 | setState({ | ||
| 63 | volume: parts[0], | ||
| 64 | muted: parts[1], | ||
| 65 | }); | ||
| 66 | setLoading(false); | ||
| 67 | }, [visible, refresh]); | ||
| 68 | |||
| 69 | // Use server socket to listen for sound updates. | ||
| 70 | useServerSocket("sound", visible, getSound, resetWidget, setLoading); | ||
| 71 | // Refresh the widget at the specified interval. | ||
| 72 | useWidgetRefresh(visible, getSound, refresh); | ||
| 73 | |||
| 74 | // Update the sound settings when dragging ends. | ||
| 75 | React.useEffect(() => { | ||
| 76 | if (!dragging) setSound(volume); | ||
| 77 | }, [dragging, volume]); | ||
| 78 | |||
| 79 | // Update the volume state when the fetched volume changes. | ||
| 80 | React.useEffect(() => { | ||
| 81 | setVolume((currentVolume) => { | ||
| 82 | if (_volume && currentVolume !== parseInt(_volume, 10)) { | ||
| 83 | return parseInt(_volume, 10); | ||
| 84 | } | ||
| 85 | return currentVolume; | ||
| 86 | }); | ||
| 87 | }, [_volume]); | ||
| 88 | |||
| 89 | if (loading) return <DataWidgetLoader.Widget className="sound" />; | ||
| 90 | if (!state || volume === undefined) return null; | ||
| 91 | |||
| 92 | const { muted } = state; | ||
| 93 | if (_volume === "missing value" || muted === "missing value") return null; | ||
| 94 | |||
| 95 | let Icon = Icons.VolumeHigh; | ||
| 96 | if (volume < 50) Icon = Icons.VolumeLow; | ||
| 97 | if (volume < 20) Icon = Icons.NoVolume; | ||
| 98 | if (muted === "true" || !volume) Icon = Icons.VolumeMuted; | ||
| 99 | |||
| 100 | /** | ||
| 101 | * Handle volume change event. | ||
| 102 | * @param {React.ChangeEvent<HTMLInputElement>} e - The change event. | ||
| 103 | */ | ||
| 104 | const onChange = (e) => { | ||
| 105 | const value = parseInt(e.target.value, 10); | ||
| 106 | setVolume(value); | ||
| 107 | }; | ||
| 108 | |||
| 109 | const onMouseDown = () => setDragging(true); | ||
| 110 | const onMouseUp = () => setDragging(false); | ||
| 111 | |||
| 112 | const formattedVolume = `${volume.toString().padStart(2, "0")}%`; | ||
| 113 | |||
| 114 | const classes = Utils.classNames("sound", { | ||
| 115 | "sound--dragging": dragging, | ||
| 116 | }); | ||
| 117 | |||
| 118 | return ( | ||
| 119 | <DataWidget.Widget classes={classes} disableSlider> | ||
| 120 | <div className="sound__display"> | ||
| 121 | {showIcon && ( | ||
| 122 | <SuspenseIcon> | ||
| 123 | <Icon /> | ||
| 124 | </SuspenseIcon> | ||
| 125 | )} | ||
| 126 | <span className="sound__value">{formattedVolume}</span> | ||
| 127 | </div> | ||
| 128 | <div className="sound__slider-container"> | ||
| 129 | <input | ||
| 130 | type="range" | ||
| 131 | min="0" | ||
| 132 | max="100" | ||
| 133 | step="1" | ||
| 134 | value={volume} | ||
| 135 | className="sound__slider" | ||
| 136 | onMouseDown={onMouseDown} | ||
| 137 | onMouseUp={onMouseUp} | ||
| 138 | onChange={onChange} | ||
| 139 | /> | ||
| 140 | </div> | ||
| 141 | </DataWidget.Widget> | ||
| 142 | ); | ||
| 143 | }); | ||
| 144 | |||
| 145 | Widget.displayName = "Sound"; | ||
| 146 | |||
| 147 | /** | ||
| 148 | * Set the system volume. | ||
| 149 | * @param {number} volume - The volume to set. | ||
| 150 | */ | ||
| 151 | function setSound(volume) { | ||
| 152 | if (volume === undefined) return; | ||
| 153 | Uebersicht.run(`osascript -e 'set volume output volume ${volume}'`); | ||
| 154 | } | ||
diff --git a/users/ryan/modules/simple-bar/simple-bar-source/lib/components/data/specter.jsx b/users/ryan/modules/simple-bar/simple-bar-source/lib/components/data/specter.jsx new file mode 100755 index 0000000..66b0850 --- /dev/null +++ b/users/ryan/modules/simple-bar/simple-bar-source/lib/components/data/specter.jsx | |||
| @@ -0,0 +1,24 @@ | |||
| 1 | import * as Uebersicht from "uebersicht"; | ||
| 2 | |||
| 3 | export { specterStyles as styles } from "../../styles/components/data/specter"; | ||
| 4 | |||
| 5 | const { React } = Uebersicht; | ||
| 6 | |||
| 7 | /** | ||
| 8 | * Specter widget component | ||
| 9 | * | ||
| 10 | * This component renders a div with six span elements inside it. These are animated in CSS. | ||
| 11 | * | ||
| 12 | * @returns {JSX.Element} The rendered widget component | ||
| 13 | */ | ||
| 14 | export const Widget = React.memo(() => { | ||
| 15 | return ( | ||
| 16 | <div className="specter"> | ||
| 17 | {[...new Array(6)].map((_, i) => ( | ||
| 18 | <span key={i} /> | ||
| 19 | ))} | ||
| 20 | </div> | ||
| 21 | ); | ||
| 22 | }); | ||
| 23 | |||
| 24 | Widget.displayName = "Specter"; | ||
diff --git a/users/ryan/modules/simple-bar/simple-bar-source/lib/components/data/spotify.jsx b/users/ryan/modules/simple-bar/simple-bar-source/lib/components/data/spotify.jsx new file mode 100755 index 0000000..bc9f6e6 --- /dev/null +++ b/users/ryan/modules/simple-bar/simple-bar-source/lib/components/data/spotify.jsx | |||
| @@ -0,0 +1,189 @@ | |||
| 1 | import * as Uebersicht from "uebersicht"; | ||
| 2 | import * as DataWidget from "./data-widget.jsx"; | ||
| 3 | import * as DataWidgetLoader from "./data-widget-loader.jsx"; | ||
| 4 | import useWidgetRefresh from "../../hooks/use-widget-refresh"; | ||
| 5 | import useServerSocket from "../../hooks/use-server-socket"; | ||
| 6 | import { useSimpleBarContext } from "../simple-bar-context.jsx"; | ||
| 7 | import * as Icons from "../icons/icons.jsx"; | ||
| 8 | import * as Utils from "../../utils"; | ||
| 9 | |||
| 10 | export { spotifyStyles as styles } from "../../styles/components/data/spotify"; | ||
| 11 | |||
| 12 | const { React } = Uebersicht; | ||
| 13 | |||
| 14 | const DEFAULT_REFRESH_FREQUENCY = 10000; | ||
| 15 | |||
| 16 | /** | ||
| 17 | * Spotify widget component. | ||
| 18 | * @returns {JSX.Element|null} The Spotify widget. | ||
| 19 | */ | ||
| 20 | export const Widget = React.memo(() => { | ||
| 21 | const { displayIndex, settings } = useSimpleBarContext(); | ||
| 22 | const { widgets, spotifyWidgetOptions } = settings; | ||
| 23 | const { spotifyWidget } = widgets; | ||
| 24 | const { | ||
| 25 | refreshFrequency, | ||
| 26 | showSpecter, | ||
| 27 | showOnDisplay, | ||
| 28 | showIcon, | ||
| 29 | showSpotifyMetadata, | ||
| 30 | } = spotifyWidgetOptions; | ||
| 31 | |||
| 32 | // Determine the refresh frequency for the widget | ||
| 33 | const refresh = React.useMemo( | ||
| 34 | () => | ||
| 35 | Utils.getRefreshFrequency(refreshFrequency, DEFAULT_REFRESH_FREQUENCY), | ||
| 36 | [refreshFrequency], | ||
| 37 | ); | ||
| 38 | |||
| 39 | // Determine if the widget should be visible | ||
| 40 | const visible = | ||
| 41 | Utils.isVisibleOnDisplay(displayIndex, showOnDisplay) && spotifyWidget; | ||
| 42 | |||
| 43 | const [state, setState] = React.useState(); | ||
| 44 | const [loading, setLoading] = React.useState(visible); | ||
| 45 | const [isSpotifyActive, setIsSpotifyActive] = React.useState(false); | ||
| 46 | |||
| 47 | /** | ||
| 48 | * Resets the widget state. | ||
| 49 | */ | ||
| 50 | const resetWidget = () => { | ||
| 51 | setState(undefined); | ||
| 52 | setLoading(false); | ||
| 53 | setIsSpotifyActive(false); | ||
| 54 | }; | ||
| 55 | |||
| 56 | /** | ||
| 57 | * Fetches the current Spotify state. | ||
| 58 | */ | ||
| 59 | const getSpotify = React.useCallback(async () => { | ||
| 60 | if (!visible) return; | ||
| 61 | const isRunning = await Utils.cachedRun( | ||
| 62 | `pgrep -fq '[S]potify Helper' && echo "true" || echo "false"`, | ||
| 63 | refresh, | ||
| 64 | ); | ||
| 65 | if (Utils.cleanupOutput(isRunning) === "false") { | ||
| 66 | setLoading(false); | ||
| 67 | setIsSpotifyActive(false); | ||
| 68 | setState({ | ||
| 69 | playerState: "", | ||
| 70 | trackName: "", | ||
| 71 | artistName: "", | ||
| 72 | }); | ||
| 73 | return; | ||
| 74 | } | ||
| 75 | // if showSpotifyMetadata is enabled, retrieves all information | ||
| 76 | if (showSpotifyMetadata) { | ||
| 77 | const output = await Utils.cachedRun( | ||
| 78 | `osascript -e 'tell application "Spotify"' -e 'set output to (player state as string) & "|" & (name of current track as string) & "|" & (artist of current track as string)' -e 'end tell' 2>/dev/null || echo "stopped||"`, | ||
| 79 | refresh, | ||
| 80 | ); | ||
| 81 | const [playerState, trackName, artistName] = | ||
| 82 | Utils.cleanupOutput(output).split("|"); | ||
| 83 | setState({ | ||
| 84 | playerState, | ||
| 85 | trackName, | ||
| 86 | artistName, | ||
| 87 | }); | ||
| 88 | // else, only playerState | ||
| 89 | } else { | ||
| 90 | const playerState = await Utils.cachedRun( | ||
| 91 | `osascript -e 'tell application "Spotify" to player state as string' 2>/dev/null || echo "stopped"`, | ||
| 92 | refresh, | ||
| 93 | ); | ||
| 94 | setState({ | ||
| 95 | playerState: Utils.cleanupOutput(playerState), | ||
| 96 | trackName: "", | ||
| 97 | artistName: "", | ||
| 98 | }); | ||
| 99 | } | ||
| 100 | |||
| 101 | setIsSpotifyActive(true); | ||
| 102 | setLoading(false); | ||
| 103 | }, [visible, showSpotifyMetadata, refresh]); | ||
| 104 | |||
| 105 | // Set up server socket and widget refresh hooks | ||
| 106 | useServerSocket("spotify", visible, getSpotify, resetWidget, setLoading); | ||
| 107 | useWidgetRefresh(visible, getSpotify, refresh); | ||
| 108 | |||
| 109 | if (loading) return <DataWidgetLoader.Widget className="spotify" />; | ||
| 110 | if (!state || !isSpotifyActive) return null; | ||
| 111 | const { playerState, trackName, artistName } = state; | ||
| 112 | |||
| 113 | if (!trackName.length && showSpotifyMetadata) return null; | ||
| 114 | |||
| 115 | const label = artistName.length ? `${trackName} - ${artistName}` : trackName; | ||
| 116 | const isPlaying = playerState === "playing"; | ||
| 117 | const Icon = getIcon(playerState); | ||
| 118 | |||
| 119 | /** | ||
| 120 | * Handles click event to toggle play/pause. | ||
| 121 | * @param {React.MouseEvent} e - The click event. | ||
| 122 | */ | ||
| 123 | const onClick = (e) => { | ||
| 124 | Utils.clickEffect(e); | ||
| 125 | togglePlay(!isPlaying); | ||
| 126 | getSpotify(); | ||
| 127 | }; | ||
| 128 | |||
| 129 | /** | ||
| 130 | * Handles right-click event to skip to the next track. | ||
| 131 | * @param {React.MouseEvent} e - The right-click event. | ||
| 132 | */ | ||
| 133 | const onRightClick = (e) => { | ||
| 134 | Utils.clickEffect(e); | ||
| 135 | Uebersicht.run(`osascript -e 'tell application "Spotify" to Next Track'`); | ||
| 136 | getSpotify(); | ||
| 137 | }; | ||
| 138 | |||
| 139 | /** | ||
| 140 | * Handles middle-click event to open Spotify. | ||
| 141 | * @param {React.MouseEvent} e - The middle-click event. | ||
| 142 | */ | ||
| 143 | const onMiddleClick = (e) => { | ||
| 144 | Utils.clickEffect(e); | ||
| 145 | Uebersicht.run(`open -a 'Spotify'`); | ||
| 146 | getSpotify(); | ||
| 147 | }; | ||
| 148 | |||
| 149 | const classes = Utils.classNames("spotify", { | ||
| 150 | "spotify--hidden-metadata": !showSpotifyMetadata, | ||
| 151 | "spotify--playing": isPlaying, | ||
| 152 | }); | ||
| 153 | |||
| 154 | return ( | ||
| 155 | <DataWidget.Widget | ||
| 156 | classes={classes} | ||
| 157 | Icon={showIcon ? Icon : null} | ||
| 158 | onClick={onClick} | ||
| 159 | onRightClick={onRightClick} | ||
| 160 | onMiddleClick={onMiddleClick} | ||
| 161 | showSpecter={showSpecter && isPlaying} | ||
| 162 | disableSlider={!showSpotifyMetadata} | ||
| 163 | > | ||
| 164 | {showSpotifyMetadata && label} | ||
| 165 | </DataWidget.Widget> | ||
| 166 | ); | ||
| 167 | }); | ||
| 168 | |||
| 169 | Widget.displayName = "Spotify"; | ||
| 170 | |||
| 171 | /** | ||
| 172 | * Toggles play/pause state of Spotify. | ||
| 173 | * @param {boolean} isPaused - Whether the player is paused. | ||
| 174 | */ | ||
| 175 | function togglePlay(isPaused) { | ||
| 176 | const state = isPaused ? "play" : "pause"; | ||
| 177 | Uebersicht.run(`osascript -e 'tell application "Spotify" to ${state}'`); | ||
| 178 | } | ||
| 179 | |||
| 180 | /** | ||
| 181 | * Gets the appropriate icon based on the player state. | ||
| 182 | * @param {string} playerState - The current state of the player. | ||
| 183 | * @returns {JSX.Element} The icon component. | ||
| 184 | */ | ||
| 185 | function getIcon(playerState) { | ||
| 186 | if (playerState === "stopped") return Icons.Stopped; | ||
| 187 | if (playerState === "playing") return Icons.Playing; | ||
| 188 | return Icons.Paused; | ||
| 189 | } | ||
diff --git a/users/ryan/modules/simple-bar/simple-bar-source/lib/components/data/stock.jsx b/users/ryan/modules/simple-bar/simple-bar-source/lib/components/data/stock.jsx new file mode 100755 index 0000000..4327f1c --- /dev/null +++ b/users/ryan/modules/simple-bar/simple-bar-source/lib/components/data/stock.jsx | |||
| @@ -0,0 +1,211 @@ | |||
| 1 | import * as Uebersicht from "uebersicht"; | ||
| 2 | import * as DataWidget from "./data-widget.jsx"; | ||
| 3 | import * as DataWidgetLoader from "./data-widget-loader.jsx"; | ||
| 4 | import * as Icons from "../icons/icons.jsx"; | ||
| 5 | import useWidgetRefresh from "../../hooks/use-widget-refresh"; | ||
| 6 | import useServerSocket from "../../hooks/use-server-socket"; | ||
| 7 | import { useSimpleBarContext } from "../simple-bar-context.jsx"; | ||
| 8 | import * as Utils from "../../utils"; | ||
| 9 | |||
| 10 | export { stockStyles as styles } from "../../styles/components/data/stock"; | ||
| 11 | |||
| 12 | const { React } = Uebersicht; | ||
| 13 | |||
| 14 | const DEFAULT_REFRESH_FREQUENCY = 15 * 60 * 1000; // 15 min | ||
| 15 | |||
| 16 | /** | ||
| 17 | * Stock widget component | ||
| 18 | * @returns {JSX.Element|null} The stock widget | ||
| 19 | */ | ||
| 20 | export const Widget = React.memo(() => { | ||
| 21 | const { displayIndex, settings, pushMissive } = useSimpleBarContext(); | ||
| 22 | const { widgets, stockWidgetOptions } = settings; | ||
| 23 | const { stockWidget } = widgets; | ||
| 24 | const { | ||
| 25 | refreshFrequency, | ||
| 26 | yahooFinanceApiKey, | ||
| 27 | symbols, | ||
| 28 | showSymbol, | ||
| 29 | showCurrency, | ||
| 30 | showMarketPrice, | ||
| 31 | showMarketChange, | ||
| 32 | showMarketPercent, | ||
| 33 | showColor, | ||
| 34 | showOnDisplay, | ||
| 35 | showIcon, | ||
| 36 | } = stockWidgetOptions; | ||
| 37 | |||
| 38 | // Determine the refresh frequency for the widget | ||
| 39 | const refresh = React.useMemo( | ||
| 40 | () => | ||
| 41 | Utils.getRefreshFrequency(refreshFrequency, DEFAULT_REFRESH_FREQUENCY), | ||
| 42 | [refreshFrequency], | ||
| 43 | ); | ||
| 44 | |||
| 45 | // Check if the widget should be visible on the current display | ||
| 46 | const visible = | ||
| 47 | Utils.isVisibleOnDisplay(displayIndex, showOnDisplay) && stockWidget; | ||
| 48 | |||
| 49 | const ref = React.useRef(); | ||
| 50 | |||
| 51 | // Memoize cleanedUpSymbols to prevent recreation on every render | ||
| 52 | const cleanedUpSymbols = React.useMemo( | ||
| 53 | () => symbols.replace(/ /g, ""), | ||
| 54 | [symbols], | ||
| 55 | ); | ||
| 56 | |||
| 57 | // Memoize enumeratedSymbols to prevent recreation on every render | ||
| 58 | const enumeratedSymbols = React.useMemo( | ||
| 59 | () => cleanedUpSymbols.replace(/ /g, "").split(","), | ||
| 60 | [cleanedUpSymbols], | ||
| 61 | ); | ||
| 62 | |||
| 63 | const [state, setState] = React.useState(); | ||
| 64 | const [loading, setLoading] = React.useState(visible); | ||
| 65 | |||
| 66 | /** | ||
| 67 | * Reset the widget state | ||
| 68 | */ | ||
| 69 | const resetWidget = () => { | ||
| 70 | setState(undefined); | ||
| 71 | setLoading(false); | ||
| 72 | }; | ||
| 73 | |||
| 74 | /** | ||
| 75 | * Fetch stock data from Yahoo Finance API | ||
| 76 | */ | ||
| 77 | const getStocks = React.useCallback(async () => { | ||
| 78 | if (!visible) return; | ||
| 79 | const response = await fetch( | ||
| 80 | `https://yfapi.net/v6/finance/quote?symbols=${cleanedUpSymbols}`, | ||
| 81 | { | ||
| 82 | headers: new Headers({ "x-api-key": yahooFinanceApiKey }), | ||
| 83 | }, | ||
| 84 | ); | ||
| 85 | if (response.status === 429) { | ||
| 86 | // Exceeded daily quota | ||
| 87 | } else if (response.status === 403) { | ||
| 88 | Utils.notification("Invalid Yahoo Finance API key", pushMissive); | ||
| 89 | } else if (response.status === 200) { | ||
| 90 | const result = await response.json(); | ||
| 91 | const symbolQuotes = result.quoteResponse.result; | ||
| 92 | |||
| 93 | if (symbolQuotes.length !== enumeratedSymbols.length) { | ||
| 94 | // One or more symbols is invalid | ||
| 95 | const receivedSymbols = symbolQuotes.map((s) => s.symbol.toLowerCase()); | ||
| 96 | const invalidSymbols = enumeratedSymbols.filter( | ||
| 97 | (s) => !receivedSymbols.includes(s), | ||
| 98 | ); | ||
| 99 | Utils.notification("Invalid stock symbol(s): " + invalidSymbols); | ||
| 100 | } else { | ||
| 101 | setState({ symbolQuotes }); | ||
| 102 | } | ||
| 103 | } else { | ||
| 104 | // eslint-disable-next-line no-console | ||
| 105 | console.error("Unexpected response from Yahoo Finance API: ", response); | ||
| 106 | } | ||
| 107 | |||
| 108 | setLoading(false); | ||
| 109 | }, [ | ||
| 110 | visible, | ||
| 111 | cleanedUpSymbols, | ||
| 112 | yahooFinanceApiKey, | ||
| 113 | pushMissive, | ||
| 114 | enumeratedSymbols, | ||
| 115 | ]); | ||
| 116 | |||
| 117 | // Use server socket to fetch stock data | ||
| 118 | useServerSocket("stock", visible, getStocks, resetWidget, setLoading); | ||
| 119 | // Refresh widget data periodically | ||
| 120 | useWidgetRefresh(visible, getStocks, refresh); | ||
| 121 | |||
| 122 | /** | ||
| 123 | * Refresh stock data on user interaction | ||
| 124 | * @param {Event} e - The event object | ||
| 125 | */ | ||
| 126 | const refreshStocks = (e) => { | ||
| 127 | Utils.clickEffect(e); | ||
| 128 | setLoading(true); | ||
| 129 | getStocks(); | ||
| 130 | }; | ||
| 131 | |||
| 132 | if (loading) return <DataWidgetLoader.Widget className="stock" />; | ||
| 133 | if (!state || !state.symbolQuotes) return null; | ||
| 134 | |||
| 135 | return enumeratedSymbols.map((symbolName, i) => { | ||
| 136 | const symbolQuote = state.symbolQuotes[i]; | ||
| 137 | const symbol = symbolQuote.symbol; | ||
| 138 | const currencySymbol = getCurrencySymbol(symbolQuote.currency); | ||
| 139 | const marketPrice = Number(symbolQuote.regularMarketPrice).toFixed(2); | ||
| 140 | const marketChange = Number(symbolQuote.regularMarketChange).toFixed(2); | ||
| 141 | const marketPercentChange = Number( | ||
| 142 | symbolQuote.regularMarketChangePercent, | ||
| 143 | ).toFixed(2); | ||
| 144 | const stockUp = !marketChange.startsWith("-"); | ||
| 145 | |||
| 146 | return ( | ||
| 147 | <DataWidget.Widget | ||
| 148 | key={symbolName} | ||
| 149 | classes="stock" | ||
| 150 | ref={ref} | ||
| 151 | Icon={showIcon ? (stockUp ? Icons.UpArrow : Icons.DownArrow) : null} | ||
| 152 | href={`https://finance.yahoo.com/quote/${symbolName}`} | ||
| 153 | onClick={openStock} | ||
| 154 | onRightClick={refreshStocks} | ||
| 155 | > | ||
| 156 | <span> | ||
| 157 | {showSymbol && symbol} | ||
| 158 | {(showCurrency || showMarketPrice) && showSymbol && ( | ||
| 159 | <span> </span> | ||
| 160 | )} | ||
| 161 | {showCurrency && currencySymbol} | ||
| 162 | {showMarketPrice && marketPrice} | ||
| 163 | </span> | ||
| 164 | <span className={showColor ? (stockUp ? "stockUp" : "stockDown") : ""}> | ||
| 165 | {(showMarketChange || showMarketPercent) && <span> </span>} | ||
| 166 | {showMarketChange && formatPriceChange(marketChange)} | ||
| 167 | {showMarketPercent && | ||
| 168 | showMarketChange && | ||
| 169 | ` (${formatPriceChange(marketPercentChange)}%)`} | ||
| 170 | {showMarketPercent && | ||
| 171 | !showMarketChange && | ||
| 172 | `${formatPriceChange(marketPercentChange)}%`} | ||
| 173 | </span> | ||
| 174 | </DataWidget.Widget> | ||
| 175 | ); | ||
| 176 | }); | ||
| 177 | }); | ||
| 178 | |||
| 179 | Widget.displayName = "Stock"; | ||
| 180 | |||
| 181 | /** | ||
| 182 | * Handle click event to open stock details | ||
| 183 | * @param {Event} e - The event object | ||
| 184 | */ | ||
| 185 | function openStock(e) { | ||
| 186 | Utils.clickEffect(e); | ||
| 187 | } | ||
| 188 | |||
| 189 | /** | ||
| 190 | * Get the currency symbol for a given currency code | ||
| 191 | * @param {string} currency - The currency code | ||
| 192 | * @returns {string} The currency symbol | ||
| 193 | */ | ||
| 194 | function getCurrencySymbol(currency) { | ||
| 195 | if (currency === "EUR") return "€"; | ||
| 196 | if (currency === "USD") return "$"; | ||
| 197 | if (currency === "GBP") return "£"; | ||
| 198 | if (currency === "CNY") return "Â¥"; | ||
| 199 | if (currency === "JPY") return "Â¥"; | ||
| 200 | return `${currency} `; | ||
| 201 | } | ||
| 202 | |||
| 203 | /** | ||
| 204 | * Format the price change with a '+' for positive changes | ||
| 205 | * @param {string} priceChange - The price change | ||
| 206 | * @returns {string} The formatted price change | ||
| 207 | */ | ||
| 208 | function formatPriceChange(priceChange) { | ||
| 209 | // Add a '+' for positive changes | ||
| 210 | return (priceChange.startsWith("-") ? "" : "+") + priceChange; | ||
| 211 | } | ||
diff --git a/users/ryan/modules/simple-bar/simple-bar-source/lib/components/data/time.jsx b/users/ryan/modules/simple-bar/simple-bar-source/lib/components/data/time.jsx new file mode 100755 index 0000000..6890ffb --- /dev/null +++ b/users/ryan/modules/simple-bar/simple-bar-source/lib/components/data/time.jsx | |||
| @@ -0,0 +1,144 @@ | |||
| 1 | import * as Uebersicht from "uebersicht"; | ||
| 2 | import * as DataWidget from "./data-widget.jsx"; | ||
| 3 | import * as DataWidgetLoader from "./data-widget-loader.jsx"; | ||
| 4 | import useWidgetRefresh from "../../hooks/use-widget-refresh"; | ||
| 5 | import useServerSocket from "../../hooks/use-server-socket"; | ||
| 6 | import { useSimpleBarContext } from "../simple-bar-context.jsx"; | ||
| 7 | import * as Utils from "../../utils"; | ||
| 8 | |||
| 9 | export { timeStyles as styles } from "../../styles/components/data/time"; | ||
| 10 | |||
| 11 | const { React } = Uebersicht; | ||
| 12 | |||
| 13 | const DEFAULT_REFRESH_FREQUENCY = 1000; | ||
| 14 | |||
| 15 | /** | ||
| 16 | * Time widget component. | ||
| 17 | * @returns {JSX.Element|null} The rendered widget. | ||
| 18 | */ | ||
| 19 | export const Widget = React.memo(() => { | ||
| 20 | const { displayIndex, settings } = useSimpleBarContext(); | ||
| 21 | const { widgets, timeWidgetOptions } = settings; | ||
| 22 | const { timeWidget } = widgets; | ||
| 23 | const { | ||
| 24 | refreshFrequency, | ||
| 25 | hour12, | ||
| 26 | dayProgress, | ||
| 27 | showSeconds, | ||
| 28 | showOnDisplay, | ||
| 29 | showIcon, | ||
| 30 | } = timeWidgetOptions; | ||
| 31 | |||
| 32 | // Determine if the widget should be visible on the current display | ||
| 33 | const visible = | ||
| 34 | Utils.isVisibleOnDisplay(displayIndex, showOnDisplay) && timeWidget; | ||
| 35 | |||
| 36 | // Calculate the refresh frequency for the widget | ||
| 37 | const refresh = React.useMemo( | ||
| 38 | () => | ||
| 39 | Utils.getRefreshFrequency(refreshFrequency, DEFAULT_REFRESH_FREQUENCY), | ||
| 40 | [refreshFrequency], | ||
| 41 | ); | ||
| 42 | |||
| 43 | const [state, setState] = React.useState(); | ||
| 44 | const [loading, setLoading] = React.useState(visible); | ||
| 45 | |||
| 46 | // Options for formatting the time string | ||
| 47 | const options = React.useMemo( | ||
| 48 | () => ({ | ||
| 49 | hour: "numeric", | ||
| 50 | minute: "numeric", | ||
| 51 | second: showSeconds ? "numeric" : undefined, | ||
| 52 | hour12, | ||
| 53 | }), | ||
| 54 | [hour12, showSeconds], | ||
| 55 | ); | ||
| 56 | |||
| 57 | /** | ||
| 58 | * Resets the widget state. | ||
| 59 | */ | ||
| 60 | const resetWidget = () => { | ||
| 61 | setState(undefined); | ||
| 62 | setLoading(false); | ||
| 63 | }; | ||
| 64 | |||
| 65 | /** | ||
| 66 | * Fetches the current time and updates the widget state. | ||
| 67 | */ | ||
| 68 | const getTime = React.useCallback(() => { | ||
| 69 | if (!visible) return; | ||
| 70 | const time = new Date().toLocaleString("en-UK", options); | ||
| 71 | setState({ time }); | ||
| 72 | setLoading(false); | ||
| 73 | }, [visible, options]); | ||
| 74 | |||
| 75 | // Use server socket to get time updates | ||
| 76 | useServerSocket("time", visible, getTime, resetWidget, setLoading); | ||
| 77 | // Refresh the widget at the specified interval | ||
| 78 | useWidgetRefresh(visible, getTime, refresh); | ||
| 79 | |||
| 80 | if (loading) return <DataWidgetLoader.Widget className="time" />; | ||
| 81 | if (!state) return null; | ||
| 82 | const { time } = state; | ||
| 83 | |||
| 84 | // Calculate the progress of the current day | ||
| 85 | const [dayStart, dayEnd] = [new Date(), new Date()]; | ||
| 86 | dayStart.setHours(0, 0, 0); | ||
| 87 | dayEnd.setHours(0, 0, 0); | ||
| 88 | dayEnd.setDate(dayEnd.getDate() + 1); | ||
| 89 | const range = dayEnd - dayStart; | ||
| 90 | const diff = Math.max(0, dayEnd - new Date()); | ||
| 91 | const fillerWidth = (100 - (100 * diff) / range) / 100; | ||
| 92 | |||
| 93 | /** | ||
| 94 | * Icon component for the time widget. | ||
| 95 | * @returns {JSX.Element} The rendered icon. | ||
| 96 | */ | ||
| 97 | const TimeIcon = () => { | ||
| 98 | return <Icon time={time} />; | ||
| 99 | }; | ||
| 100 | |||
| 101 | return ( | ||
| 102 | <DataWidget.Widget | ||
| 103 | classes="time" | ||
| 104 | Icon={showIcon ? TimeIcon : null} | ||
| 105 | disableSlider | ||
| 106 | > | ||
| 107 | {time} | ||
| 108 | {dayProgress && ( | ||
| 109 | <div | ||
| 110 | className="time__filler" | ||
| 111 | style={{ transform: `scaleX(${fillerWidth})` }} | ||
| 112 | /> | ||
| 113 | )} | ||
| 114 | </DataWidget.Widget> | ||
| 115 | ); | ||
| 116 | }); | ||
| 117 | |||
| 118 | Widget.displayName = "Time"; | ||
| 119 | |||
| 120 | /** | ||
| 121 | * Icon component for displaying the time as a clock. | ||
| 122 | * @param {Object} props - The component props. | ||
| 123 | * @param {string} props.time - The current time string. | ||
| 124 | * @returns {JSX.Element} The rendered icon. | ||
| 125 | */ | ||
| 126 | function Icon({ time }) { | ||
| 127 | const [hours, minutes] = time.split(":"); | ||
| 128 | |||
| 129 | const hoursInDegree = ((parseInt(hours, 10) % 12) / 12) * 360; | ||
| 130 | const minutesInDegree = (parseInt(minutes, 10) / 60) * 360; | ||
| 131 | |||
| 132 | return ( | ||
| 133 | <div className="time__icon"> | ||
| 134 | <div | ||
| 135 | className="time__hours" | ||
| 136 | style={{ transform: `rotate(${hoursInDegree}deg)` }} | ||
| 137 | /> | ||
| 138 | <div | ||
| 139 | className="time__minutes" | ||
| 140 | style={{ transform: `rotate(${minutesInDegree}deg)` }} | ||
| 141 | /> | ||
| 142 | </div> | ||
| 143 | ); | ||
| 144 | } | ||
diff --git a/users/ryan/modules/simple-bar/simple-bar-source/lib/components/data/user-widgets.jsx b/users/ryan/modules/simple-bar/simple-bar-source/lib/components/data/user-widgets.jsx new file mode 100755 index 0000000..efa9876 --- /dev/null +++ b/users/ryan/modules/simple-bar/simple-bar-source/lib/components/data/user-widgets.jsx | |||
| @@ -0,0 +1,181 @@ | |||
| 1 | import * as Uebersicht from "uebersicht"; | ||
| 2 | import * as DataWidget from "./data-widget.jsx"; | ||
| 3 | import * as DataWidgetLoader from "./data-widget-loader.jsx"; | ||
| 4 | import * as Icons from "../icons/icons.jsx"; | ||
| 5 | import useWidgetRefresh from "../../hooks/use-widget-refresh"; | ||
| 6 | import useServerSocket from "../../hooks/use-server-socket"; | ||
| 7 | import { useSimpleBarContext } from "../simple-bar-context.jsx"; | ||
| 8 | import * as Settings from "../../settings"; | ||
| 9 | import * as Utils from "../../utils"; | ||
| 10 | |||
| 11 | const { React } = Uebersicht; | ||
| 12 | |||
| 13 | export default React.memo(UserWidgets); | ||
| 14 | |||
| 15 | /** | ||
| 16 | * UserWidgets component that renders a list of user-defined widgets. | ||
| 17 | * @returns {JSX.Element[]} Array of UserWidget components. | ||
| 18 | */ | ||
| 19 | function UserWidgets() { | ||
| 20 | const { settings } = useSimpleBarContext(); | ||
| 21 | const { userWidgetsList } = settings.userWidgets; | ||
| 22 | |||
| 23 | // Get the keys of the userWidgetsList object | ||
| 24 | const keys = Object.keys(userWidgetsList); | ||
| 25 | |||
| 26 | // Map over the keys and render a UserWidget for each key | ||
| 27 | return keys.map((key) => ( | ||
| 28 | <UserWidget key={key} index={key} widget={userWidgetsList[key]} /> | ||
| 29 | )); | ||
| 30 | } | ||
| 31 | |||
| 32 | UserWidgets.displayName = "UserWidgets"; | ||
| 33 | |||
| 34 | /** | ||
| 35 | * UserWidget component that renders an individual user-defined widget. | ||
| 36 | * @param {Object} props - The component props. | ||
| 37 | * @param {string} props.index - The index of the widget. | ||
| 38 | * @param {Object} props.widget - The widget configuration object. | ||
| 39 | * @returns {JSX.Element|null} The rendered UserWidget component or null if not visible. | ||
| 40 | */ | ||
| 41 | const UserWidget = React.memo(({ index, widget }) => { | ||
| 42 | const { displayIndex, settings } = useSimpleBarContext(); | ||
| 43 | const [state, setState] = React.useState(); | ||
| 44 | const [loading, setLoading] = React.useState(true); | ||
| 45 | const [isWidgetActive, setIsWidgetActive] = React.useState(true); | ||
| 46 | const { | ||
| 47 | icon, | ||
| 48 | backgroundColor, | ||
| 49 | output, | ||
| 50 | onClickAction, | ||
| 51 | onRightClickAction, | ||
| 52 | onMiddleClickAction, | ||
| 53 | refreshFrequency, | ||
| 54 | active, | ||
| 55 | noIcon, | ||
| 56 | hideWhenNoOutput = true, | ||
| 57 | showOnDisplay = "", | ||
| 58 | } = widget; | ||
| 59 | |||
| 60 | // Determine if the widget should be visible based on display settings and active status | ||
| 61 | const visible = | ||
| 62 | Utils.isVisibleOnDisplay(displayIndex, showOnDisplay) && active; | ||
| 63 | |||
| 64 | /** | ||
| 65 | * Resets the widget state and loading status. | ||
| 66 | */ | ||
| 67 | const resetWidget = () => { | ||
| 68 | setState(undefined); | ||
| 69 | setLoading(false); | ||
| 70 | setIsWidgetActive(true); | ||
| 71 | }; | ||
| 72 | |||
| 73 | /** | ||
| 74 | * Fetches the widget output and updates the state. | ||
| 75 | */ | ||
| 76 | const getUserWidget = React.useCallback(async () => { | ||
| 77 | if (!visible) return; | ||
| 78 | const widgetOutput = await Utils.cachedRun(output, refreshFrequency); | ||
| 79 | const cleanedOutput = Utils.cleanupOutput(widgetOutput); | ||
| 80 | |||
| 81 | // Hide widget if script returns empty output and hideWhenNoOutput is enabled | ||
| 82 | if ( | ||
| 83 | hideWhenNoOutput && | ||
| 84 | (!cleanedOutput.length || cleanedOutput.trim() === "") | ||
| 85 | ) { | ||
| 86 | setLoading(false); | ||
| 87 | setIsWidgetActive(false); | ||
| 88 | setState(undefined); | ||
| 89 | return; | ||
| 90 | } | ||
| 91 | |||
| 92 | setState(widgetOutput); | ||
| 93 | setIsWidgetActive(true); | ||
| 94 | setLoading(false); | ||
| 95 | }, [visible, output, hideWhenNoOutput, refreshFrequency]); | ||
| 96 | |||
| 97 | // Use server socket to listen for widget updates | ||
| 98 | useServerSocket( | ||
| 99 | "user-widget", | ||
| 100 | visible, | ||
| 101 | getUserWidget, | ||
| 102 | resetWidget, | ||
| 103 | setLoading, | ||
| 104 | index, | ||
| 105 | ); | ||
| 106 | |||
| 107 | // Refresh the widget at the specified frequency | ||
| 108 | useWidgetRefresh(visible, getUserWidget, refreshFrequency); | ||
| 109 | |||
| 110 | // Hide widget if not visible or if script indicates it should be inactive (only when hideWhenNoOutput is enabled) | ||
| 111 | if (!visible || (hideWhenNoOutput && !isWidgetActive)) return null; | ||
| 112 | |||
| 113 | const isCustomColor = !Settings.userWidgetColors.includes(backgroundColor); | ||
| 114 | |||
| 115 | const property = settings.global.widgetsBackgroundColorAsForeground | ||
| 116 | ? "color" | ||
| 117 | : "backgroundColor"; | ||
| 118 | |||
| 119 | const style = settings.global.noColorInData | ||
| 120 | ? undefined | ||
| 121 | : { | ||
| 122 | [property]: isCustomColor ? backgroundColor : `var(${backgroundColor})`, | ||
| 123 | }; | ||
| 124 | |||
| 125 | if (loading) return <DataWidgetLoader.Widget style={style} />; | ||
| 126 | |||
| 127 | const Icon = !noIcon ? Icons[icon] : null; | ||
| 128 | |||
| 129 | const hasOnClickAction = onClickAction?.trim().length > 0; | ||
| 130 | const hasRightClickAction = onRightClickAction?.trim().length > 0; | ||
| 131 | const hasMiddleClickAction = onMiddleClickAction?.trim().length > 0; | ||
| 132 | |||
| 133 | /** | ||
| 134 | * Handles the click event for the widget. | ||
| 135 | * @param {Event} e - The click event. | ||
| 136 | */ | ||
| 137 | const onClick = async (e) => { | ||
| 138 | Utils.clickEffect(e); | ||
| 139 | await Uebersicht.run(onClickAction); | ||
| 140 | getUserWidget(); | ||
| 141 | }; | ||
| 142 | |||
| 143 | /** | ||
| 144 | * Handles the right-click event for the widget. | ||
| 145 | * @param {Event} e - The right-click event. | ||
| 146 | */ | ||
| 147 | const onRightClick = async (e) => { | ||
| 148 | Utils.clickEffect(e); | ||
| 149 | await Uebersicht.run(onRightClickAction); | ||
| 150 | getUserWidget(); | ||
| 151 | }; | ||
| 152 | |||
| 153 | /** | ||
| 154 | * Handles the middle-click event for the widget. | ||
| 155 | * @param {Event} e - The middle-click event. | ||
| 156 | */ | ||
| 157 | const onMiddleClick = async (e) => { | ||
| 158 | Utils.clickEffect(e); | ||
| 159 | await Uebersicht.run(onMiddleClickAction); | ||
| 160 | getUserWidget(); | ||
| 161 | }; | ||
| 162 | |||
| 163 | const onClickProps = { | ||
| 164 | onClick: hasOnClickAction ? onClick : undefined, | ||
| 165 | onRightClick: hasRightClickAction ? onRightClick : undefined, | ||
| 166 | onMiddleClick: hasMiddleClickAction ? onMiddleClick : undefined, | ||
| 167 | }; | ||
| 168 | |||
| 169 | return ( | ||
| 170 | <DataWidget.Widget | ||
| 171 | classes={`user-widget user-widget--${index}`} | ||
| 172 | Icon={Icon} | ||
| 173 | style={style} | ||
| 174 | {...onClickProps} | ||
| 175 | > | ||
| 176 | {state} | ||
| 177 | </DataWidget.Widget> | ||
| 178 | ); | ||
| 179 | }); | ||
| 180 | |||
| 181 | UserWidget.displayName = "UserWidget"; | ||
diff --git a/users/ryan/modules/simple-bar/simple-bar-source/lib/components/data/viscosity-vpn.jsx b/users/ryan/modules/simple-bar/simple-bar-source/lib/components/data/viscosity-vpn.jsx new file mode 100755 index 0000000..340c59b --- /dev/null +++ b/users/ryan/modules/simple-bar/simple-bar-source/lib/components/data/viscosity-vpn.jsx | |||
| @@ -0,0 +1,144 @@ | |||
| 1 | import * as Uebersicht from "uebersicht"; | ||
| 2 | import * as DataWidget from "./data-widget.jsx"; | ||
| 3 | import * as DataWidgetLoader from "./data-widget-loader.jsx"; | ||
| 4 | import * as Icons from "../icons/icons.jsx"; | ||
| 5 | import useWidgetRefresh from "../../hooks/use-widget-refresh"; | ||
| 6 | import useServerSocket from "../../hooks/use-server-socket"; | ||
| 7 | import { useSimpleBarContext } from "../simple-bar-context.jsx"; | ||
| 8 | import * as Utils from "../../utils"; | ||
| 9 | |||
| 10 | export { viscosityVPNStyles as styles } from "../../styles/components/data/viscosity-vpn"; | ||
| 11 | |||
| 12 | const { React } = Uebersicht; | ||
| 13 | |||
| 14 | const DEFAULT_REFRESH_FREQUENCY = 8000; | ||
| 15 | |||
| 16 | /** | ||
| 17 | * Viscosity VPN Widget component. | ||
| 18 | * @returns {JSX.Element|null} The Viscosity VPN widget. | ||
| 19 | */ | ||
| 20 | export const Widget = React.memo(() => { | ||
| 21 | const { displayIndex, settings, pushMissive } = useSimpleBarContext(); | ||
| 22 | const { widgets, vpnWidgetOptions } = settings; | ||
| 23 | const { vpnWidget } = widgets; | ||
| 24 | const { | ||
| 25 | refreshFrequency, | ||
| 26 | vpnConnectionName, | ||
| 27 | vpnShowConnectionName, | ||
| 28 | showOnDisplay, | ||
| 29 | showIcon, | ||
| 30 | } = vpnWidgetOptions; | ||
| 31 | |||
| 32 | // Determine the refresh frequency for the widget | ||
| 33 | const refresh = React.useMemo( | ||
| 34 | () => | ||
| 35 | Utils.getRefreshFrequency(refreshFrequency, DEFAULT_REFRESH_FREQUENCY), | ||
| 36 | [refreshFrequency], | ||
| 37 | ); | ||
| 38 | |||
| 39 | // Determine if the widget should be visible | ||
| 40 | const visible = | ||
| 41 | Utils.isVisibleOnDisplay(displayIndex, showOnDisplay) && vpnWidget; | ||
| 42 | |||
| 43 | const [state, setState] = React.useState(); | ||
| 44 | const [loading, setLoading] = React.useState(visible); | ||
| 45 | const [isViscosityActive, setIsViscosityActive] = React.useState(false); | ||
| 46 | |||
| 47 | /** | ||
| 48 | * Reset the widget state. | ||
| 49 | */ | ||
| 50 | const resetWidget = () => { | ||
| 51 | setState(undefined); | ||
| 52 | setLoading(false); | ||
| 53 | setIsViscosityActive(false); | ||
| 54 | }; | ||
| 55 | |||
| 56 | /** | ||
| 57 | * Fetch the current VPN status. | ||
| 58 | */ | ||
| 59 | const getVPN = React.useCallback(async () => { | ||
| 60 | if (!visible) return; | ||
| 61 | const isRunning = await Utils.cachedRun( | ||
| 62 | `pgrep -xq 'Viscosity' && echo "true" || echo "false"`, | ||
| 63 | refresh, | ||
| 64 | ); | ||
| 65 | if (Utils.cleanupOutput(isRunning) === "false") { | ||
| 66 | setLoading(false); | ||
| 67 | setIsViscosityActive(false); | ||
| 68 | return; | ||
| 69 | } | ||
| 70 | const status = await Utils.cachedRun( | ||
| 71 | `osascript -e "tell application \\"Viscosity\\" to return state of the first connection where name is equal to \\"${vpnConnectionName}\\"" 2>/dev/null`, | ||
| 72 | refresh, | ||
| 73 | ); | ||
| 74 | if (!status.length) return; | ||
| 75 | setState({ status: Utils.cleanupOutput(status) }); | ||
| 76 | setIsViscosityActive(true); | ||
| 77 | setLoading(false); | ||
| 78 | }, [visible, vpnConnectionName, refresh]); | ||
| 79 | |||
| 80 | // Use server socket to listen for VPN status updates | ||
| 81 | useServerSocket("viscosity-vpn", visible, getVPN, resetWidget, setLoading); | ||
| 82 | // Refresh the widget at the specified interval | ||
| 83 | useWidgetRefresh(visible, getVPN, refresh); | ||
| 84 | |||
| 85 | if (loading) return <DataWidgetLoader.Widget className="viscosity-vpn" />; | ||
| 86 | if (!state || !vpnConnectionName.length || !isViscosityActive) return null; | ||
| 87 | |||
| 88 | const { status } = state; | ||
| 89 | const isConnected = status === "Connected"; | ||
| 90 | |||
| 91 | const classes = Utils.classNames("viscosity-vpn", { | ||
| 92 | "viscosity-vpn--disconnected": !isConnected, | ||
| 93 | }); | ||
| 94 | |||
| 95 | const Icon = isConnected ? Icons.VPN : Icons.VPNOff; | ||
| 96 | |||
| 97 | /** | ||
| 98 | * Handle click event to toggle VPN connection. | ||
| 99 | * @param {Event} e - The click event. | ||
| 100 | */ | ||
| 101 | const clicked = (e) => { | ||
| 102 | Utils.clickEffect(e); | ||
| 103 | toggleVPN(isConnected, vpnConnectionName, pushMissive); | ||
| 104 | setTimeout(getVPN, refreshFrequency / 2); | ||
| 105 | }; | ||
| 106 | |||
| 107 | return ( | ||
| 108 | <DataWidget.Widget | ||
| 109 | classes={classes} | ||
| 110 | Icon={showIcon ? Icon : null} | ||
| 111 | onClick={clicked} | ||
| 112 | > | ||
| 113 | {vpnShowConnectionName ? vpnConnectionName : status} | ||
| 114 | </DataWidget.Widget> | ||
| 115 | ); | ||
| 116 | }); | ||
| 117 | |||
| 118 | Widget.displayName = "ViscosityVPN"; | ||
| 119 | |||
| 120 | /** | ||
| 121 | * Toggle the VPN connection. | ||
| 122 | * @param {boolean} isConnected - Whether the VPN is currently connected. | ||
| 123 | * @param {string} vpnConnectionName - The name of the VPN connection. | ||
| 124 | * @param {function} pushMissive - Function to push notifications. | ||
| 125 | */ | ||
| 126 | function toggleVPN(isConnected, vpnConnectionName, pushMissive) { | ||
| 127 | if (isConnected) { | ||
| 128 | Uebersicht.run( | ||
| 129 | `osascript -e 'tell application "Viscosity" to disconnect "${vpnConnectionName}"'`, | ||
| 130 | ); | ||
| 131 | Utils.notification( | ||
| 132 | `Disabling Viscosity ${vpnConnectionName} network...`, | ||
| 133 | pushMissive, | ||
| 134 | ); | ||
| 135 | } else { | ||
| 136 | Uebersicht.run( | ||
| 137 | `osascript -e 'tell application "Viscosity" to connect "${vpnConnectionName}"'`, | ||
| 138 | ); | ||
| 139 | Utils.notification( | ||
| 140 | `Enabling Viscosity ${vpnConnectionName} network...`, | ||
| 141 | pushMissive, | ||
| 142 | ); | ||
| 143 | } | ||
| 144 | } | ||
diff --git a/users/ryan/modules/simple-bar/simple-bar-source/lib/components/data/weather.jsx b/users/ryan/modules/simple-bar/simple-bar-source/lib/components/data/weather.jsx new file mode 100755 index 0000000..946fda1 --- /dev/null +++ b/users/ryan/modules/simple-bar/simple-bar-source/lib/components/data/weather.jsx | |||
| @@ -0,0 +1,214 @@ | |||
| 1 | import * as Uebersicht from "uebersicht"; | ||
| 2 | import * as DataWidget from "./data-widget.jsx"; | ||
| 3 | import * as DataWidgetLoader from "./data-widget-loader.jsx"; | ||
| 4 | import * as Icons from "../icons/icons.jsx"; | ||
| 5 | import * as Utils from "../../utils"; | ||
| 6 | import useWidgetRefresh from "../../hooks/use-widget-refresh"; | ||
| 7 | import useServerSocket from "../../hooks/use-server-socket"; | ||
| 8 | import { useSimpleBarContext } from "../simple-bar-context.jsx"; | ||
| 9 | |||
| 10 | export { weatherStyles as styles } from "../../styles/components/data/weather"; | ||
| 11 | |||
| 12 | const { React } = Uebersicht; | ||
| 13 | |||
| 14 | const DEFAULT_REFRESH_FREQUENCY = 1000 * 60 * 30; // Default refresh frequency set to 30 minutes | ||
| 15 | |||
| 16 | /** | ||
| 17 | * Weather widget component | ||
| 18 | */ | ||
| 19 | export const Widget = React.memo(() => { | ||
| 20 | const { displayIndex, settings, pushMissive } = useSimpleBarContext(); | ||
| 21 | const { widgets, weatherWidgetOptions } = settings; | ||
| 22 | const { weatherWidget } = widgets; | ||
| 23 | const { | ||
| 24 | refreshFrequency, | ||
| 25 | customLocation, | ||
| 26 | unit, | ||
| 27 | hideLocation, | ||
| 28 | hideGradient, | ||
| 29 | showOnDisplay, | ||
| 30 | showIcon, | ||
| 31 | } = weatherWidgetOptions; | ||
| 32 | |||
| 33 | const refresh = React.useMemo( | ||
| 34 | () => | ||
| 35 | Utils.getRefreshFrequency(refreshFrequency, DEFAULT_REFRESH_FREQUENCY), | ||
| 36 | [refreshFrequency], | ||
| 37 | ); | ||
| 38 | |||
| 39 | const visible = | ||
| 40 | Utils.isVisibleOnDisplay(displayIndex, showOnDisplay) && weatherWidget; | ||
| 41 | |||
| 42 | const [state, setState] = React.useState(); | ||
| 43 | const [loading, setLoading] = React.useState(visible); | ||
| 44 | const location = React.useRef( | ||
| 45 | visible && customLocation.length ? customLocation : undefined, | ||
| 46 | ); | ||
| 47 | |||
| 48 | /** | ||
| 49 | * Resets the widget state and loading status | ||
| 50 | */ | ||
| 51 | const resetWidget = () => { | ||
| 52 | setState(undefined); | ||
| 53 | setLoading(false); | ||
| 54 | }; | ||
| 55 | |||
| 56 | /** | ||
| 57 | * Fetches weather data from wttr.in | ||
| 58 | */ | ||
| 59 | const getWeather = React.useCallback(async () => { | ||
| 60 | if (!visible) return; | ||
| 61 | if (!location.current) { | ||
| 62 | const position = await Promise.race([getPosition(), Utils.timeout(5000)]); | ||
| 63 | if (!position) await getWeather(); | ||
| 64 | const { city, zip } = position?.address || {}; | ||
| 65 | location.current = zip || city; | ||
| 66 | if (!location.current) return setLoading(false); | ||
| 67 | } | ||
| 68 | try { | ||
| 69 | const result = await fetch( | ||
| 70 | `https://wttr.in/${location.current}?format=j1`, | ||
| 71 | ); | ||
| 72 | const data = await result.json(); | ||
| 73 | setState({ location: location.current, data }); | ||
| 74 | } catch { | ||
| 75 | // eslint-disable-next-line no-console | ||
| 76 | console.error("Error while fetching weather") | ||
| 77 | } | ||
| 78 | setLoading(false); | ||
| 79 | }, [visible, location]); | ||
| 80 | |||
| 81 | useServerSocket("weather", visible, getWeather, resetWidget, setLoading); | ||
| 82 | useWidgetRefresh(visible, getWeather, refresh); | ||
| 83 | |||
| 84 | if (loading) return <DataWidgetLoader.Widget className="weather" />; | ||
| 85 | if (!state || !state.data.current_condition) return null; | ||
| 86 | |||
| 87 | const { | ||
| 88 | temp_C: tempC, | ||
| 89 | temp_F: tempF, | ||
| 90 | weatherDesc, | ||
| 91 | } = state.data.current_condition[0]; | ||
| 92 | const temperature = unit === "C" ? tempC : tempF; | ||
| 93 | const wttrUnitParam = unit === "C" ? "?m" : "?u"; | ||
| 94 | |||
| 95 | const description = weatherDesc[0].value; | ||
| 96 | |||
| 97 | const { astronomy } = state.data.weather[0]; | ||
| 98 | const sunriseData = astronomy[0].sunrise.replace(" AM", "").split(":"); | ||
| 99 | const sunsetData = astronomy[0].sunset.replace(" PM", "").split(":"); | ||
| 100 | |||
| 101 | const now = new Date(); | ||
| 102 | const nowIntervalStart = new Date(); | ||
| 103 | nowIntervalStart.setHours(nowIntervalStart.getHours() - 1); | ||
| 104 | const nowIntervalStop = new Date(); | ||
| 105 | nowIntervalStop.setHours(nowIntervalStop.getHours() + 1); | ||
| 106 | const sunriseTime = new Date(); | ||
| 107 | sunriseTime.setHours( | ||
| 108 | parseInt(sunriseData[0], 10), | ||
| 109 | parseInt(sunriseData[1], 10), | ||
| 110 | 0, | ||
| 111 | 0, | ||
| 112 | ); | ||
| 113 | const sunsetTime = new Date(); | ||
| 114 | sunsetTime.setHours( | ||
| 115 | parseInt(sunsetData[0], 10) + 12, | ||
| 116 | parseInt(sunsetData[1], 10), | ||
| 117 | 0, | ||
| 118 | 0, | ||
| 119 | ); | ||
| 120 | |||
| 121 | const atNight = sunriseTime >= now || now >= sunsetTime; | ||
| 122 | |||
| 123 | const Icon = getIcon(description, atNight); | ||
| 124 | const label = getLabel(state.location, temperature, unit, hideLocation); | ||
| 125 | |||
| 126 | const sunRising = | ||
| 127 | sunriseTime >= nowIntervalStart && sunriseTime <= nowIntervalStop; | ||
| 128 | const sunSetting = | ||
| 129 | sunsetTime >= nowIntervalStart && sunsetTime <= nowIntervalStop; | ||
| 130 | |||
| 131 | /** | ||
| 132 | * Handles right-click event to refresh weather data | ||
| 133 | * @param {Event} e - The event object | ||
| 134 | */ | ||
| 135 | const onRightClick = (e) => { | ||
| 136 | Utils.clickEffect(e); | ||
| 137 | setLoading(true); | ||
| 138 | getWeather(); | ||
| 139 | Utils.notification("Refreshing forecast from wttr.in...", pushMissive); | ||
| 140 | }; | ||
| 141 | |||
| 142 | const classes = Utils.classNames("weather", { | ||
| 143 | "weather--sunrise": sunRising, | ||
| 144 | "weather--sunset": sunSetting, | ||
| 145 | }); | ||
| 146 | |||
| 147 | return ( | ||
| 148 | <DataWidget.Widget | ||
| 149 | classes={classes} | ||
| 150 | Icon={showIcon ? Icon : null} | ||
| 151 | href={`https://wttr.in/${state.location}${wttrUnitParam}`} | ||
| 152 | onClick={(e) => openWeather(e, pushMissive)} | ||
| 153 | onRightClick={onRightClick} | ||
| 154 | disableSlider | ||
| 155 | > | ||
| 156 | {!hideGradient && <div className="weather__gradient" />} | ||
| 157 | {label} | ||
| 158 | </DataWidget.Widget> | ||
| 159 | ); | ||
| 160 | }); | ||
| 161 | |||
| 162 | Widget.displayName = "Weather"; | ||
| 163 | |||
| 164 | /** | ||
| 165 | * Returns the appropriate weather icon based on the description and time of day | ||
| 166 | * @param {string} description - Weather description | ||
| 167 | * @param {boolean} atNight - Whether it is currently night time | ||
| 168 | * @returns {JSX.Element} - The weather icon component | ||
| 169 | */ | ||
| 170 | function getIcon(description, atNight) { | ||
| 171 | if (description.includes("fog") || description.includes("mist")) { | ||
| 172 | return Icons.Fog; | ||
| 173 | } | ||
| 174 | if (description.includes("storm")) return Icons.Storm; | ||
| 175 | if (description.includes("snow")) return Icons.Snow; | ||
| 176 | if (description.includes("rain")) return Icons.Rain; | ||
| 177 | if (description.includes("cloud")) return Icons.Cloud; | ||
| 178 | if (atNight) return Icons.Moon; | ||
| 179 | return Icons.Sun; | ||
| 180 | } | ||
| 181 | |||
| 182 | /** | ||
| 183 | * Returns the label for the weather widget | ||
| 184 | * @param {string} location - The location name | ||
| 185 | * @param {string} temperature - The temperature value | ||
| 186 | * @param {string} unit - The temperature unit (C or F) | ||
| 187 | * @param {boolean} hideLocation - Whether to hide the location name | ||
| 188 | * @returns {string} - The label text | ||
| 189 | */ | ||
| 190 | function getLabel(location, temperature, unit, hideLocation) { | ||
| 191 | if (!location) return "Fetching..."; | ||
| 192 | if (hideLocation) return `${temperature}°${unit}`; | ||
| 193 | return `${location}, ${temperature}°${unit}`; | ||
| 194 | } | ||
| 195 | |||
| 196 | /** | ||
| 197 | * Opens the weather forecast in a new tab | ||
| 198 | * @param {Event} e - The event object | ||
| 199 | * @param {Function} pushMissive - Function to push notifications | ||
| 200 | */ | ||
| 201 | function openWeather(e, pushMissive) { | ||
| 202 | Utils.clickEffect(e); | ||
| 203 | Utils.notification("Opening forecast from wttr.in...", pushMissive); | ||
| 204 | } | ||
| 205 | |||
| 206 | /** | ||
| 207 | * Gets the current geographical position of the user | ||
| 208 | * @returns {Promise<GeolocationPosition>} - The position object | ||
| 209 | */ | ||
| 210 | async function getPosition() { | ||
| 211 | return new Promise((resolve) => | ||
| 212 | navigator.geolocation.getCurrentPosition(resolve), | ||
| 213 | ); | ||
| 214 | } | ||
diff --git a/users/ryan/modules/simple-bar/simple-bar-source/lib/components/data/wifi.jsx b/users/ryan/modules/simple-bar/simple-bar-source/lib/components/data/wifi.jsx new file mode 100755 index 0000000..c7bedcd --- /dev/null +++ b/users/ryan/modules/simple-bar/simple-bar-source/lib/components/data/wifi.jsx | |||
| @@ -0,0 +1,154 @@ | |||
| 1 | import * as Uebersicht from "uebersicht"; | ||
| 2 | import * as DataWidget from "./data-widget.jsx"; | ||
| 3 | import * as DataWidgetLoader from "./data-widget-loader.jsx"; | ||
| 4 | import * as Icons from "../icons/icons.jsx"; | ||
| 5 | import useWidgetRefresh from "../../hooks/use-widget-refresh"; | ||
| 6 | import useServerSocket from "../../hooks/use-server-socket"; | ||
| 7 | import { useSimpleBarContext } from "../simple-bar-context.jsx"; | ||
| 8 | import * as Utils from "../../utils"; | ||
| 9 | |||
| 10 | export { wifiStyles as styles } from "../../styles/components/data/wifi"; | ||
| 11 | |||
| 12 | const { React } = Uebersicht; | ||
| 13 | |||
| 14 | const DEFAULT_REFRESH_FREQUENCY = 20000; | ||
| 15 | |||
| 16 | /** | ||
| 17 | * Wifi widget component. | ||
| 18 | * @returns {JSX.Element|null} The Wifi widget. | ||
| 19 | */ | ||
| 20 | export const Widget = React.memo(() => { | ||
| 21 | const { displayIndex, settings, pushMissive } = useSimpleBarContext(); | ||
| 22 | const { widgets, networkWidgetOptions } = settings; | ||
| 23 | const { wifiWidget } = widgets; | ||
| 24 | const { | ||
| 25 | refreshFrequency, | ||
| 26 | hideWifiIfDisabled, | ||
| 27 | toggleWifiOnClick, | ||
| 28 | networkDevice, | ||
| 29 | hideNetworkName, | ||
| 30 | showOnDisplay, | ||
| 31 | showIcon, | ||
| 32 | } = networkWidgetOptions; | ||
| 33 | const visible = | ||
| 34 | Utils.isVisibleOnDisplay(displayIndex, showOnDisplay) && wifiWidget; | ||
| 35 | |||
| 36 | const refresh = React.useMemo( | ||
| 37 | () => | ||
| 38 | Utils.getRefreshFrequency(refreshFrequency, DEFAULT_REFRESH_FREQUENCY), | ||
| 39 | [refreshFrequency], | ||
| 40 | ); | ||
| 41 | |||
| 42 | const [state, setState] = React.useState(); | ||
| 43 | const [loading, setLoading] = React.useState(visible); | ||
| 44 | |||
| 45 | /** | ||
| 46 | * Resets the widget state. | ||
| 47 | */ | ||
| 48 | const resetWidget = () => { | ||
| 49 | setState(undefined); | ||
| 50 | setLoading(false); | ||
| 51 | }; | ||
| 52 | |||
| 53 | /** | ||
| 54 | * Fetches the wifi status and SSID. | ||
| 55 | */ | ||
| 56 | const getWifi = React.useCallback(async () => { | ||
| 57 | if (!visible) return; | ||
| 58 | const [status, ssid] = await Promise.all([ | ||
| 59 | Utils.cachedRun( | ||
| 60 | `ifconfig ${networkDevice} | grep status | cut -c 10-`, | ||
| 61 | refresh, | ||
| 62 | ), | ||
| 63 | Utils.cachedRun( | ||
| 64 | `system_profiler SPAirPortDataType | awk '/Current Network/ {getline;$1=$1;print $0 | "tr -d ':'";exit}'`, | ||
| 65 | refresh, | ||
| 66 | ), | ||
| 67 | ]); | ||
| 68 | setState({ | ||
| 69 | status: Utils.cleanupOutput(status), | ||
| 70 | ssid: Utils.cleanupOutput(ssid), | ||
| 71 | }); | ||
| 72 | setLoading(false); | ||
| 73 | }, [networkDevice, visible, refresh]); | ||
| 74 | |||
| 75 | useServerSocket("wifi", visible, getWifi, resetWidget, setLoading); | ||
| 76 | useWidgetRefresh(visible, getWifi, refresh); | ||
| 77 | |||
| 78 | if (loading) return <DataWidgetLoader.Widget className="wifi" />; | ||
| 79 | if (!state) return null; | ||
| 80 | |||
| 81 | const { status, ssid } = state; | ||
| 82 | const isActive = status === "active"; | ||
| 83 | const name = renderName(ssid, hideNetworkName); | ||
| 84 | |||
| 85 | if (hideWifiIfDisabled && !isActive) return null; | ||
| 86 | |||
| 87 | const classes = Utils.classNames("wifi", { | ||
| 88 | "wifi--hidden-name": !name, | ||
| 89 | "wifi--inactive": !isActive, | ||
| 90 | }); | ||
| 91 | |||
| 92 | const Icon = isActive ? Icons.Wifi : Icons.WifiOff; | ||
| 93 | |||
| 94 | /** | ||
| 95 | * Handles the click event to toggle wifi. | ||
| 96 | * @param {React.MouseEvent} e - The click event. | ||
| 97 | */ | ||
| 98 | const onClick = async (e) => { | ||
| 99 | Utils.clickEffect(e); | ||
| 100 | await toggleWifi(isActive, networkDevice, pushMissive); | ||
| 101 | getWifi(); | ||
| 102 | }; | ||
| 103 | |||
| 104 | return ( | ||
| 105 | <DataWidget.Widget | ||
| 106 | classes={classes} | ||
| 107 | Icon={showIcon ? Icon : null} | ||
| 108 | onClick={toggleWifiOnClick ? onClick : undefined} | ||
| 109 | onRightClick={openWifiPreferences} | ||
| 110 | > | ||
| 111 | {name} | ||
| 112 | </DataWidget.Widget> | ||
| 113 | ); | ||
| 114 | }); | ||
| 115 | |||
| 116 | Widget.displayName = "Wifi"; | ||
| 117 | |||
| 118 | /** | ||
| 119 | * Toggles the wifi on or off. | ||
| 120 | * @param {boolean} isActive - Whether the wifi is currently active. | ||
| 121 | * @param {string} networkDevice - The network device name. | ||
| 122 | * @param {function} pushMissive - Function to push notifications. | ||
| 123 | */ | ||
| 124 | async function toggleWifi(isActive, networkDevice, pushMissive) { | ||
| 125 | if (isActive) { | ||
| 126 | await Uebersicht.run(`networksetup -setairportpower ${networkDevice} off`); | ||
| 127 | Utils.notification("Disabling network...", pushMissive); | ||
| 128 | } else { | ||
| 129 | await Uebersicht.run(`networksetup -setairportpower ${networkDevice} on`); | ||
| 130 | Utils.notification("Enabling network...", pushMissive); | ||
| 131 | } | ||
| 132 | } | ||
| 133 | |||
| 134 | /** | ||
| 135 | * Opens the wifi preferences pane. | ||
| 136 | * @param {React.MouseEvent} e - The click event. | ||
| 137 | */ | ||
| 138 | function openWifiPreferences(e) { | ||
| 139 | Utils.clickEffect(e); | ||
| 140 | Uebersicht.run(`open /System/Library/PreferencePanes/Network.prefPane/`); | ||
| 141 | } | ||
| 142 | |||
| 143 | /** | ||
| 144 | * Renders the wifi network name. | ||
| 145 | * @param {string} name - The network name. | ||
| 146 | * @param {boolean} hideNetworkName - Whether to hide the network name. | ||
| 147 | * @returns {string} The rendered network name. | ||
| 148 | */ | ||
| 149 | function renderName(name, hideNetworkName) { | ||
| 150 | if (!name || hideNetworkName) return ""; | ||
| 151 | if (name === "with an AirPort network.y off.") return "Disabled"; | ||
| 152 | if (name === "with an AirPort network.") return "Searching..."; | ||
| 153 | return name; | ||
| 154 | } | ||
diff --git a/users/ryan/modules/simple-bar/simple-bar-source/lib/components/data/youtube-music.jsx b/users/ryan/modules/simple-bar/simple-bar-source/lib/components/data/youtube-music.jsx new file mode 100755 index 0000000..bf586b1 --- /dev/null +++ b/users/ryan/modules/simple-bar/simple-bar-source/lib/components/data/youtube-music.jsx | |||
| @@ -0,0 +1,191 @@ | |||
| 1 | import * as Uebersicht from "uebersicht"; | ||
| 2 | import * as DataWidget from "./data-widget.jsx"; | ||
| 3 | import * as DataWidgetLoader from "./data-widget-loader.jsx"; | ||
| 4 | import useWidgetRefresh from "../../hooks/use-widget-refresh"; | ||
| 5 | import useServerSocket from "../../hooks/use-server-socket"; | ||
| 6 | import { useSimpleBarContext } from "../simple-bar-context.jsx"; | ||
| 7 | import * as Icons from "../icons/icons.jsx"; | ||
| 8 | import * as Utils from "../../utils"; | ||
| 9 | |||
| 10 | export { youtubeMusicStyles as styles } from "../../styles/components/data/youtube-music"; | ||
| 11 | |||
| 12 | const { React } = Uebersicht; | ||
| 13 | |||
| 14 | const DEFAULT_REFRESH_FREQUENCY = 10000; | ||
| 15 | |||
| 16 | /** | ||
| 17 | * YouTube Music Widget component. | ||
| 18 | * @returns {JSX.Element} The YouTube Music widget. | ||
| 19 | */ | ||
| 20 | export const Widget = React.memo(() => { | ||
| 21 | const { displayIndex, settings } = useSimpleBarContext(); | ||
| 22 | const { | ||
| 23 | widgets: { youtubeMusicWidget }, | ||
| 24 | youtubeMusicWidgetOptions: { | ||
| 25 | refreshFrequency, | ||
| 26 | showSpecter, | ||
| 27 | showOnDisplay, | ||
| 28 | youtubeMusicPort, | ||
| 29 | showIcon, | ||
| 30 | }, | ||
| 31 | } = settings; | ||
| 32 | |||
| 33 | // Determine the refresh frequency for the widget | ||
| 34 | const refresh = React.useMemo( | ||
| 35 | () => | ||
| 36 | Utils.getRefreshFrequency(refreshFrequency, DEFAULT_REFRESH_FREQUENCY), | ||
| 37 | [refreshFrequency], | ||
| 38 | ); | ||
| 39 | |||
| 40 | // Determine if the widget should be visible on the current display | ||
| 41 | const visible = | ||
| 42 | Utils.isVisibleOnDisplay(displayIndex, showOnDisplay) && youtubeMusicWidget; | ||
| 43 | |||
| 44 | const [state, setState] = React.useState(); | ||
| 45 | const [cachedAccessToken, setCachedAccessToken] = React.useState(); | ||
| 46 | const [loading, setLoading] = React.useState(visible); | ||
| 47 | |||
| 48 | const apiUrl = `http://localhost:${youtubeMusicPort}`; | ||
| 49 | |||
| 50 | /** | ||
| 51 | * Resets the widget state. | ||
| 52 | */ | ||
| 53 | const resetWidget = () => { | ||
| 54 | setState(undefined); | ||
| 55 | setCachedAccessToken(undefined); | ||
| 56 | setLoading(false); | ||
| 57 | }; | ||
| 58 | |||
| 59 | /** | ||
| 60 | * Fetches the access token for the YouTube Music API. | ||
| 61 | * @returns {Promise<string>} The access token. | ||
| 62 | */ | ||
| 63 | const getAccessToken = React.useCallback(async () => { | ||
| 64 | // As of 2024/12/01, the generated access tokens don't expire | ||
| 65 | if (cachedAccessToken) return cachedAccessToken; | ||
| 66 | |||
| 67 | const response = await fetch(`${apiUrl}/auth/simple-bar`, { | ||
| 68 | method: "POST", | ||
| 69 | }); | ||
| 70 | const json = await response.json(); | ||
| 71 | setCachedAccessToken(json.accessToken); | ||
| 72 | return json.accessToken; | ||
| 73 | }, [apiUrl, cachedAccessToken]); | ||
| 74 | |||
| 75 | /** | ||
| 76 | * Fetches data from a specified route. | ||
| 77 | * @param {string} route - The API route to fetch data from. | ||
| 78 | * @param {string} [method="POST"] - The HTTP method to use. | ||
| 79 | * @returns {Promise<Response>} The fetch response. | ||
| 80 | */ | ||
| 81 | const fetchRoute = React.useCallback( | ||
| 82 | async (route, method = "POST") => { | ||
| 83 | const headers = {}; | ||
| 84 | const url = new URL(route, apiUrl); | ||
| 85 | |||
| 86 | // All routes under /api are protected | ||
| 87 | const needsAuthentication = url.pathname.startsWith("/api"); | ||
| 88 | if (needsAuthentication) { | ||
| 89 | const accessToken = await getAccessToken(); | ||
| 90 | if (!accessToken) { | ||
| 91 | throw new Error("Failed to get access token"); | ||
| 92 | } | ||
| 93 | headers.Authorization = `Bearer ${accessToken}`; | ||
| 94 | } | ||
| 95 | |||
| 96 | return await fetch(url, { method, headers }); | ||
| 97 | }, | ||
| 98 | [apiUrl, getAccessToken], | ||
| 99 | ); | ||
| 100 | |||
| 101 | /** | ||
| 102 | * Refreshes the widget state by fetching the current song info. | ||
| 103 | */ | ||
| 104 | const refreshState = React.useCallback(async () => { | ||
| 105 | if (!visible) return; | ||
| 106 | |||
| 107 | try { | ||
| 108 | const response = await fetchRoute("/api/v1/song-info", "GET"); | ||
| 109 | const json = await response.json(); | ||
| 110 | setState(json); | ||
| 111 | } catch { | ||
| 112 | // most likely due to offline server, reset state | ||
| 113 | resetWidget(); | ||
| 114 | } | ||
| 115 | }, [visible, fetchRoute]); | ||
| 116 | |||
| 117 | // Use server socket to listen for updates | ||
| 118 | useServerSocket( | ||
| 119 | "youtube-music", | ||
| 120 | visible, | ||
| 121 | refreshState, | ||
| 122 | resetWidget, | ||
| 123 | setLoading, | ||
| 124 | ); | ||
| 125 | // Use widget refresh hook to periodically refresh the state | ||
| 126 | useWidgetRefresh(visible, refreshState, refresh); | ||
| 127 | |||
| 128 | if (loading) return <DataWidgetLoader.Widget className="youtube-music" />; | ||
| 129 | |||
| 130 | /** | ||
| 131 | * Handles click event to toggle play/pause. | ||
| 132 | * @param {React.MouseEvent} e - The click event. | ||
| 133 | */ | ||
| 134 | const onClick = async (e) => { | ||
| 135 | Utils.clickEffect(e); | ||
| 136 | await fetchRoute("/api/v1/toggle-play"); | ||
| 137 | await refreshState(); | ||
| 138 | }; | ||
| 139 | |||
| 140 | /** | ||
| 141 | * Handles right-click event to skip to the next song. | ||
| 142 | * @param {React.MouseEvent} e - The right-click event. | ||
| 143 | */ | ||
| 144 | const onRightClick = async (e) => { | ||
| 145 | Utils.clickEffect(e); | ||
| 146 | await fetchRoute("/api/v1/next"); | ||
| 147 | await refreshState(); | ||
| 148 | }; | ||
| 149 | |||
| 150 | /** | ||
| 151 | * Handles middle-click event to open YouTube Music app. | ||
| 152 | * @param {React.MouseEvent} e - The middle-click event. | ||
| 153 | */ | ||
| 154 | const onMiddleClick = async (e) => { | ||
| 155 | Utils.clickEffect(e); | ||
| 156 | Uebersicht.run(`open -a 'YouTube Music'`); | ||
| 157 | await refreshState(); | ||
| 158 | }; | ||
| 159 | |||
| 160 | const classes = Utils.classNames("youtube-music", {}); | ||
| 161 | if (!state) return null; | ||
| 162 | const { title, artist, isPaused } = state; | ||
| 163 | if (!title) return null; | ||
| 164 | |||
| 165 | const label = artist.length ? `${title} - ${artist}` : title; | ||
| 166 | const Icon = getIcon(isPaused); | ||
| 167 | |||
| 168 | return ( | ||
| 169 | <DataWidget.Widget | ||
| 170 | classes={classes} | ||
| 171 | Icon={showIcon ? Icon : null} | ||
| 172 | onClick={onClick} | ||
| 173 | onRightClick={onRightClick} | ||
| 174 | onMiddleClick={onMiddleClick} | ||
| 175 | showSpecter={showSpecter && !isPaused} | ||
| 176 | > | ||
| 177 | {label} | ||
| 178 | </DataWidget.Widget> | ||
| 179 | ); | ||
| 180 | }); | ||
| 181 | |||
| 182 | Widget.displayName = "YouTube Music"; | ||
| 183 | |||
| 184 | /** | ||
| 185 | * Returns the appropriate icon based on the play/pause state. | ||
| 186 | * @param {boolean} isPaused - Whether the music is paused. | ||
| 187 | * @returns {JSX.Element} The icon component. | ||
| 188 | */ | ||
| 189 | function getIcon(isPaused) { | ||
| 190 | return isPaused ? Icons.Paused : Icons.Playing; | ||
| 191 | } | ||
diff --git a/users/ryan/modules/simple-bar/simple-bar-source/lib/components/data/zoom.jsx b/users/ryan/modules/simple-bar/simple-bar-source/lib/components/data/zoom.jsx new file mode 100755 index 0000000..2c123e9 --- /dev/null +++ b/users/ryan/modules/simple-bar/simple-bar-source/lib/components/data/zoom.jsx | |||
| @@ -0,0 +1,107 @@ | |||
| 1 | import * as Uebersicht from "uebersicht"; | ||
| 2 | import * as DataWidget from "./data-widget.jsx"; | ||
| 3 | import * as DataWidgetLoader from "./data-widget-loader.jsx"; | ||
| 4 | import * as Icons from "../icons/icons.jsx"; | ||
| 5 | import { SuspenseIcon } from "../icons/icon.jsx"; | ||
| 6 | import useWidgetRefresh from "../../hooks/use-widget-refresh"; | ||
| 7 | import useServerSocket from "../../hooks/use-server-socket"; | ||
| 8 | import { useSimpleBarContext } from "../simple-bar-context.jsx"; | ||
| 9 | import * as Utils from "../../utils"; | ||
| 10 | |||
| 11 | export { zoomStyles as styles } from "../../styles/components/data/zoom"; | ||
| 12 | |||
| 13 | const { React } = Uebersicht; | ||
| 14 | |||
| 15 | const DEFAULT_REFRESH_FREQUENCY = 5000; | ||
| 16 | |||
| 17 | /** | ||
| 18 | * Zoom widget component. | ||
| 19 | * @returns {JSX.Element|null} The Zoom widget. | ||
| 20 | */ | ||
| 21 | export const Widget = React.memo(() => { | ||
| 22 | const { displayIndex, settings } = useSimpleBarContext(); | ||
| 23 | const { widgets, zoomWidgetOptions } = settings; | ||
| 24 | const { zoomWidget } = widgets; | ||
| 25 | const { refreshFrequency, showVideo, showMic, showOnDisplay } = | ||
| 26 | zoomWidgetOptions; | ||
| 27 | |||
| 28 | // Determine the refresh frequency for the widget | ||
| 29 | const refresh = React.useMemo( | ||
| 30 | () => | ||
| 31 | Utils.getRefreshFrequency(refreshFrequency, DEFAULT_REFRESH_FREQUENCY), | ||
| 32 | [refreshFrequency], | ||
| 33 | ); | ||
| 34 | |||
| 35 | // Determine if the widget should be visible | ||
| 36 | const visible = | ||
| 37 | Utils.isVisibleOnDisplay(displayIndex, showOnDisplay) && zoomWidget; | ||
| 38 | |||
| 39 | const [state, setState] = React.useState(); | ||
| 40 | const [loading, setLoading] = React.useState(visible); | ||
| 41 | |||
| 42 | /** | ||
| 43 | * Reset the widget state. | ||
| 44 | */ | ||
| 45 | const resetWidget = React.useCallback(() => { | ||
| 46 | setState(undefined); | ||
| 47 | setLoading(false); | ||
| 48 | }, []); | ||
| 49 | |||
| 50 | /** | ||
| 51 | * Fetch the Zoom status for mic and video. | ||
| 52 | */ | ||
| 53 | const getZoom = React.useCallback(async () => { | ||
| 54 | if (!visible) return; | ||
| 55 | try { | ||
| 56 | const [mic, video] = await Promise.all([ | ||
| 57 | Utils.cachedRun( | ||
| 58 | `osascript ./simple-bar/lib/scripts/zoom-mute-status.applescript`, | ||
| 59 | refresh, | ||
| 60 | ), | ||
| 61 | Utils.cachedRun( | ||
| 62 | `osascript ./simple-bar/lib/scripts/zoom-video-status.applescript`, | ||
| 63 | refresh, | ||
| 64 | ), | ||
| 65 | ]); | ||
| 66 | setState({ | ||
| 67 | mic: Utils.cleanupOutput(mic), | ||
| 68 | video: Utils.cleanupOutput(video), | ||
| 69 | }); | ||
| 70 | setLoading(false); | ||
| 71 | } catch (error) { | ||
| 72 | // eslint-disable-next-line no-console | ||
| 73 | console.error("Error fetching Zoom status:", error); | ||
| 74 | setLoading(false); | ||
| 75 | } | ||
| 76 | }, [visible, refresh]); | ||
| 77 | |||
| 78 | // Use server socket to listen for Zoom events | ||
| 79 | useServerSocket("zoom", visible, getZoom, resetWidget, setLoading); | ||
| 80 | // Refresh the widget at the specified interval | ||
| 81 | useWidgetRefresh(visible, getZoom, refresh); | ||
| 82 | |||
| 83 | if (loading) return <DataWidgetLoader.Widget className="zoom" />; | ||
| 84 | if (!state || (!state.mic.length && !state.video.length)) return null; | ||
| 85 | |||
| 86 | const { mic, video } = state; | ||
| 87 | |||
| 88 | const VideoIcon = video === "off" ? Icons.CameraOff : Icons.Camera; | ||
| 89 | const MicIcon = mic === "off" ? Icons.MicOff : Icons.MicOn; | ||
| 90 | |||
| 91 | return ( | ||
| 92 | <DataWidget.Widget classes="zoom"> | ||
| 93 | {showVideo && ( | ||
| 94 | <SuspenseIcon> | ||
| 95 | <VideoIcon className={`zoom__icon zoom__icon--${video}`} /> | ||
| 96 | </SuspenseIcon> | ||
| 97 | )} | ||
| 98 | {showMic && ( | ||
| 99 | <SuspenseIcon> | ||
| 100 | <MicIcon className={`zoom__icon zoom__icon--${mic}`} /> | ||
| 101 | </SuspenseIcon> | ||
| 102 | )} | ||
| 103 | </DataWidget.Widget> | ||
| 104 | ); | ||
| 105 | }); | ||
| 106 | |||
| 107 | Widget.displayName = "Zoom"; | ||
