aboutsummaryrefslogtreecommitdiff
path: root/users/ryan/modules/simple-bar/simple-bar-source/lib/utils.js
diff options
context:
space:
mode:
Diffstat (limited to 'users/ryan/modules/simple-bar/simple-bar-source/lib/utils.js')
-rwxr-xr-xusers/ryan/modules/simple-bar/simple-bar-source/lib/utils.js828
1 files changed, 828 insertions, 0 deletions
diff --git a/users/ryan/modules/simple-bar/simple-bar-source/lib/utils.js b/users/ryan/modules/simple-bar/simple-bar-source/lib/utils.js
new file mode 100755
index 0000000..f9d0460
--- /dev/null
+++ b/users/ryan/modules/simple-bar/simple-bar-source/lib/utils.js
@@ -0,0 +1,828 @@
1import * as Uebersicht from "uebersicht";
2import * as Settings from "./settings";
3
4/**
5 * Parses a JSON string and returns the corresponding JavaScript object.
6 * Cleans up JSON that contains escape sequences, such as `\\` or `\"`.
7 *
8 * @param {string} json - The JSON string to parse.
9 * @returns {Object|undefined} The parsed JavaScript object, or undefined if parsing fails.
10 */
11export function parseJson(json) {
12 try {
13 // clean up JSON with escape sequences, (i.e.)
14 // empty arrays: [,] -> []
15 // escape sequences: \\" -> "
16 let cleanedJson = json
17 .replace(/\[,+/g, "[")
18 .replace(/,+\]/g, "]")
19 .replace(/,+,/g, ",")
20 .replace(/\[,/g, "[")
21 .replace(/,\]/g, "]")
22 .replace(/\\/g, "\\\\")
23 .replace(/\\"/g, '"');
24 return JSON.parse(cleanedJson);
25 } catch (error) {
26 // eslint-disable-next-line no-console
27 console.error(error, json);
28 return undefined;
29 }
30}
31
32/*!
33 Copyright (c) 2017 Jed Watson.
34 Licensed under the MIT License (MIT), see
35 http://jedwatson.github.io/classnames
36*/
37const hasOwn = {}.hasOwnProperty;
38
39/**
40 * Combines multiple class names into a single string.
41 *
42 * @param {...(string|boolean|undefined|null)} arguments - The class names to combine.
43 * Each argument can be a string, boolean, undefined, or null. Only truthy values will be included.
44 * @returns {string} The combined class names.
45 */
46export function classNames() {
47 let classes = "";
48
49 for (let i = 0; i < arguments.length; i++) {
50 const arg = arguments[i];
51 if (arg) {
52 classes = appendClass(classes, parseValue(arg));
53 }
54 }
55
56 return classes;
57}
58
59function parseValue(arg) {
60 if (typeof arg === "string" || typeof arg === "number") {
61 return arg;
62 }
63 if (typeof arg !== "object") {
64 return "";
65 }
66 if (Array.isArray(arg)) {
67 return classNames.apply(null, arg);
68 }
69 if (
70 arg.toString !== Object.prototype.toString &&
71 !arg.toString.toString().includes("[native code]")
72 ) {
73 return arg.toString();
74 }
75
76 let classes = "";
77 for (let key in arg) {
78 if (hasOwn.call(arg, key) && arg[key]) {
79 classes = appendClass(classes, key);
80 }
81 }
82 return classes;
83}
84
85function appendClass(value, newClass) {
86 if (!newClass) {
87 return value;
88 }
89 if (value) {
90 return value + " " + newClass;
91 }
92 return value + newClass;
93}
94/*!
95 End of Jed Watson's classNames
96*/
97
98const WIDTH = 20;
99const DURATION = 320;
100
101const CACHE_PREFIX = "sb_cmd_";
102const CACHE_CLEANUP_INTERVAL = 300000; // Clean up every 5 minutes
103let _lastCleanup = 0;
104
105/**
106 * Simple string hash for generating cache keys from command strings.
107 * @param {string} str - The string to hash.
108 * @returns {string} A base-36 hash string.
109 */
110function hashString(str) {
111 let hash = 0;
112 for (let i = 0; i < str.length; i++) {
113 const char = str.charCodeAt(i);
114 hash = (hash << 5) - hash + char;
115 hash |= 0;
116 }
117 return Math.abs(hash).toString(36);
118}
119
120/**
121 * Removes expired cache entries from localStorage to prevent unbounded growth.
122 * Uses a conservative 60-second grace period beyond the longest widget refresh.
123 */
124function cleanupExpiredCache() {
125 const now = Date.now();
126 if (now - _lastCleanup < CACHE_CLEANUP_INTERVAL) return;
127 _lastCleanup = now;
128
129 try {
130 const keysToRemove = [];
131 for (let i = 0; i < localStorage.length; i++) {
132 const key = localStorage.key(i);
133 if (key && key.startsWith(CACHE_PREFIX)) {
134 try {
135 const cached = localStorage.getItem(key);
136 if (cached) {
137 const parsed = JSON.parse(cached);
138 // Remove if older than 60 seconds (well beyond any widget refresh)
139 if (now - parsed.t > 60000) {
140 keysToRemove.push(key);
141 }
142 }
143 } catch {
144 // Remove malformed entries
145 keysToRemove.push(key);
146 }
147 }
148 }
149 keysToRemove.forEach((key) => localStorage.removeItem(key));
150 } catch {
151 // Ignore cleanup errors
152 }
153}
154
155/**
156 * Runs a shell command via Uebersicht.run() with cross-display caching.
157 *
158 * In multi-display setups, each display runs its own simple-bar instance.
159 * System-wide data (CPU, memory, battery, etc.) is identical across displays,
160 * so this function uses localStorage (shared across all Übersicht WebViews)
161 * to cache command results and prevent redundant execution.
162 *
163 * Expired cache entries are automatically cleaned up every 5 minutes.
164 *
165 * @param {string} command - The shell command to execute.
166 * @param {number} cacheTimeout - How long (ms) the cached result remains valid.
167 * Typically set to the widget's refresh frequency so that within one refresh
168 * cycle, only one display actually executes the command.
169 * @returns {Promise<string>} The command output (from cache or fresh execution).
170 */
171export async function cachedRun(command, cacheTimeout) {
172 if (!cacheTimeout || cacheTimeout <= 0) {
173 return Uebersicht.run(command);
174 }
175
176 cleanupExpiredCache();
177
178 const cacheKey = CACHE_PREFIX + hashString(command);
179 const now = Date.now();
180
181 try {
182 const cached = localStorage.getItem(cacheKey);
183 if (cached) {
184 const parsed = JSON.parse(cached);
185 if (now - parsed.t < cacheTimeout) {
186 return parsed.r;
187 }
188 }
189 } catch {
190 // Ignore cache read errors
191 }
192
193 const result = await Uebersicht.run(command);
194 try {
195 localStorage.setItem(cacheKey, JSON.stringify({ r: result, t: now }));
196 } catch {
197 // Ignore cache write errors (e.g., storage full)
198 }
199 return result;
200}
201
202/**
203 * Creates a click effect animation at the location of the mouse click event.
204 *
205 * @param {MouseEvent} e - The mouse event object containing the click coordinates.
206 */
207export function clickEffect(e) {
208 const { body } = document;
209 const { clientX, clientY } = e;
210 const cursor = Object.assign(document.createElement("div"), {
211 id: "simple-bar-click-effect",
212 });
213 Object.assign(cursor.style, {
214 top: `${clientY - WIDTH / 2}px`,
215 left: `${clientX - WIDTH / 2}px`,
216 width: `${WIDTH}px`,
217 height: `${WIDTH}px`,
218 transition: `transform ${DURATION} ease`,
219 });
220 if (cursor && "animate" in cursor) {
221 body.appendChild(cursor);
222 cursor.animate(
223 [
224 { opacity: 0, transform: "scale(0)" },
225 { opacity: 1, transform: "scale(2)" },
226 { opacity: 0, transform: "scale(1.6)" },
227 ],
228 { duration: DURATION },
229 );
230 }
231 setTimeout(() => cursor && body.removeChild(cursor), DURATION);
232}
233
234function isObject(item) {
235 return item && typeof item === "object" && !Array.isArray(item);
236}
237
238/**
239 * Deeply merges multiple source objects into a target object.
240 *
241 * @param {Object} target - The target object to merge properties into.
242 * @param {...Object} sources - One or more source objects to merge properties from.
243 * @returns {Object} - The target object after merging.
244 */
245export function mergeDeep(target, ...sources) {
246 if (!sources.length) return target;
247 const source = sources.shift();
248 if (isObject(target) && isObject(source)) {
249 for (const key in source) {
250 if (isObject(source[key])) {
251 if (!target[key]) Object.assign(target, { [key]: {} });
252 mergeDeep(target[key], source[key]);
253 } else {
254 Object.assign(target, { [key]: source[key] });
255 }
256 }
257 }
258 return mergeDeep(target, ...sources);
259}
260
261/*!
262 * (c) 2019 Chris Ferdinandi & Jascha Brinkmann, MIT License, https://gomakethings.com & https://twitter.com/jaschaio
263 */
264/**
265 * Compares two objects and returns the differences.
266 *
267 * @param {Object} obj1 - The first object to compare.
268 * @param {Object} obj2 - The second object to compare.
269 * @returns {Object} An object containing the differences between obj1 and obj2.
270 *
271 * @example
272 * const obj1 = { a: 1, b: 2, c: 3 };
273 * const obj2 = { a: 1, b: 4, d: 5 };
274 * const diffs = compareObjects(obj1, obj2);
275 * // diffs = { b: 4, c: null, d: 5 }
276 */
277export function compareObjects(obj1, obj2) {
278 if (!obj2 || Object.prototype.toString.call(obj2) !== "[object Object]") {
279 return obj1;
280 }
281 const diffs = {};
282 let key;
283 const arraysMatch = (arr1, arr2) => {
284 if (arr1.length !== arr2.length) return false;
285 for (let i = 0; i < arr1.length; i++) {
286 if (arr1[i] !== arr2[i]) return false;
287 }
288 return true;
289 };
290 const compare = (item1, item2, key) => {
291 const type1 = Object.prototype.toString.call(item1);
292 const type2 = Object.prototype.toString.call(item2);
293 if (type2 === "[object Undefined]") {
294 diffs[key] = null;
295 return;
296 }
297 if (type1 !== type2) {
298 diffs[key] = item2;
299 return;
300 }
301 if (type1 === "[object Object]") {
302 const objDiff = compareObjects(item1, item2);
303 if (Object.keys(objDiff).length > 0) {
304 diffs[key] = objDiff;
305 }
306 return;
307 }
308 if (type1 === "[object Array]") {
309 if (!arraysMatch(item1, item2)) {
310 diffs[key] = item2;
311 }
312 return;
313 }
314 if (type1 === "[object Function]") {
315 if (item1.toString() !== item2.toString()) {
316 diffs[key] = item2;
317 }
318 } else {
319 if (item1 !== item2) {
320 diffs[key] = item2;
321 }
322 }
323 };
324 for (key in obj1) {
325 if (Object.prototype.hasOwnProperty.call(obj1, key)) {
326 compare(obj1[key], obj2[key], key);
327 }
328 }
329 for (key in obj2) {
330 if (Object.prototype.hasOwnProperty.call(obj2, key)) {
331 if (!obj1[key] && obj1[key] !== obj2[key]) {
332 diffs[key] = obj2[key];
333 }
334 }
335 }
336 return diffs;
337}
338
339/**
340 * Filters applications based on provided exclusions and title exclusions.
341 *
342 * @param {Object} app - The application object to be filtered.
343 * @param {Array<string>} exclusions - List of application names to be excluded.
344 * @param {Array<string>} titleExclusions - List of application titles to be excluded.
345 * @param {boolean} exclusionsAsRegex - Flag indicating if exclusions should be treated as regular expressions.
346 * @returns {boolean} - Returns true if the application should be included, false otherwise.
347 */
348export function filterApps(
349 app,
350 exclusions,
351 titleExclusions,
352 exclusionsAsRegex,
353) {
354 const fullscreen = app.isNativeFullscreen ?? app.__legacyIsNativeFullscreen;
355 const appName = app.app || app["app-name"];
356 const appTitle = app.title || app["window-title"];
357
358 const isAppNameExcluded = exclusionsAsRegex
359 ? exclusions.length !== 0 && new RegExp(exclusions).test(appName)
360 : exclusions.includes(appName);
361
362 const isAppTitleExcluded = exclusionsAsRegex
363 ? titleExclusions.length !== 0 && new RegExp(titleExclusions).test(appTitle)
364 : titleExclusions.includes(appTitle);
365
366 return (
367 fullscreen ||
368 (!isAppNameExcluded && !appTitle?.length) ||
369 (!isAppNameExcluded && !isAppTitleExcluded)
370 );
371}
372
373export function isSpaceExcluded(id, exclusions /*list*/, exclusionsAsRegex) {
374 const isSpaceNameExcluded = exclusionsAsRegex
375 ? exclusions.some(
376 (rx) =>
377 rx.length !== 0 &&
378 (rx instanceof RegExp ? rx : new RegExp(rx)).test(id),
379 )
380 : exclusions.includes(id);
381
382 return exclusions && isSpaceNameExcluded;
383}
384
385/**
386 * Filters and categorizes windows into sticky and non-sticky based on various criteria.
387 *
388 * @param {Object} params - The parameters for the function.
389 * @param {Array} params.windows - The list of windows to filter.
390 * @param {boolean} params.uniqueApps - Whether to treat apps as unique.
391 * @param {number} params.currentDisplay - The current display identifier.
392 * @param {number} params.currentSpace - The current space identifier.
393 * @param {Array} params.exclusions - The list of app names to exclude.
394 * @param {Array} params.titleExclusions - The list of window titles to exclude.
395 * @param {boolean} params.exclusionsAsRegex - Whether exclusions are regex patterns.
396 * @returns {Object} An object containing two arrays: `nonStickyWindows` and `stickyWindows`.
397 */
398export function stickyWindowWorkaround({
399 windows = [],
400 uniqueApps,
401 currentDisplay,
402 currentSpace,
403 exclusions,
404 titleExclusions,
405 exclusionsAsRegex,
406}) {
407 const stickySet = new Set();
408 const stickyWindows = windows.filter((app) => {
409 const {
410 "is-sticky": isSticky,
411 sticky: __legacyIsSticky,
412 "is-native-fullscreen": isNativeFullscreen,
413 "native-fullscreen": __legacyIsNativeFullscreen,
414 display,
415 app: appName,
416 id,
417 } = app;
418 return (
419 (isSticky ?? __legacyIsSticky) &&
420 !(isNativeFullscreen ?? __legacyIsNativeFullscreen) &&
421 display === currentDisplay &&
422 filterApps(app, exclusions, titleExclusions, exclusionsAsRegex) &&
423 !stickySet.has(uniqueApps ? appName : id) &&
424 stickySet.add(uniqueApps ? appName : id)
425 );
426 });
427 const nonStickySet = new Set();
428 const nonStickyWindows = windows.filter((app) => {
429 const {
430 "is-sticky": isSticky,
431 sticky: __legacyIsSticky,
432 "is-native-fullscreen": isNativeFullscreen,
433 "native-fullscreen": __legacyIsNativeFullscreen,
434 space,
435 app: appName,
436 id,
437 } = app;
438 return (
439 (!(isSticky ?? __legacyIsSticky) ||
440 (isNativeFullscreen ?? __legacyIsNativeFullscreen)) &&
441 space === currentSpace &&
442 filterApps(app, exclusions, titleExclusions, exclusionsAsRegex) &&
443 !nonStickySet.has(uniqueApps ? appName : id) &&
444 nonStickySet.add(uniqueApps ? appName : id)
445 );
446 });
447 return { nonStickyWindows, stickyWindows };
448}
449
450/**
451 * Sorts an array of window objects based on their frame coordinates and stack index.
452 *
453 * The sorting priority is as follows:
454 * 1. `frame.x` - Windows are sorted by their x-coordinate.
455 * 2. `frame.y` - If x-coordinates are equal, windows are sorted by their y-coordinate.
456 * 3. `stack-index` - If both x and y coordinates are equal, windows are sorted by their stack index.
457 * 4. `id` - If all previous criteria are equal, windows are sorted by their id.
458 *
459 * @param {Array} windows - The array of window objects to be sorted.
460 * @param {Object} windows[].frame - The frame object containing x and y coordinates.
461 * @param {number} windows[].frame.x - The x-coordinate of the window.
462 * @param {number} windows[].frame.y - The y-coordinate of the window.
463 * @param {number} windows[].stack-index - The stack index of the window.
464 * @param {number} windows[].id - The unique identifier of the window.
465 * @returns {Array} The sorted array of window objects.
466 */
467export function sortWindows(windows) {
468 return windows.sort((a, b) => {
469 if (a.frame.x !== b.frame.x) {
470 return a.frame.x > b.frame.x;
471 }
472 if (a.frame.y !== b.frame.y) {
473 return a.frame.y > b.frame.y;
474 }
475 if (a["stack-index"] !== b["stack-index"]) {
476 return a["stack-index"] > b["stack-index"];
477 }
478 return a.id > b.id;
479 });
480}
481
482/**
483 * Softly refreshes the Uebersicht widget with the specified ID.
484 *
485 * This function runs an AppleScript command to refresh the widget with the ID "simple-bar-index-jsx"
486 * in the Uebersicht application.
487 *
488 * @async
489 * @function softRefresh
490 * @returns {Promise<void>} A promise that resolves when the refresh command has been executed.
491 */
492export async function softRefresh() {
493 await Uebersicht.run(
494 `osascript -e 'tell application id "tracesOf.Uebersicht" to refresh widget id "simple-bar-index-jsx"'`,
495 );
496}
497
498/**
499 * Triggers a hard refresh of the Uebersicht application by running an AppleScript command.
500 *
501 * @async
502 * @function hardRefresh
503 * @returns {Promise<void>} A promise that resolves when the refresh command has been executed.
504 */
505export async function hardRefresh() {
506 await Uebersicht.run(
507 `osascript -e 'tell application id "tracesOf.Uebersicht" to refresh'`,
508 );
509}
510
511/**
512 * Displays a notification using either the pushMissive function or the system notification.
513 *
514 * @param {string} content - The content of the notification.
515 * @param {Function} pushMissive - A function to push a missive notification.
516 * @param {Object} [options] - Optional settings for the notification.
517 * @param {string} [options.side="right"] - The side where the notification should appear.
518 * @param {number} [options.delay=5000] - The delay before the notification disappears.
519 */
520export function notification(
521 content,
522 pushMissive,
523 { side = "right", delay = 5000 } = {},
524) {
525 const settings = Settings.get();
526 const { disableNotifications, enableMissives } = settings.global;
527 if (disableNotifications) return;
528 if (enableMissives && typeof pushMissive === "function") {
529 pushMissive({ side, content, delay });
530 } else {
531 Uebersicht.run(
532 `osascript -e 'tell app "System Events" to display notification "${content}" with title "simple-bar"'`,
533 );
534 }
535}
536
537/**
538 * Injects styles into the document head. If styles with the given ID already exist, it updates them.
539 * Otherwise, it creates a new style element and appends it to the document head.
540 *
541 * @param {string} id - The ID to assign to the style element.
542 * @param {string[]} [styles=[]] - An array of CSS styles to inject.
543 */
544export function injectStyles(id, styles = []) {
545 const existingStyles = document.getElementById(id);
546 // Merge all styles and minify them
547 const stylesToInject = styles.join("").replace(/\s+/g, " ");
548 if (existingStyles) {
549 existingStyles.innerHTML = stylesToInject;
550 return;
551 }
552 document.head.appendChild(
553 Object.assign(document.createElement("style"), {
554 id,
555 innerHTML: stylesToInject,
556 }),
557 );
558}
559
560const DEFAULT_PACE = 4;
561
562/**
563 * Starts the sliding animation for a given container.
564 *
565 * @param {HTMLElement} container - The container element that holds the inner and slider elements.
566 * @param {string} innerSelector - The CSS selector for the inner element.
567 * @param {string} sliderSelector - The CSS selector for the slider element.
568 */
569export function startSliding(container, innerSelector, sliderSelector) {
570 if (!container) return;
571 const settings = Settings.get();
572 const { slidingAnimationPace = DEFAULT_PACE } = settings.global;
573 const pace =
574 !slidingAnimationPace || slidingAnimationPace < 1
575 ? DEFAULT_PACE
576 : parseInt(slidingAnimationPace);
577 const inner = container.querySelector(innerSelector);
578 const slider = container.querySelector(sliderSelector);
579 const delta = inner.clientWidth - slider.clientWidth;
580 if (delta > 0) return;
581 const timing = Math.round((Math.abs(delta) * 100) / pace);
582 Object.assign(slider.style, {
583 transform: `translateX(${delta}px)`,
584 transition: `transform ${timing}ms linear`,
585 });
586}
587
588/**
589 * Stops the sliding effect by removing the inline style of the slider element.
590 *
591 * @param {HTMLElement} container - The container element that holds the slider.
592 * @param {string} sliderSelector - The CSS selector for the slider element.
593 */
594export function stopSliding(container, sliderSelector) {
595 if (!container) return;
596 container.querySelector(sliderSelector).removeAttribute("style");
597}
598
599/**
600 * Cleans up the given output by trimming whitespace and removing all newline characters.
601 *
602 * @param {string} output - The string output to be cleaned.
603 * @returns {string} - The cleaned output string.
604 */
605export function cleanupOutput(output) {
606 return output?.trim().replace(/(\r\n|\n|\r)/gm, "");
607}
608
609/**
610 * Returns a promise that resolves after a specified number of milliseconds.
611 *
612 * @param {number} ms - The number of milliseconds to wait before the promise resolves.
613 * @returns {Promise<void>} An empty promise that resolves after the specified delay.
614 */
615export function timeout(ms) {
616 return new Promise((resolve) => setTimeout(resolve, ms));
617}
618
619/**
620 * Switches the space (virtual desktop) on macOS.
621 *
622 * @param {number} currentIndex - The current space index.
623 * @param {number} desiredIndex - The desired space index to switch to.
624 * @returns {Promise<void>} A promise that resolves when the space switch is complete.
625 */
626export async function switchSpace(currentIndex, desiredIndex) {
627 const repeats = Math.abs(currentIndex - desiredIndex);
628 const left = currentIndex > desiredIndex;
629 for (let i = 0; i < repeats; i++) {
630 await Uebersicht.run(
631 `osascript -e 'tell app "System Events" to key code ${
632 left ? "123" : "124"
633 } using control down'`,
634 );
635 }
636}
637
638/**
639 * Adds event listeners to handle focus and blur events on the bar element.
640 *
641 * This function selects the element with the class "simple-bar" and adds
642 * event listeners for "click" and "mouseleave" events. When the bar is clicked,
643 * it checks if the click target is the bar itself and then calls the `focusBar` function.
644 * When the mouse leaves the bar, it calls the `blurBar` function.
645 */
646export function handleBarFocus() {
647 const bar = document.querySelector(".simple-bar");
648 if (!bar) return;
649 bar.addEventListener("click", (e) => {
650 if (e.target !== bar) return;
651 focusBar();
652 });
653 bar.addEventListener("mouseleave", blurBar);
654}
655
656/**
657 * Focuses the bar element by adding the "simple-bar--focused" class to it.
658 * If the bar element is not found, the function does nothing.
659 */
660function focusBar() {
661 const bar = document.querySelector(".simple-bar");
662 if (!bar) return;
663 bar.classList.add("simple-bar--focused");
664}
665
666/**
667 * Removes the "simple-bar--focused" class from the element with the class "simple-bar".
668 * If the element is not found, the function does nothing.
669 */
670export function blurBar() {
671 const bar = document.querySelector(".simple-bar");
672 if (!bar) return;
673 bar.classList.remove("simple-bar--focused");
674}
675
676/**
677 * Retrieves the system architecture.
678 *
679 * This function runs two shell commands using Uebersicht to get the system's
680 * architecture and system type. It then processes the output to determine
681 * if the system is using an ARM64 architecture or x86_64.
682 *
683 * The result is cached permanently (in memory and localStorage) since
684 * system architecture never changes at runtime.
685 *
686 * @returns {Promise<"arm64" | "x86_64">} A promise that resolves to a string indicating the system architecture ("arm64" or "x86_64").
687 */
688let _cachedSystem = null;
689
690export async function getSystem() {
691 if (_cachedSystem) return _cachedSystem;
692 try {
693 const stored = localStorage.getItem("sb_system_arch");
694 if (stored) {
695 _cachedSystem = stored;
696 return _cachedSystem;
697 }
698 } catch {
699 // Ignore localStorage errors
700 }
701 const [bareArchitecture, bareSystem] = await Promise.all([
702 Uebersicht.run("uname -a"),
703 Uebersicht.run("uname -m"),
704 ]);
705 const architecture = cleanupOutput(bareArchitecture);
706 const system = cleanupOutput(bareSystem);
707 if (
708 system.startsWith("arm64") ||
709 (system.startsWith("x86_64") && architecture.includes("ARM64"))
710 ) {
711 _cachedSystem = "arm64";
712 } else {
713 _cachedSystem = "x86_64";
714 }
715 try {
716 localStorage.setItem("sb_system_arch", _cachedSystem);
717 } catch {
718 // Ignore localStorage errors
719 }
720 return _cachedSystem;
721}
722
723/**
724 * Checks if a display is visible based on the provided setting.
725 *
726 * @param {number} display - The display number to check.
727 * @param {string} setting - A comma-separated string of display numbers.
728 * @returns {boolean} - Returns true if the display is visible, otherwise false.
729 */
730export function isVisibleOnDisplay(display, setting) {
731 try {
732 if (!setting?.trim()) return true;
733 const displays = setting.split(",").map((d) => parseInt(d, 10));
734 return displays.includes(display);
735 } catch {
736 return true;
737 }
738}
739
740/**
741 * Adds a value to the graph history and ensures the graph does not exceed the specified maximum length.
742 *
743 * @param {any} value - The value to add to the graph history.
744 * @param {function} setGraph - The state setter function for updating the graph.
745 * @param {number} maxLength - The maximum length of the graph history.
746 */
747export function addToGraphHistory(value, setGraph, maxLength) {
748 setGraph((graph) => {
749 const newGraph = [...graph, value];
750 if (newGraph.length > maxLength) {
751 newGraph.shift();
752 }
753 return newGraph;
754 });
755}
756
757/**
758 * Parses a string value into an integer and returns it if valid, otherwise returns a default value.
759 *
760 * @param {string} value - The string value to parse.
761 * @param {number} defaultValue - The default value to return if parsing fails.
762 * @returns {number} - The parsed integer or the default value if parsing fails.
763 */
764export function getRefreshFrequency(value, defaultValue) {
765 const parsedValue = parseInt(value, 10);
766 return isNaN(parsedValue) ? defaultValue : parsedValue;
767}
768
769/**
770 * Executes a given command in the user's preferred terminal application.
771 *
772 * The terminal application is determined by the global settings.
773 * Supported terminals are "Terminal" and "iTerm2".
774 *
775 * @param {string} command - The command to be executed in the terminal.
776 */
777export function runInUserTerminal(command) {
778 const settings = Settings.get();
779 const { terminal } = settings.global;
780 switch (terminal) {
781 case "Terminal":
782 Uebersicht.run(
783 `osascript ./simple-bar/lib/scripts/run-command-in-terminal.applescript` +
784 ` "${command}"`,
785 );
786 break;
787 case "iTerm2":
788 Uebersicht.run(
789 `osascript ./simple-bar/lib/scripts/run-command-in-iterm2.applescript` +
790 ` "${command}"`,
791 );
792 break;
793 default:
794 break;
795 }
796}
797
798/**
799 * Formats a given number of bytes into a more readable string with appropriate units.
800 *
801 * @param {number} bytes - The number of bytes to format.
802 * @param {number} [decimals=1] - The number of decimal places to include in the formatted string.
803 * @returns {string} The formatted string with the appropriate unit.
804 */
805export function formatBytes(bytes, decimals = 1) {
806 if (!+bytes) return "0b";
807
808 const k = 1024;
809 const dm = decimals < 0 ? 0 : decimals;
810 const sizes = ["b", "kb", "mb", "gb", "tb", "pb", "eb", "zb", "yb"];
811
812 const i = Math.floor(Math.log(bytes) / Math.log(k));
813
814 return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))}<em>${
815 sizes[i]
816 }</em>`;
817}
818
819/**
820 * Normalizes an application name by removing any left-to-right mark characters (U+200E).
821 *
822 * @param {string} name - The application name to normalize.
823 * @returns {string} The normalized application name without left-to-right mark characters.
824 */
825export function normalizeAppName(name) {
826 if (!name) return "";
827 return name.replace(/[\u200E]/g, "");
828}