aboutsummaryrefslogtreecommitdiff
path: root/users/ryan/modules/simple-bar/simple-bar-source/lib/components/data
diff options
context:
space:
mode:
Diffstat (limited to 'users/ryan/modules/simple-bar/simple-bar-source/lib/components/data')
-rwxr-xr-xusers/ryan/modules/simple-bar/simple-bar-source/lib/components/data/battery.jsx183
-rwxr-xr-xusers/ryan/modules/simple-bar/simple-bar-source/lib/components/data/browser-track.jsx147
-rwxr-xr-xusers/ryan/modules/simple-bar/simple-bar-source/lib/components/data/cpu.jsx153
-rwxr-xr-xusers/ryan/modules/simple-bar/simple-bar-source/lib/components/data/crypto.jsx160
-rwxr-xr-xusers/ryan/modules/simple-bar/simple-bar-source/lib/components/data/data-widget-loader.jsx37
-rwxr-xr-xusers/ryan/modules/simple-bar/simple-bar-source/lib/components/data/data-widget.jsx136
-rwxr-xr-xusers/ryan/modules/simple-bar/simple-bar-source/lib/components/data/date-display.jsx118
-rwxr-xr-xusers/ryan/modules/simple-bar/simple-bar-source/lib/components/data/github.jsx105
-rwxr-xr-xusers/ryan/modules/simple-bar/simple-bar-source/lib/components/data/gpu.jsx128
-rwxr-xr-xusers/ryan/modules/simple-bar/simple-bar-source/lib/components/data/graph.jsx87
-rwxr-xr-xusers/ryan/modules/simple-bar/simple-bar-source/lib/components/data/keyboard.jsx108
-rwxr-xr-xusers/ryan/modules/simple-bar/simple-bar-source/lib/components/data/memory.jsx134
-rwxr-xr-xusers/ryan/modules/simple-bar/simple-bar-source/lib/components/data/mic.jsx152
-rwxr-xr-xusers/ryan/modules/simple-bar/simple-bar-source/lib/components/data/mpd.jsx228
-rwxr-xr-xusers/ryan/modules/simple-bar/simple-bar-source/lib/components/data/music.jsx161
-rwxr-xr-xusers/ryan/modules/simple-bar/simple-bar-source/lib/components/data/netstats.jsx182
-rwxr-xr-xusers/ryan/modules/simple-bar/simple-bar-source/lib/components/data/next-meeting.jsx310
-rwxr-xr-xusers/ryan/modules/simple-bar/simple-bar-source/lib/components/data/notifications.jsx189
-rwxr-xr-xusers/ryan/modules/simple-bar/simple-bar-source/lib/components/data/sound.jsx154
-rwxr-xr-xusers/ryan/modules/simple-bar/simple-bar-source/lib/components/data/specter.jsx24
-rwxr-xr-xusers/ryan/modules/simple-bar/simple-bar-source/lib/components/data/spotify.jsx189
-rwxr-xr-xusers/ryan/modules/simple-bar/simple-bar-source/lib/components/data/stock.jsx211
-rwxr-xr-xusers/ryan/modules/simple-bar/simple-bar-source/lib/components/data/time.jsx144
-rwxr-xr-xusers/ryan/modules/simple-bar/simple-bar-source/lib/components/data/user-widgets.jsx181
-rwxr-xr-xusers/ryan/modules/simple-bar/simple-bar-source/lib/components/data/viscosity-vpn.jsx144
-rwxr-xr-xusers/ryan/modules/simple-bar/simple-bar-source/lib/components/data/weather.jsx214
-rwxr-xr-xusers/ryan/modules/simple-bar/simple-bar-source/lib/components/data/wifi.jsx154
-rwxr-xr-xusers/ryan/modules/simple-bar/simple-bar-source/lib/components/data/youtube-music.jsx191
-rwxr-xr-xusers/ryan/modules/simple-bar/simple-bar-source/lib/components/data/zoom.jsx107
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 @@
1import * as Uebersicht from "uebersicht";
2import * as DataWidget from "./data-widget.jsx";
3import * as DataWidgetLoader from "./data-widget-loader.jsx";
4import * as Icons from "../icons/icons.jsx";
5import { SuspenseIcon } from "../icons/icon.jsx";
6import useWidgetRefresh from "../../hooks/use-widget-refresh";
7import useServerSocket from "../../hooks/use-server-socket";
8import { useSimpleBarContext } from "../simple-bar-context.jsx";
9import * as Utils from "../../utils";
10
11export { batteryStyles as styles } from "../../styles/components/data/battery";
12
13const { React } = Uebersicht;
14
15const DEFAULT_REFRESH_FREQUENCY = 10000;
16
17/**
18 * Battery widget component
19 * @returns {JSX.Element|null} The battery widget component
20 */
21export 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
152Widget.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 */
159function 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 */
173async 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 @@
1import * as Uebersicht from "uebersicht";
2import * as DataWidget from "./data-widget.jsx";
3import * as DataWidgetLoader from "./data-widget-loader.jsx";
4import * as Icons from "../icons/icons.jsx";
5import { SuspenseIcon } from "../icons/icon.jsx";
6import useWidgetRefresh from "../../hooks/use-widget-refresh";
7import useServerSocket from "../../hooks/use-server-socket";
8import { useSimpleBarContext } from "../simple-bar-context.jsx";
9import * as Utils from "../../utils";
10
11export { browserTrackStyles as styles } from "../../styles/components/data/browser-track";
12
13const { React } = Uebersicht;
14
15const DEFAULT_REFRESH_FREQUENCY = 10000;
16
17/**
18 * BrowserTrack Widget component
19 * @returns {JSX.Element|null} The BrowserTrack widget
20 */
21export 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
134Widget.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 */
141function 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 @@
1import * as Uebersicht from "uebersicht";
2import * as DataWidget from "./data-widget.jsx";
3import * as DataWidgetLoader from "./data-widget-loader.jsx";
4import Graph from "./graph.jsx";
5import * as Icons from "../icons/icons.jsx";
6import useWidgetRefresh from "../../hooks/use-widget-refresh";
7import useServerSocket from "../../hooks/use-server-socket";
8import { useSimpleBarContext } from "../simple-bar-context.jsx";
9import * as Utils from "../../utils";
10
11export { cpuStyles as styles } from "../../styles/components/data/cpu";
12
13const { React } = Uebersicht;
14
15const DEFAULT_REFRESH_FREQUENCY = 2000;
16const GRAPH_LENGTH = 50;
17
18/**
19 * CPU Widget component
20 * @returns {JSX.Element|null} The CPU widget
21 */
22export 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
138Widget.displayName = "Cpu";
139
140/**
141 * Open the specified CPU usage monitoring application
142 * @param {string} app - The name of the application to open
143 */
144function 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 @@
1import * as Uebersicht from "uebersicht";
2import * as DataWidget from "./data-widget.jsx";
3import * as DataWidgetLoader from "./data-widget-loader.jsx";
4import * as Icons from "../icons/icons.jsx";
5import useWidgetRefresh from "../../hooks/use-widget-refresh";
6import useServerSocket from "../../hooks/use-server-socket";
7import { useSimpleBarContext } from "../simple-bar-context.jsx";
8import * as Utils from "../../utils";
9
10export { cryptoStyles as styles } from "../../styles/components/data/crypto";
11
12const { React } = Uebersicht;
13
14const 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 */
20export 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
127Widget.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 */
134function 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 */
146function 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 */
157function 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 @@
1import * as Uebersicht from "uebersicht";
2import * as Utils from "../../utils";
3
4export { dataWidgetLoaderStyles as styles } from "../../styles/components/data/data-widget-loader";
5
6const { 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 */
18export 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
37Widget.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 @@
1import * as Uebersicht from "uebersicht";
2import * as Specter from "./specter.jsx";
3import * as Utils from "../../utils";
4import { SuspenseIcon } from "../icons/icon.jsx";
5export { dataWidgetStyles as styles } from "../../styles/components/data/data-widget";
6
7const { 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 */
25export 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 */
120function 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 */
134function 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 @@
1import * as Uebersicht from "uebersicht";
2import * as DataWidget from "./data-widget.jsx";
3import * as DataWidgetLoader from "./data-widget-loader.jsx";
4import * as Icons from "../icons/icons.jsx";
5import * as Utils from "../../utils";
6import useWidgetRefresh from "../../hooks/use-widget-refresh";
7import useServerSocket from "../../hooks/use-server-socket";
8import { useSimpleBarContext } from "../simple-bar-context.jsx";
9
10export { dateStyles as styles } from "../../styles/components/data/date-display";
11
12const { React } = Uebersicht;
13
14const DEFAULT_REFRESH_FREQUENCY = 30000;
15
16/**
17 * Date display widget component.
18 * @returns {JSX.Element} The date display widget.
19 */
20export 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
109Widget.displayName = "DateDisplay";
110
111/**
112 * Open the specified calendar application.
113 * @param {string} calendarApp - The name of the calendar application to open.
114 */
115function 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 @@
1import * as Uebersicht from "uebersicht";
2import * as DataWidget from "./data-widget.jsx";
3import * as DataWidgetLoader from "./data-widget-loader.jsx";
4import * as Icons from "../icons/icons.jsx";
5import useWidgetRefresh from "../../hooks/use-widget-refresh.js";
6import useServerSocket from "../../hooks/use-server-socket.js";
7import { useSimpleBarContext } from "../simple-bar-context.jsx";
8import * as Utils from "../../utils.js";
9
10export { githubStyles as styles } from "../../styles/components/data/github.js";
11
12const { React } = Uebersicht;
13
14const DEFAULT_REFRESH_FREQUENCY = 1000 * 60 * 10;
15
16/**
17 * GitHub notification widget component
18 * @returns {JSX.Element|null} The GitHub notification widget component
19 */
20export 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
105Widget.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 @@
1import * as Uebersicht from "uebersicht";
2import * as DataWidget from "./data-widget.jsx";
3import * as DataWidgetLoader from "./data-widget-loader.jsx";
4import Graph from "./graph.jsx";
5import * as Icons from "../icons/icons.jsx";
6import useWidgetRefresh from "../../hooks/use-widget-refresh.js";
7import useServerSocket from "../../hooks/use-server-socket.js";
8import { useSimpleBarContext } from "../simple-bar-context.jsx";
9import * as Utils from "../../utils.js";
10
11export { gpuStyles as styles } from "../../styles/components/data/gpu.js";
12
13const { React } = Uebersicht;
14
15const DEFAULT_REFRESH_FREQUENCY = 2000;
16const GRAPH_LENGTH = 40;
17
18/**
19 * GPU Widget component
20 * @returns {JSX.Element|null} The GPU widget component
21 */
22export 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
128Widget.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 @@
1import * as Uebersicht from "uebersicht";
2import * as Utils from "../../utils.js";
3import { SuspenseIcon } from "../icons/icon.jsx";
4
5const { React } = Uebersicht;
6
7export { 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 */
20export 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 @@
1import * as Uebersicht from "uebersicht";
2import * as DataWidget from "./data-widget.jsx";
3import * as DataWidgetLoader from "./data-widget-loader.jsx";
4import * as Icons from "../icons/icons.jsx";
5import useWidgetRefresh from "../../hooks/use-widget-refresh";
6import useServerSocket from "../../hooks/use-server-socket";
7import { useSimpleBarContext } from "../simple-bar-context.jsx";
8import * as Utils from "../../utils";
9
10export { keyboardStyles as styles } from "../../styles/components/data/keyboard";
11
12const { React } = Uebersicht;
13
14const DEFAULT_REFRESH_FREQUENCY = 20000;
15
16/**
17 * Keyboard widget component.
18 * @returns {JSX.Element|null} The rendered widget or null if not visible.
19 */
20export 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
108Widget.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 @@
1import * as Uebersicht from "uebersicht";
2import * as DataWidget from "./data-widget.jsx";
3import * as DataWidgetLoader from "./data-widget-loader.jsx";
4import useWidgetRefresh from "../../hooks/use-widget-refresh";
5import useServerSocket from "../../hooks/use-server-socket";
6import { useSimpleBarContext } from "../simple-bar-context.jsx";
7import * as Utils from "../../utils";
8
9export { memoryStyles as styles } from "../../styles/components/data/memory";
10
11const { React } = Uebersicht;
12
13const DEFAULT_REFRESH_FREQUENCY = 4000;
14
15/**
16 * Memory Widget component
17 * @returns {JSX.Element|null} The memory widget component
18 */
19export 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 */
125function 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 @@
1import * as Uebersicht from "uebersicht";
2import * as DataWidget from "./data-widget.jsx";
3import * as DataWidgetLoader from "./data-widget-loader.jsx";
4import * as Icons from "../icons/icons.jsx";
5import { SuspenseIcon } from "../icons/icon.jsx";
6import useWidgetRefresh from "../../hooks/use-widget-refresh";
7import useServerSocket from "../../hooks/use-server-socket";
8import * as Utils from "../../utils";
9import { useSimpleBarContext } from "../simple-bar-context.jsx";
10
11const { React } = Uebersicht;
12
13export { micStyles as styles } from "../../styles/components/data/mic";
14
15const DEFAULT_REFRESH_FREQUENCY = 20000;
16
17/**
18 * Mic widget component.
19 * @returns {JSX.Element} The rendered mic widget.
20 */
21export 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
143Widget.displayName = "Mic";
144
145/**
146 * Set the microphone volume.
147 * @param {number} volume - The volume level to set.
148 */
149function 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 @@
1import * as Uebersicht from "uebersicht";
2import * as DataWidget from "./data-widget.jsx";
3import * as DataWidgetLoader from "./data-widget-loader.jsx";
4import * as Icons from "../icons/icons.jsx";
5import useWidgetRefresh from "../../hooks/use-widget-refresh";
6import useServerSocket from "../../hooks/use-server-socket";
7import { useSimpleBarContext } from "../simple-bar-context.jsx";
8import * as Utils from "../../utils";
9
10export { mpdStyles as styles } from "../../styles/components/data/mpd";
11
12const { React } = Uebersicht;
13
14const DEFAULT_REFRESH_FREQUENCY = 10000;
15
16/**
17 * MPD Widget component
18 * @returns {JSX.Element|null} The MPD widget
19 */
20export 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
205Widget.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 */
214async 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 */
225function 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 @@
1import * as Uebersicht from "uebersicht";
2import * as DataWidget from "./data-widget.jsx";
3import * as DataWidgetLoader from "./data-widget-loader.jsx";
4import * as Icons from "../icons/icons.jsx";
5import useWidgetRefresh from "../../hooks/use-widget-refresh";
6import useServerSocket from "../../hooks/use-server-socket";
7import { useSimpleBarContext } from "../simple-bar-context.jsx";
8import * as Utils from "../../utils";
9
10export { musicStyles as styles } from "../../styles/components/data/music";
11
12const { React } = Uebersicht;
13
14const DEFAULT_REFRESH_FREQUENCY = 10000;
15
16/**
17 * Music widget component.
18 * @returns {JSX.Element|null} The music widget.
19 */
20export 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
148Widget.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 */
155function 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 @@
1import * as Uebersicht from "uebersicht";
2import * as DataWidget from "./data-widget.jsx";
3import * as DataWidgetLoader from "./data-widget-loader.jsx";
4import * as Icons from "../icons/icons.jsx";
5import { SuspenseIcon } from "../icons/icon.jsx";
6import Graph from "./graph.jsx";
7import useWidgetRefresh from "../../hooks/use-widget-refresh.js";
8import useServerSocket from "../../hooks/use-server-socket";
9import { useSimpleBarContext } from "../simple-bar-context.jsx";
10import * as Utils from "../../utils.js";
11
12export { netstatsStyles as styles } from "../../styles/components/data/netstats";
13
14const { React } = Uebersicht;
15
16const DEFAULT_REFRESH_FREQUENCY = 2000;
17const GRAPH_LENGTH = 30;
18
19/**
20 * Netstats widget component.
21 * @returns {JSX.Element|null} The rendered component.
22 */
23export 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
182Widget.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 */
14import * as Uebersicht from "uebersicht";
15import * as DataWidget from "./data-widget.jsx";
16import * as DataWidgetLoader from "./data-widget-loader.jsx";
17import * as Icons from "../icons/icons.jsx";
18import useWidgetRefresh from "../../hooks/use-widget-refresh";
19import useServerSocket from "../../hooks/use-server-socket";
20import { useSimpleBarContext } from "../simple-bar-context.jsx";
21import * as Utils from "../../utils";
22
23export { nextMeetingStyles as styles } from "../../styles/components/data/next-meeting";
24
25const { React } = Uebersicht;
26
27const DEFAULT_REFRESH_FREQUENCY = 60000; // 1 minute
28
29// Timing thresholds (in minutes)
30const URGENT_THRESHOLD = 5; // Red pulsing when ≤5 min until meeting
31const 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
35const 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 */
43function 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
54const 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)
62const 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 */
70function 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 */
83function 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 */
97function 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 */
141function 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 */
156function 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 */
182const 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
207JoinButton.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 */
214export 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
310Widget.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 */
13import * as Uebersicht from "uebersicht";
14import * as AppIcons from "../../app-icons.js";
15import { SuspenseIcon } from "../icons/icon.jsx";
16import useWidgetRefresh from "../../hooks/use-widget-refresh";
17import useServerSocket from "../../hooks/use-server-socket";
18import { useSimpleBarContext } from "../simple-bar-context.jsx";
19import * as Utils from "../../utils";
20
21export { notificationsStyles as styles } from "../../styles/components/data/notifications";
22
23const { React } = Uebersicht;
24
25const DEFAULT_REFRESH_FREQUENCY = 10000;
26
27// Shell command to get notification badges from running apps
28const COMMAND = `lsappinfo -all list 2>/dev/null | perl -0777 -pe 's/---/\\n---\\n/g' | perl -ne '
29 if (/"CFBundleName"="([^"]+)"/) { $app = $1; }
30 if (/"StatusLabel"=\\{ "label"="([^"]*)"/) { $badge = $1; }
31 if (/"LSBundlePath"="([^"]+)"/) { $path = $1; }
32 if (/^---$/ || eof) {
33 print "$app|$badge|$path\\n" if ($app && $badge);
34 ($app, $badge, $path) = ("", "", "");
35 }
36'`;
37
38/**
39 * Parses shell command output into notification objects.
40 * @param {string} output - Raw command output from lsappinfo
41 * @returns {Array<{name: string, badge: string, bundlePath: string}>} Array of notification objects
42 */
43function parseNotifications(output) {
44 if (!output || !output.trim()) return [];
45
46 return output
47 .trim()
48 .split("\n")
49 .filter((line) => line.includes("|"))
50 .map((line) => {
51 const [name, badge, bundlePath] = line.split("|");
52 return { name, badge, bundlePath };
53 })
54 .filter((item) => item.name && item.badge);
55}
56
57/**
58 * Single app notification pill component.
59 * @component
60 * @param {Object} props - Component props
61 * @param {Object} props.app - Application notification data
62 * @param {string} props.app.name - Application name
63 * @param {string} props.app.badge - Badge count
64 * @param {string} props.app.bundlePath - Path to application bundle
65 * @returns {JSX.Element} Notification pill button
66 */
67const NotificationPill = React.memo(({ app }) => {
68 /**
69 * Opens the application when pill is clicked.
70 * @param {React.MouseEvent} e - Click event
71 */
72 const openApp = async (e) => {
73 Utils.clickEffect(e);
74 // Escape special shell characters to prevent injection
75 const safePath = app.bundlePath.replace(/["$`\\]/g, "\\$&");
76 await Uebersicht.run(`open "${safePath}"`);
77 };
78
79 // Get the SVG icon for this app, or use Default
80 const Icon = AppIcons.apps[app.name] || AppIcons.apps.Default;
81
82 return (
83 <button
84 className="notification-pill"
85 onClick={openApp}
86 title={`${app.name}: ${app.badge} notification${app.badge !== "1" ? "s" : ""}`}
87 aria-label={`Open ${app.name} - ${app.badge} notification${app.badge !== "1" ? "s" : ""}`}
88 >
89 <SuspenseIcon>
90 <Icon className="notification-pill__icon" />
91 </SuspenseIcon>
92 <span className="notification-pill__badge">{app.badge}</span>
93 </button>
94 );
95});
96
97NotificationPill.displayName = "NotificationPill";
98
99/**
100 * Notification Badges widget component.
101 * Shows each app with notifications as an inline pill.
102 * @component
103 * @returns {JSX.Element|null} The notifications widget component or null if no notifications
104 */
105export const Widget = React.memo(() => {
106 const { displayIndex, settings } = useSimpleBarContext();
107 const { widgets, notificationsWidgetOptions } = settings;
108 const { notificationsWidget } = widgets;
109 const { refreshFrequency, showOnDisplay, excludedApps } = notificationsWidgetOptions;
110
111 // Determine if the widget should be visible based on display settings
112 const visible =
113 Utils.isVisibleOnDisplay(displayIndex, showOnDisplay) &&
114 notificationsWidget;
115
116 // Calculate the refresh frequency for the widget
117 const refresh = React.useMemo(
118 () =>
119 Utils.getRefreshFrequency(refreshFrequency, DEFAULT_REFRESH_FREQUENCY),
120 [refreshFrequency],
121 );
122
123 const [state, setState] = React.useState([]);
124 const [loading, setLoading] = React.useState(visible);
125
126 /**
127 * Resets the widget state.
128 */
129 const resetWidget = React.useCallback(() => {
130 setState([]);
131 setLoading(false);
132 }, []);
133
134 // Build the exclusion list from the comma-separated setting
135 const exclusionList = React.useMemo(
136 () =>
137 excludedApps
138 ? excludedApps
139 .split(",")
140 .map((s) => s.trim().toLowerCase())
141 .filter(Boolean)
142 : [],
143 [excludedApps],
144 );
145
146 /**
147 * Fetches notification badges and updates the state.
148 */
149 const getNotifications = React.useCallback(async () => {
150 if (!visible) return;
151 try {
152 const output = await Utils.cachedRun(COMMAND, refresh);
153 const notifications = parseNotifications(output).filter(
154 (item) => !exclusionList.includes(item.name.toLowerCase()),
155 );
156 setState(notifications);
157 } catch (error) {
158 // eslint-disable-next-line no-console
159 console.error("Error fetching notifications:", error);
160 setState([]);
161 }
162 setLoading(false);
163 }, [visible, refresh, exclusionList]);
164
165 // Server socket for real-time updates
166 useServerSocket(
167 "notifications",
168 visible,
169 getNotifications,
170 resetWidget,
171 setLoading,
172 );
173
174 // Refresh the widget at the specified interval
175 useWidgetRefresh(visible, getNotifications, refresh);
176
177 // Don't render anything if loading or no notifications
178 if (loading || !state.length) return null;
179
180 return (
181 <div className="notifications">
182 {state.map((app) => (
183 <NotificationPill key={app.bundlePath} app={app} />
184 ))}
185 </div>
186 );
187});
188
189Widget.displayName = "Notifications";
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 @@
1import * as Uebersicht from "uebersicht";
2import * as DataWidget from "./data-widget.jsx";
3import * as DataWidgetLoader from "./data-widget-loader.jsx";
4import * as Icons from "../icons/icons.jsx";
5import { SuspenseIcon } from "../icons/icon.jsx";
6import useWidgetRefresh from "../../hooks/use-widget-refresh";
7import useServerSocket from "../../hooks/use-server-socket";
8import { useSimpleBarContext } from "../simple-bar-context.jsx";
9import * as Utils from "../../utils";
10
11export { soundStyles as styles } from "../../styles/components/data/sound";
12
13const { React } = Uebersicht;
14
15const DEFAULT_REFRESH_FREQUENCY = 20000;
16
17/**
18 * Sound widget component.
19 * @returns {JSX.Element|null} The sound widget.
20 */
21export 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
145Widget.displayName = "Sound";
146
147/**
148 * Set the system volume.
149 * @param {number} volume - The volume to set.
150 */
151function 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 @@
1import * as Uebersicht from "uebersicht";
2
3export { specterStyles as styles } from "../../styles/components/data/specter";
4
5const { 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 */
14export 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
24Widget.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 @@
1import * as Uebersicht from "uebersicht";
2import * as DataWidget from "./data-widget.jsx";
3import * as DataWidgetLoader from "./data-widget-loader.jsx";
4import useWidgetRefresh from "../../hooks/use-widget-refresh";
5import useServerSocket from "../../hooks/use-server-socket";
6import { useSimpleBarContext } from "../simple-bar-context.jsx";
7import * as Icons from "../icons/icons.jsx";
8import * as Utils from "../../utils";
9
10export { spotifyStyles as styles } from "../../styles/components/data/spotify";
11
12const { React } = Uebersicht;
13
14const DEFAULT_REFRESH_FREQUENCY = 10000;
15
16/**
17 * Spotify widget component.
18 * @returns {JSX.Element|null} The Spotify widget.
19 */
20export 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
169Widget.displayName = "Spotify";
170
171/**
172 * Toggles play/pause state of Spotify.
173 * @param {boolean} isPaused - Whether the player is paused.
174 */
175function 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 */
185function 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 @@
1import * as Uebersicht from "uebersicht";
2import * as DataWidget from "./data-widget.jsx";
3import * as DataWidgetLoader from "./data-widget-loader.jsx";
4import * as Icons from "../icons/icons.jsx";
5import useWidgetRefresh from "../../hooks/use-widget-refresh";
6import useServerSocket from "../../hooks/use-server-socket";
7import { useSimpleBarContext } from "../simple-bar-context.jsx";
8import * as Utils from "../../utils";
9
10export { stockStyles as styles } from "../../styles/components/data/stock";
11
12const { React } = Uebersicht;
13
14const DEFAULT_REFRESH_FREQUENCY = 15 * 60 * 1000; // 15 min
15
16/**
17 * Stock widget component
18 * @returns {JSX.Element|null} The stock widget
19 */
20export 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>&nbsp;</span>
160 )}
161 {showCurrency && currencySymbol}
162 {showMarketPrice && marketPrice}
163 </span>
164 <span className={showColor ? (stockUp ? "stockUp" : "stockDown") : ""}>
165 {(showMarketChange || showMarketPercent) && <span>&nbsp;</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
179Widget.displayName = "Stock";
180
181/**
182 * Handle click event to open stock details
183 * @param {Event} e - The event object
184 */
185function 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 */
194function 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 */
208function 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 @@
1import * as Uebersicht from "uebersicht";
2import * as DataWidget from "./data-widget.jsx";
3import * as DataWidgetLoader from "./data-widget-loader.jsx";
4import useWidgetRefresh from "../../hooks/use-widget-refresh";
5import useServerSocket from "../../hooks/use-server-socket";
6import { useSimpleBarContext } from "../simple-bar-context.jsx";
7import * as Utils from "../../utils";
8
9export { timeStyles as styles } from "../../styles/components/data/time";
10
11const { React } = Uebersicht;
12
13const DEFAULT_REFRESH_FREQUENCY = 1000;
14
15/**
16 * Time widget component.
17 * @returns {JSX.Element|null} The rendered widget.
18 */
19export 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
118Widget.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 */
126function 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 @@
1import * as Uebersicht from "uebersicht";
2import * as DataWidget from "./data-widget.jsx";
3import * as DataWidgetLoader from "./data-widget-loader.jsx";
4import * as Icons from "../icons/icons.jsx";
5import useWidgetRefresh from "../../hooks/use-widget-refresh";
6import useServerSocket from "../../hooks/use-server-socket";
7import { useSimpleBarContext } from "../simple-bar-context.jsx";
8import * as Settings from "../../settings";
9import * as Utils from "../../utils";
10
11const { React } = Uebersicht;
12
13export 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 */
19function 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
32UserWidgets.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 */
41const 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
181UserWidget.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 @@
1import * as Uebersicht from "uebersicht";
2import * as DataWidget from "./data-widget.jsx";
3import * as DataWidgetLoader from "./data-widget-loader.jsx";
4import * as Icons from "../icons/icons.jsx";
5import useWidgetRefresh from "../../hooks/use-widget-refresh";
6import useServerSocket from "../../hooks/use-server-socket";
7import { useSimpleBarContext } from "../simple-bar-context.jsx";
8import * as Utils from "../../utils";
9
10export { viscosityVPNStyles as styles } from "../../styles/components/data/viscosity-vpn";
11
12const { React } = Uebersicht;
13
14const DEFAULT_REFRESH_FREQUENCY = 8000;
15
16/**
17 * Viscosity VPN Widget component.
18 * @returns {JSX.Element|null} The Viscosity VPN widget.
19 */
20export 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
118Widget.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 */
126function 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 @@
1import * as Uebersicht from "uebersicht";
2import * as DataWidget from "./data-widget.jsx";
3import * as DataWidgetLoader from "./data-widget-loader.jsx";
4import * as Icons from "../icons/icons.jsx";
5import * as Utils from "../../utils";
6import useWidgetRefresh from "../../hooks/use-widget-refresh";
7import useServerSocket from "../../hooks/use-server-socket";
8import { useSimpleBarContext } from "../simple-bar-context.jsx";
9
10export { weatherStyles as styles } from "../../styles/components/data/weather";
11
12const { React } = Uebersicht;
13
14const DEFAULT_REFRESH_FREQUENCY = 1000 * 60 * 30; // Default refresh frequency set to 30 minutes
15
16/**
17 * Weather widget component
18 */
19export 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
162Widget.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 */
170function 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 */
190function 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 */
201function 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 */
210async 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 @@
1import * as Uebersicht from "uebersicht";
2import * as DataWidget from "./data-widget.jsx";
3import * as DataWidgetLoader from "./data-widget-loader.jsx";
4import * as Icons from "../icons/icons.jsx";
5import useWidgetRefresh from "../../hooks/use-widget-refresh";
6import useServerSocket from "../../hooks/use-server-socket";
7import { useSimpleBarContext } from "../simple-bar-context.jsx";
8import * as Utils from "../../utils";
9
10export { wifiStyles as styles } from "../../styles/components/data/wifi";
11
12const { React } = Uebersicht;
13
14const DEFAULT_REFRESH_FREQUENCY = 20000;
15
16/**
17 * Wifi widget component.
18 * @returns {JSX.Element|null} The Wifi widget.
19 */
20export 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
116Widget.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 */
124async 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 */
138function 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 */
149function 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 @@
1import * as Uebersicht from "uebersicht";
2import * as DataWidget from "./data-widget.jsx";
3import * as DataWidgetLoader from "./data-widget-loader.jsx";
4import useWidgetRefresh from "../../hooks/use-widget-refresh";
5import useServerSocket from "../../hooks/use-server-socket";
6import { useSimpleBarContext } from "../simple-bar-context.jsx";
7import * as Icons from "../icons/icons.jsx";
8import * as Utils from "../../utils";
9
10export { youtubeMusicStyles as styles } from "../../styles/components/data/youtube-music";
11
12const { React } = Uebersicht;
13
14const DEFAULT_REFRESH_FREQUENCY = 10000;
15
16/**
17 * YouTube Music Widget component.
18 * @returns {JSX.Element} The YouTube Music widget.
19 */
20export 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
182Widget.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 */
189function 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 @@
1import * as Uebersicht from "uebersicht";
2import * as DataWidget from "./data-widget.jsx";
3import * as DataWidgetLoader from "./data-widget-loader.jsx";
4import * as Icons from "../icons/icons.jsx";
5import { SuspenseIcon } from "../icons/icon.jsx";
6import useWidgetRefresh from "../../hooks/use-widget-refresh";
7import useServerSocket from "../../hooks/use-server-socket";
8import { useSimpleBarContext } from "../simple-bar-context.jsx";
9import * as Utils from "../../utils";
10
11export { zoomStyles as styles } from "../../styles/components/data/zoom";
12
13const { React } = Uebersicht;
14
15const DEFAULT_REFRESH_FREQUENCY = 5000;
16
17/**
18 * Zoom widget component.
19 * @returns {JSX.Element|null} The Zoom widget.
20 */
21export 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
107Widget.displayName = "Zoom";