diff options
Diffstat (limited to 'users/ryan/modules/simple-bar/simple-bar-source/lib/utils.js')
| -rwxr-xr-x | users/ryan/modules/simple-bar/simple-bar-source/lib/utils.js | 828 |
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 @@ | |||
| 1 | import * as Uebersicht from "uebersicht"; | ||
| 2 | import * 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 | */ | ||
| 11 | export 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 | */ | ||
| 37 | const 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 | */ | ||
| 46 | export 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 | |||
| 59 | function 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 | |||
| 85 | function 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 | |||
| 98 | const WIDTH = 20; | ||
| 99 | const DURATION = 320; | ||
| 100 | |||
| 101 | const CACHE_PREFIX = "sb_cmd_"; | ||
| 102 | const CACHE_CLEANUP_INTERVAL = 300000; // Clean up every 5 minutes | ||
| 103 | let _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 | */ | ||
| 110 | function 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 | */ | ||
| 124 | function 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 | */ | ||
| 171 | export 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 | */ | ||
| 207 | export 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 | |||
| 234 | function 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 | */ | ||
| 245 | export 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 | */ | ||
| 277 | export 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 | */ | ||
| 348 | export 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 | |||
| 373 | export 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 | */ | ||
| 398 | export 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 | */ | ||
| 467 | export 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 | */ | ||
| 492 | export 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 | */ | ||
| 505 | export 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 | */ | ||
| 520 | export 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 | */ | ||
| 544 | export 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 | |||
| 560 | const 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 | */ | ||
| 569 | export 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 | */ | ||
| 594 | export 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 | */ | ||
| 605 | export 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 | */ | ||
| 615 | export 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 | */ | ||
| 626 | export 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 | */ | ||
| 646 | export 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 | */ | ||
| 660 | function 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 | */ | ||
| 670 | export 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 | */ | ||
| 688 | let _cachedSystem = null; | ||
| 689 | |||
| 690 | export 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 | */ | ||
| 730 | export 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 | */ | ||
| 747 | export 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 | */ | ||
| 764 | export 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 | */ | ||
| 777 | export 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 | */ | ||
| 805 | export 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 | */ | ||
| 825 | export function normalizeAppName(name) { | ||
| 826 | if (!name) return ""; | ||
| 827 | return name.replace(/[\u200E]/g, ""); | ||
| 828 | } | ||
