function isPlainObject(value) { return !!value && typeof value === "object" && !Array.isArray(value) } function normalizePosition(value) { var next = String(value || "").trim() return /^(top|bottom|left|right)$/.test(next) ? next : "top" } function entrySettings(entry) { if (!isPlainObject(entry)) return {} var copy = {} for (var key in entry) { if (key === "id") continue copy[key] = entry[key] } return copy } function entryId(entry) { if (typeof entry === "string") return entry if (isPlainObject(entry)) { var id = entry["id"] if (id !== undefined && id !== null && String(id) !== "") return String(id) } return "" } function pinTrayToInner(entries, section) { var trayEntry = null var result = [] var values = Array.isArray(entries) ? entries : [] for (var i = 0; i < values.length; i++) { if (entryId(values[i]) === "omarchy.tray") trayEntry = values[i] else result.push(values[i]) } if (trayEntry) { if (section === "right") result.unshift(trayEntry) else result.push(trayEntry) } return result } function moduleString(entry, key, fallback) { var settings = entrySettings(entry) var value = settings[key] return value === undefined || value === null ? fallback : String(value) } function entryIndex(entries, name) { if (!Array.isArray(entries)) return -1 for (var i = 0; i < entries.length; i++) { if (entryId(entries[i]) === name) return i } return -1 } function entriesBefore(entries, name) { var index = entryIndex(entries, name) return index <= 0 ? [] : entries.slice(0, index) } function entriesAfter(entries, name) { var index = entryIndex(entries, name) return index === -1 ? [] : entries.slice(index + 1) } // A shell.json write that only changes inline widget settings (the battery // percentage toggle, a clock format change) must not rebuild the bar. // Compare two normalized layouts: when the structure is unchanged — same // entry ids in the same order per region — return the settings-only changes // as {region, index, entry}. Return null when the change is structural, or // touches an entry a live settings push cannot safely reach: custom modules // read their entry directly rather than an injected settings property, and // a duplicated id makes the push ambiguous. function inlineSettingsDelta(current, next) { if (!isPlainObject(current) || !isPlainObject(next)) return null var regions = ["left", "center", "right"] var counts = {} for (var r = 0; r < regions.length; r++) { var entries = Array.isArray(next[regions[r]]) ? next[regions[r]] : [] for (var i = 0; i < entries.length; i++) { var id = entryId(entries[i]) counts[id] = (counts[id] || 0) + 1 } } var changes = [] for (var s = 0; s < regions.length; s++) { var region = regions[s] var a = Array.isArray(current[region]) ? current[region] : [] var b = Array.isArray(next[region]) ? next[region] : [] if (a.length !== b.length) return null for (var j = 0; j < a.length; j++) { if (entryId(a[j]) !== entryId(b[j])) return null if (JSON.stringify(a[j]) === JSON.stringify(b[j])) continue if (customModuleType(a[j]) || customModuleType(b[j])) return null if (counts[entryId(b[j])] > 1) return null changes.push({ region: region, index: j, entry: b[j] }) } } return changes } function expandPath(value, home) { var path = String(value || "") if (path === "") return "" if (path.indexOf("~/") === 0) return home + path.substring(1) if (path.indexOf("$HOME/") === 0) return home + path.substring(5) return path } function customModuleSafeName(name) { var value = String(name || "") return value !== "" && value.indexOf("..") === -1 && value[0] !== "/" } function customModuleType(entry) { var settings = entrySettings(entry) var type = String(settings.type || "") if (type) return type if (settings.exec) return "command" if (settings.source) return "qml" return "" } function customModulePath(entry, home, configDir) { var settings = entrySettings(entry) var name = entryId(entry) var source = settings.source ? expandPath(settings.source, home) : "" if (!source && customModuleSafeName(name)) source = String(configDir || "") + "/bar/modules/" + String(name) + ".qml" return source } // A center module is mounted twice once an anchor is set: the copy that is // actually drawn, and a zero-size placeholder holding its place in the flow // beside the anchor. Panel routing has to pick the drawn one — it is the only // one that can anchor a popup, carry the open-panel mark, or be found again // by switchPanelFrom — and fall back to the placeholder only when nothing is // on screen. The order the two are registered in is not stable across a live // bar reconfiguration, so picking the first match is not good enough. function isDrawnSlot(slot) { return !!slot && slot.visible === true && slot.width > 0 && slot.height > 0 } function pickDrawnSlot(slots) { var placeholder = null var list = slots || [] for (var i = 0; i < list.length; i++) { if (!list[i]) continue if (isDrawnSlot(list[i])) return list[i] if (!placeholder) placeholder = list[i] } return placeholder } // Resolve a pointer anywhere along the bar to the closest insertion edge. // Requiring the pointer to sit inside another widget makes the empty space // around a centered group a dead zone, even though it visually reads as the // most natural place to drop. function nearestDropTarget(candidates, point, vertical) { var rows = Array.isArray(candidates) ? candidates : [] var axis = vertical ? Number(point && point.y) : Number(point && point.x) if (!isFinite(axis)) return null var best = null var bestDistance = Infinity for (var i = 0; i < rows.length; i++) { var row = rows[i] if (!row || !row.slot) continue var start = Number(vertical ? row.y : row.x) var size = Number(vertical ? row.height : row.width) if (!isFinite(start) || !isFinite(size) || size <= 0) continue var beforeDistance = Math.abs(axis - start) var afterDistance = Math.abs(axis - (start + size)) var after = afterDistance < beforeDistance var distance = after ? afterDistance : beforeDistance if (distance < bestDistance) { best = { slot: row.slot, after: after } bestDistance = distance } } return best } if (typeof module !== "undefined") { module.exports = { isDrawnSlot: isDrawnSlot, pickDrawnSlot: pickDrawnSlot, nearestDropTarget: nearestDropTarget, normalizePosition: normalizePosition, entrySettings: entrySettings, entryId: entryId, pinTrayToInner: pinTrayToInner, moduleString: moduleString, entryIndex: entryIndex, entriesBefore: entriesBefore, entriesAfter: entriesAfter, inlineSettingsDelta: inlineSettingsDelta, expandPath: expandPath, customModuleSafeName: customModuleSafeName, customModuleType: customModuleType, customModulePath: customModulePath } }