.pragma library // Switchboard.js — every piece of logic that does not need a compositor. // // Kept free of QML types on purpose: this file is unit-tested under `node // --test`, and the QML above it stays declarative. Nothing here reads a // global, throws, or mutates its arguments. // ---------------------------------------------------------------- safe reads // Hyprland's `lastIpcObject` is compositor data arriving over a socket. A // missing key here would take down the whole shell process, since plugins run // unsandboxed inside omarchy-shell, so every read goes through these. function ipcField(toplevel, key, fallback) { if (!toplevel) return fallback var obj = toplevel.lastIpcObject if (!obj || typeof obj !== "object") return fallback var value = obj[key] return value === undefined || value === null ? fallback : value } function windowClass(toplevel) { return String(ipcField(toplevel, "class", "")) } // `size` and `at` arrive as QVariantLists, which are indexable but fail // Array.isArray — the same boundary problem as the toplevel model. Rejecting // them made every window fall back to the same placeholder aspect ratio, which // quietly defeated the entire point of laying tiles out at their true shape. function windowSize(toplevel) { var size = toArray(ipcField(toplevel, "size", null)) if (size.length < 2) return null var w = Number(size[0]) var h = Number(size[1]) if (!isFinite(w) || !isFinite(h) || w <= 0 || h <= 0) return null return { width: w, height: h } } function windowRect(toplevel) { var at = toArray(ipcField(toplevel, "at", null)) var size = windowSize(toplevel) if (at.length < 2 || !size) return null var x = Number(at[0]) var y = Number(at[1]) if (!isFinite(x) || !isFinite(y)) return null return { x: x, y: y, width: size.width, height: size.height } } // Falls back to 16:10 rather than 1:1 — an unknown window is far more likely // to be a normal app window than a perfect square. function aspectRatio(toplevel) { var size = windowSize(toplevel) if (!size) return 1.6 return clampNumber(size.width / size.height, 0.25, 6.0) } function clampNumber(value, min, max) { if (!isFinite(value)) return min return value < min ? min : (value > max ? max : value) } // Quickshell hands QML an UntypedObjectModel whose `values` is array-*like* but // fails Array.isArray. Everything downstream iterates a real array, so the // conversion happens once, here, at the boundary. function toArray(value) { if (Array.isArray(value)) return value if (!value || typeof value.length !== "number") return [] var out = [] for (var i = 0; i < value.length; i++) out.push(value[i]) return out } // ---------------------------------------------------------- bounded config IO // FileView eagerly retains the complete target before exposing text(). That is // unsafe for a fixed path the user can replace with a huge file or a symlink. // Read one byte beyond a small, divisible-by-three ceiling and Base64 the // bounded result so QML can distinguish the exact byte limit after UTF-8 // decoding. `timeout` also keeps a FIFO or device from pinning a reader. function configReadCommand(path, maxBytes) { var limit = Math.max(3, Math.floor(Number(maxBytes) || 0)) limit -= limit % 3 var script = [ "set -o pipefail", 'if [[ ! -e "$1" ]]; then printf M; exit 0; fi', 'payload=$(timeout --signal=KILL 1 head -c "$2" -- "$1" | base64 -w0) || { printf E; exit 0; }', 'printf "D%s" "$payload"' ].join("\n") return ["bash", "-c", script, "switchboard-config-reader", String(path || ""), String(limit + 1)] } function classifyConfigPayload(payload, maxBytes) { var value = String(payload || "") if (value === "M") return { status: "missing" } if (value === "E") return { status: "error" } if (value.charAt(0) !== "D") return { status: "error" } var limit = Math.max(3, Math.floor(Number(maxBytes) || 0)) limit -= limit % 3 var encoded = value.slice(1) if (encoded.length > (limit / 3) * 4) return { status: "oversized" } return { status: "data", encoded: encoded } } // ------------------------------------------------------------ display naming // Chrome and Chromium mangle their WM class for web apps and profiles: // chrome-x.com__-Default // chrome-__home_bmw_Work_pocket_docs_demo_pocket-demo.html-Default // Left raw, those dominate a tile chip and read as noise. Recover something a // person would actually call the window. function prettyClass(raw) { var cls = String(raw || "").trim() if (cls === "") return "" var webApp = cls.match(/^(?:chrome|chromium|brave|edge|vivaldi)-(.+?)(?:-(?:Default|Profile[ _-]?\d+))?$/i) if (webApp) { var slug = webApp[1] // A file:// web app: keep the final path segment, drop the extension. if (slug.indexOf("__") === 0 || slug.indexOf("_home_") === 0) { var tail = slug.split("_").filter(function (part) { return part !== "" }).pop() || slug return titleCase(tail.replace(/\.[a-z0-9]+$/i, "").replace(/[-.]+/g, " ")) } // A hosted web app: chrome encodes "/" as "_", so the host is the head. var host = slug.split("__")[0].split("_")[0] return titleCase(hostLabel(host)) } // Ordinary reverse-DNS ids: org.gnome.Nautilus -> Nautilus if (cls.indexOf(".") !== -1 && !/\s/.test(cls)) { var last = cls.split(".").pop() if (last && last.length > 1) return titleCase(last) } return titleCase(cls) } // Generic prefixes carry no identity — app.slack.com is Slack, not "App". var HOST_NOISE = ["www", "app", "web", "my", "portal", "console", "dashboard"] // Picks the label a person would name the site by: drop the TLD, then take the // first meaningful label. mail.google.com is Mail, app.slack.com is Slack, // github.com is Github, x.com is X. function hostLabel(host) { var parts = String(host || "").toLowerCase().split(".").filter(function (p) { return p !== "" }) if (parts.length === 0) return "" if (parts.length > 1) parts.pop() // the TLD never identifies the app for (var i = 0; i < parts.length; i++) { if (HOST_NOISE.indexOf(parts[i]) === -1) return parts[i] } return parts[parts.length - 1] } function titleCase(value) { var text = String(value || "").replace(/[_-]+/g, " ").trim() if (text === "") return "" // Leave deliberate casing alone (VLC, iTerm, kdeconnect-app stays kdeconnect). if (/[A-Z]/.test(text.slice(1))) return text return text.charAt(0).toUpperCase() + text.slice(1) } // What the tile chip shows, and what the filter matches against. function searchHaystack(toplevel) { var cls = windowClass(toplevel) var pretty = prettyClass(cls) var title = String(toplevel && toplevel.title ? toplevel.title : "") return { app: pretty, rawClass: cls, title: title } } // -------------------------------------------------------------- fuzzy filter // Subsequence matching with a contiguity bonus: "fir" hits Firefox, "chgpt" // hits ChatGPT. Returns the matched index ranges so the chip can highlight // exactly the characters that earned the match. function fuzzyMatch(query, text) { var q = String(query || "") var t = String(text || "") if (q === "") return { matched: true, score: 0, ranges: [] } if (t === "") return { matched: false, score: 0, ranges: [] } var ql = q.toLowerCase() var tl = t.toLowerCase() // A plain substring hit always beats a scattered one, and needs no search. var direct = tl.indexOf(ql) if (direct !== -1) { var atStart = direct === 0 var atWord = direct > 0 && /[\s\-_/.]/.test(tl.charAt(direct - 1)) var score = 1000 - direct + (atStart ? 500 : (atWord ? 250 : 0)) return { matched: true, score: score, ranges: [[direct, direct + ql.length]] } } var indices = [] var ti = 0 for (var qi = 0; qi < ql.length; qi++) { var found = -1 while (ti < tl.length) { if (tl.charAt(ti) === ql.charAt(qi)) { found = ti; ti++; break } ti++ } if (found === -1) return { matched: false, score: 0, ranges: [] } indices.push(found) } var contiguous = 0 for (var i = 1; i < indices.length; i++) { if (indices[i] === indices[i - 1] + 1) contiguous++ } var spread = indices[indices.length - 1] - indices[0] var fuzzyScore = 200 + contiguous * 40 - spread * 2 - indices[0] return { matched: true, score: fuzzyScore, ranges: rangesFromIndices(indices) } } function rangesFromIndices(indices) { var ranges = [] if (indices.length === 0) return ranges var start = indices[0] var prev = indices[0] for (var i = 1; i < indices.length; i++) { if (indices[i] !== prev + 1) { ranges.push([start, prev + 1]) start = indices[i] } prev = indices[i] } ranges.push([start, prev + 1]) return ranges } // Matches app name and title independently, then keeps the better of the two — // the app name is weighted up, because "fir" should mean Firefox the app before // it means a page that happens to mention firs. function matchWindow(query, toplevel) { var hay = searchHaystack(toplevel) if (String(query || "") === "") { return { matched: true, score: 0, field: "", ranges: [] } } var onApp = fuzzyMatch(query, hay.app) var onTitle = fuzzyMatch(query, hay.title) var onRaw = fuzzyMatch(query, hay.rawClass) var best = { matched: false, score: -1, field: "", ranges: [] } if (onApp.matched && onApp.score + 300 > best.score) { best = { matched: true, score: onApp.score + 300, field: "app", ranges: onApp.ranges } } if (onTitle.matched && onTitle.score > best.score) { best = { matched: true, score: onTitle.score, field: "title", ranges: onTitle.ranges } } // The raw class is a last resort so "chrome" still finds a web app whose // pretty name no longer contains it. It never contributes highlight ranges, // since the raw class is not what the chip displays. if (!best.matched && onRaw.matched) { best = { matched: true, score: onRaw.score - 200, field: "", ranges: [] } } return best } // ------------------------------------------------------- compositor commands // Quickshell reports a toplevel address without the 0x prefix ("556b5b461e50"), // while Hyprland's selectors require it. Getting this wrong makes every action // fail silently, since an unmatched selector is not an error. function hyprAddress(address) { var value = String(address || "").trim() if (value === "") return "" return value.indexOf("0x") === 0 ? value : "0x" + value } // Hyprland 0.56 replaced string dispatchers with a Lua API, so // "movetoworkspacesilent N,address:0x…" no longer parses. These build the // current forms, verified against the running compositor. function focusCommand(address) { var target = hyprAddress(address) if (target === "") return "" return 'hl.dsp.focus({ window = "address:' + target + '" })' } // `follow = false` is the point of moving from an overview: the user is // reorganizing their desktop, not asking to be taken somewhere else. function moveCommand(address, workspaceId) { var target = hyprAddress(address) var workspace = String(workspaceId === undefined || workspaceId === null ? "" : workspaceId).trim() if (target === "" || workspace === "") return "" return 'hl.dsp.window.move({ window = "address:' + target + '", workspace = "' + workspace + '", follow = false })' } // ------------------------------------------------------------------ dragging // Which workspace column a pointer at `x` is over, or -1 for none. // // Columns are a plain horizontal strip, so this is arithmetic rather than hit // testing against live items — which keeps the drop target resolvable from the // overlay without reaching into the section delegates. function columnIndexAt(x, sectionCount, colWidth, metrics) { var m = defaultMetrics(metrics) var n = Math.max(0, Math.floor(sectionCount)) if (n <= 0 || !(colWidth > 0)) return -1 if (!isFinite(x) || x < 0) return -1 var stride = colWidth + m.columnGap var index = Math.floor(x / stride) if (index < 0 || index >= n) return -1 // Inside the gap after a column counts as no target, so a drop between two // workspaces does nothing rather than guessing. if (x - index * stride > colWidth) return -1 return index } // --------------------------------------------------------------- highlighting function escapeHtml(value) { return String(value === undefined || value === null ? "" : value) .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """) } // Renders a label as Text.StyledText markup with the matched characters tinted. // Window titles are attacker-adjacent data — any web page can set one — so every // segment is escaped before it reaches a rich-text renderer. function highlightHtml(text, ranges, color) { var source = String(text === undefined || text === null ? "" : text) if (!Array.isArray(ranges) || ranges.length === 0) return escapeHtml(source) var out = "" var cursor = 0 for (var i = 0; i < ranges.length; i++) { if (!Array.isArray(ranges[i]) || ranges[i].length < 2) continue var start = Math.max(0, Math.min(source.length, Number(ranges[i][0]))) var end = Math.max(start, Math.min(source.length, Number(ranges[i][1]))) if (!isFinite(start) || !isFinite(end)) continue if (start >= end) continue // a stale range against shorter text wraps nothing if (start < cursor) continue // defensive: overlapping ranges never double-wrap out += escapeHtml(source.slice(cursor, start)) out += '' out += escapeHtml(source.slice(start, end)) out += "" cursor = end } out += escapeHtml(source.slice(cursor)) return out } // Titles arrive carrying spinner characters and progress noise from running // processes ("⠋ - Building… - project"). Trim the churn so a tile chip stays // legible instead of rewriting itself several times a second. function cleanTitle(value) { return String(value === undefined || value === null ? "" : value) .replace(/[⠀-⣿■-◿─-╿]/g, "") .replace(/\s{2,}/g, " ") .replace(/^[\s\-–—·|]+/, "") .trim() } // ------------------------------------------------------------- grouping // A cheap fingerprint of everything that affects layout: which windows exist, // where they live, and how big they are. // // The overlay rebuilds its structure only when this string changes. That guard // is doing two jobs at once. It lets the overlay poll for changes the property // bindings miss — a toplevel's `workspace` arrives after the toplevel itself, // and mutating a child property does not retrigger a binding on the parent // list — while making sure an unchanged desktop never produces a new model // object, which would rebuild every delegate and restart every live capture. function structureSignature(toplevels, activeWorkspaceId) { var list = toArray(toplevels) var parts = [] for (var i = 0; i < list.length; i++) { var tl = list[i] if (!isRealWindow(tl)) continue var size = windowSize(tl) parts.push([ String(tl.address || ""), String(tl.workspace.id), size ? size.width + "x" + size.height : "?", tl.urgent === true ? "u" : "" ].join(":")) } // Sorted so a reordering of the same windows is not mistaken for a change. parts.sort() return String(activeWorkspaceId) + "|" + parts.join(",") } // A toplevel with no workspace is not a real desktop window — it is a layer // surface or the shell's own window. The spike saw exactly one of these. function isRealWindow(toplevel) { if (!toplevel) return false if (!toplevel.workspace) return false var id = Number(toplevel.workspace.id) return isFinite(id) && id !== 0 } // Ordinary workspaces ascend; Hyprland's special/scratchpad workspaces carry // negative ids and sink to the bottom under their own heading. function compareWorkspaces(a, b) { var aSpecial = a.id < 0 var bSpecial = b.id < 0 if (aSpecial !== bSpecial) return aSpecial ? 1 : -1 return a.id - b.id } function workspaceLabel(workspace) { if (!workspace) return "Workspace" var name = String(workspace.name || "").trim() var id = Number(workspace.id) if (id < 0) return name.replace(/^special:/, "") || "Scratchpad" // Hyprland names numeric workspaces after their own id; only a renamed // workspace is worth showing as more than a number. if (name === "" || name === String(id)) return "Workspace " + id return name } // Builds the structural render model: sections in display order, each holding // its windows. Deliberately knows nothing about the search query. // // The split matters for more than tidiness. QML rebuilds a Repeater's delegates // whenever its model object identity changes, and a rebuilt delegate restarts // its ScreencopyView — so if the query were baked in here, every keystroke // would tear down and restart every live capture on screen. Structure changes // only when windows do; match state rides alongside via applyQuery(). function buildModel(toplevels, activeWorkspaceId, options) { var opts = options || {} var includeScratchpad = opts.includeScratchpad !== false var list = toArray(toplevels) var groups = {} var order = [] var total = 0 for (var i = 0; i < list.length; i++) { var tl = list[i] if (!isRealWindow(tl)) continue var ws = tl.workspace if (!includeScratchpad && Number(ws.id) < 0) continue var key = String(ws.id) if (!groups[key]) { groups[key] = { id: Number(ws.id), label: workspaceLabel(ws), active: Number(ws.id) === Number(activeWorkspaceId), windows: [] } order.push(groups[key]) } var hay = searchHaystack(tl) groups[key].windows.push({ toplevel: tl, address: String(tl.address || ""), app: hay.app, title: hay.title, aspect: aspectRatio(tl), rect: windowRect(tl), urgent: tl.urgent === true, activated: tl.activated === true }) total++ } order.sort(compareWorkspaces) return { sections: order, total: total } } // Resolves match state for a query against an already-built model. // // Badges are assigned in final reading order across every section, so the digit // on a tile always matches where the eye lands. Only matching windows get one — // a badge on a dimmed tile would be a key that does nothing. Beyond nine // matches the rest go unbadged rather than wrapping onto two-key labels; if you // have ten candidates on screen, typing another letter is faster than reading. function applyQuery(model, query) { var states = {} var order = [] var matched = 0 var badge = 1 if (!model || !Array.isArray(model.sections)) { return { states: states, order: order, matched: 0, total: 0 } } for (var s = 0; s < model.sections.length; s++) { var windows = model.sections[s].windows for (var w = 0; w < windows.length; w++) { var entry = windows[w] var match = matchWindow(query, entry.toplevel) var assigned = 0 if (match.matched) { matched++ order.push(entry.address) if (badge <= 9) { assigned = badge; badge++ } } states[entry.address] = { matched: match.matched, score: match.score, field: match.field, ranges: match.ranges, badge: assigned } } } return { states: states, order: order, matched: matched, total: model.total } } // What a tile falls back to before applyQuery has run, or for a window that // appeared between the two. Visible and reachable is the safe default — an // unknown window must never be silently unreachable. function neutralState() { return { matched: true, score: 0, field: "", ranges: [], badge: 0 } } // The address the badge digit refers to, or "" when that digit is unassigned. function addressForBadge(queryState, digit) { if (!queryState || !queryState.states) return "" for (var address in queryState.states) { if (queryState.states[address].badge === digit) return address } return "" } // ------------------------------------------------------- justified row layout // Photo-gallery packing: rows share a height, widths follow each window's true // aspect ratio, and every full row fills the container exactly. This is what // keeps a tall terminal tall and a wide browser wide, so the overview reads as // your actual desktop instead of a wall of identical rectangles. function justify(items, containerWidth, options) { var opts = options || {} var target = opts.targetHeight || 200 var gap = opts.gap === undefined ? 12 : opts.gap var minHeight = opts.minHeight || Math.round(target * 0.62) var maxHeight = opts.maxHeight || Math.round(target * 1.45) var rows = [] var list = toArray(items) if (list.length === 0) return rows if (!(containerWidth > 0)) return rows var row = [] var aspectSum = 0 function flush(isLast) { if (row.length === 0) return var available = containerWidth - gap * (row.length - 1) // The height at which this row exactly fills the container. Never exceed // it: raising the height above this makes the row wider than the column, // and a window's shape is not ours to distort to hide that. var exact = available / aspectSum // A last row must not stretch — a couple of leftover windows blown up to // full width is the classic justified-gallery tell and reads as a bug. var height = isLast ? Math.min(exact, target) : exact height = Math.min(height, maxHeight) // minHeight is advisory, and only applies where it cannot cause overflow. // A full row of wide windows is genuinely short; forcing it taller would // push it past the container edge. if (height < minHeight && minHeight <= exact) height = minHeight var rounded = Math.round(height) var cells = [] var used = 0 for (var i = 0; i < row.length; i++) { var width = Math.max(1, Math.round(row[i].aspect * height)) cells.push({ item: row[i].item, width: width, height: rounded }) used += width } // Absorb sub-pixel rounding so a full row lands flush on the container edge // instead of a pixel short. Strictly bounded to rounding error — one pixel // per cell — because anything larger would be a layout fault being hidden // by silently reshaping a window. if (!isLast) { var drift = containerWidth - (used + gap * (row.length - 1)) if (drift !== 0 && Math.abs(drift) <= row.length) { var widest = 0 for (var c = 1; c < cells.length; c++) if (cells[c].width > cells[widest].width) widest = c cells[widest].width += drift } } rows.push({ height: rounded, cells: cells }) row = [] aspectSum = 0 } for (var i = 0; i < list.length; i++) { var aspect = clampNumber(Number(list[i].aspect) || 1.6, 0.25, 6.0) row.push({ item: list[i], aspect: aspect }) aspectSum += aspect var projected = aspectSum * target + gap * (row.length - 1) if (projected >= containerWidth) flush(false) } flush(true) return rows } // ------------------------------------------------------------ column packing // Default geometry of the chrome around the tiles. Mirrors WorkspaceSection. function defaultMetrics(overrides) { var m = { gap: 14, // between tiles inside a workspace columnGap: 22, // between workspace columns chipHeight: 30, headerHeight: 20, headerGap: 10, minColumnWidth: 260, minTileHeight: 96, maxTileHeight: 0 // 0 = bounded only by the column } var o = overrides || {} for (var key in m) if (o[key] !== undefined && o[key] !== null) m[key] = o[key] return m } // Workspaces are laid out as side-by-side columns rather than stacked bands. // // Stacking wasted almost all of the width on a wide screen: four workspaces // holding one window each used a fifth of the display and read as a sidebar. // Columns spend the width on the tiles, they match the order workspaces // already have in the user's head, and they give drag-to-move an obvious // target. // // Below minColumnWidth the strip stops shrinking and scrolls sideways, so many // workspaces degrade into panning rather than slivers. function columnWidth(sectionCount, availableWidth, metrics) { var m = defaultMetrics(metrics) var n = Math.max(0, Math.floor(sectionCount)) if (n <= 0 || !(availableWidth > 0)) return 0 var raw = (availableWidth - m.columnGap * (n - 1)) / n return Math.max(m.minColumnWidth, Math.floor(raw)) } function chunkEvenly(list, perRow) { var out = [] for (var i = 0; i < list.length; i += perRow) out.push(list.slice(i, i + perRow)) return out } // Lays one workspace's windows out inside its column. // // Rather than greedily filling each row to the column width, this tries every // row count and keeps the one that makes the smallest tile as large as // possible while still fitting the column's height. Greedy filling produced a // cliff: two windows would sit side by side at half height, leaving two thirds // of the column empty, purely because a third window would not have fit. // // Tile heights therefore differ between columns — a busy workspace shows // smaller tiles than a quiet one. That is the same bargain Mission Control // makes, and it beats sizing every column for the worst one. function packColumn(windows, colWidth, colHeight, metrics) { var m = defaultMetrics(metrics) var list = toArray(windows) var empty = { rows: [], height: 0 } if (list.length === 0 || !(colWidth > 0)) return empty var chrome = m.headerHeight + m.headerGap var available = Math.max(0, colHeight - chrome) var aspects = [] for (var i = 0; i < list.length; i++) { aspects.push(clampNumber(Number(list[i].aspect) || 1.6, 0.25, 6.0)) } var best = null for (var rowCount = 1; rowCount <= list.length; rowCount++) { var perRow = Math.ceil(list.length / rowCount) var chunks = chunkEvenly(list, perRow) var aspectChunks = chunkEvenly(aspects, perRow) var heights = [] var smallest = Infinity var stack = m.gap * (chunks.length - 1) for (var c = 0; c < chunks.length; c++) { var sum = 0 for (var a = 0; a < aspectChunks[c].length; a++) sum += aspectChunks[c][a] var width = colWidth - m.gap * (chunks[c].length - 1) var h = width / sum // A tall window can want more height than the column has; clamping keeps // it inside and simply leaves the row narrower than the column. var ceiling = available - stack - m.chipHeight * chunks.length if (ceiling > 0) h = Math.min(h, ceiling / chunks.length) if (m.maxTileHeight > 0) h = Math.min(h, m.maxTileHeight) heights.push(h) if (h < smallest) smallest = h } var total = stack + m.chipHeight * chunks.length for (var t = 0; t < heights.length; t++) total += heights[t] if (total > available && best !== null) continue if (best === null || smallest > best.smallest) { best = { chunks: chunks, aspectChunks: aspectChunks, heights: heights, smallest: smallest, total: total } } } if (!best) return empty var rows = [] var height = chrome for (var r = 0; r < best.chunks.length; r++) { // The height at which this row exactly spans the column. minTileHeight is // advisory and must never win against it: raising a row above its fitting // height is how a row ends up wider than the column it lives in. var sumAspect = 0 for (var sa = 0; sa < best.aspectChunks[r].length; sa++) sumAspect += best.aspectChunks[r][sa] var fitHeight = (colWidth - m.gap * (best.chunks[r].length - 1)) / sumAspect // Floored, not rounded: rounding up by half a pixel here multiplies out // through the aspect ratios and pushes the row past the column edge. var rowHeight = Math.floor(Math.min(Math.max(m.minTileHeight, best.heights[r]), fitHeight)) if (rowHeight < 1) rowHeight = 1 var cells = [] for (var k = 0; k < best.chunks[r].length; k++) { cells.push({ item: best.chunks[r][k], width: Math.max(1, Math.floor(best.aspectChunks[r][k] * rowHeight)), height: rowHeight }) } rows.push({ height: rowHeight, cells: cells }) height += rowHeight + m.chipHeight } height += m.gap * (rows.length - 1) return { rows: rows, height: height } } // ------------------------------------------------------------------- exports // QML reads these as plain functions off the imported namespace; Node picks // them up through the guard. `module` is undefined inside a QML .pragma // library, so this costs nothing there. if (typeof module !== "undefined" && module.exports) { module.exports = { ipcField: ipcField, windowClass: windowClass, windowSize: windowSize, windowRect: windowRect, aspectRatio: aspectRatio, clampNumber: clampNumber, prettyClass: prettyClass, hostLabel: hostLabel, titleCase: titleCase, searchHaystack: searchHaystack, toArray: toArray, hyprAddress: hyprAddress, focusCommand: focusCommand, moveCommand: moveCommand, escapeHtml: escapeHtml, highlightHtml: highlightHtml, cleanTitle: cleanTitle, configReadCommand: configReadCommand, classifyConfigPayload: classifyConfigPayload, fuzzyMatch: fuzzyMatch, matchWindow: matchWindow, isRealWindow: isRealWindow, compareWorkspaces: compareWorkspaces, workspaceLabel: workspaceLabel, structureSignature: structureSignature, buildModel: buildModel, applyQuery: applyQuery, neutralState: neutralState, addressForBadge: addressForBadge, justify: justify, defaultMetrics: defaultMetrics, columnWidth: columnWidth, columnIndexAt: columnIndexAt, packColumn: packColumn } }