mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-08-02 03:28:28 -04:00
6c2572fbfe
Since Mango v0.15.0, disable_monitor sets only_sleep=0 which removes the
output from the layout entirely. On NVIDIA (proprietary driver), the DRM
handshake fails on re-enable, leaving a black screen recoverable only via
TTY switch.
sleep_monitor/wakeup_monitor set only_sleep=1, keeping the output in the
layout while powered off. This matches the pre-v0.15 behavior and fixes
DPMS wake on NVIDIA.
These commands are available since Mango v0.15.0 (commit b0326d7).
(cherry picked from commit da7cc5648b)
668 lines
25 KiB
QML
668 lines
25 KiB
QML
pragma Singleton
|
|
pragma ComponentBehavior: Bound
|
|
|
|
import QtCore
|
|
import QtQuick
|
|
import Quickshell
|
|
import Quickshell.Io
|
|
import qs.Common
|
|
import qs.Services
|
|
|
|
// Native MangoWM IPC client. mango advertises a JSON-over-Unix-socket protocol
|
|
// via MANGO_INSTANCE_SIGNATURE; each connection issues one `watch <target>` verb
|
|
// and gets a full JSON snapshot followed by newline-delimited updates. Exposes
|
|
// a dwl-style tag API plus a per-client window list.
|
|
Singleton {
|
|
id: root
|
|
readonly property var log: Log.scoped("MangoService")
|
|
|
|
readonly property string socketPath: Quickshell.env("MANGO_INSTANCE_SIGNATURE")
|
|
readonly property bool available: socketPath.length > 0
|
|
|
|
readonly property string configDir: Paths.strip(StandardPaths.writableLocation(StandardPaths.ConfigLocation))
|
|
readonly property string configPath: configDir + "/mango/config.conf"
|
|
readonly property string mangoDmsDir: configDir + "/mango/dms"
|
|
readonly property string bindsPath: mangoDmsDir + "/binds.conf"
|
|
readonly property string colorsPath: mangoDmsDir + "/colors.conf"
|
|
readonly property string outputsPath: mangoDmsDir + "/outputs.conf"
|
|
readonly property string layoutPath: mangoDmsDir + "/layout.conf"
|
|
readonly property string cursorPath: mangoDmsDir + "/cursor.conf"
|
|
readonly property string windowRulesPath: mangoDmsDir + "/windowrules.conf"
|
|
|
|
property int _lastGapValue: -1
|
|
property real _ignoreWatchedReloadUntil: 0
|
|
property real _lastWatchedReloadAt: 0
|
|
|
|
// name -> { name, active, x, y, width, height, scale, layoutIndex,
|
|
// layoutSymbol, lastOpenSurface, kbLayout, keymode,
|
|
// tags: [{ tag, state, clients, focused, urgent, layout }] }
|
|
property var outputs: ({})
|
|
property string activeOutput: ""
|
|
readonly property bool inOverview: isOutputInOverview(activeOutput)
|
|
property int tagCount: 9
|
|
property var displayScales: ({})
|
|
property string currentKeyboardLayout: ""
|
|
// Rich client list from `watch all-clients` (mango "clients").
|
|
property var windows: []
|
|
|
|
// windowsChanged is auto-generated by the `windows` property's change signal.
|
|
signal stateChanged
|
|
|
|
// ── State sockets ──────────────────────────────────────────────────────
|
|
// One connection per watch target; mango streams a fresh full snapshot on
|
|
// every change, so each line is treated as the complete state.
|
|
|
|
FileView {
|
|
id: mangoConfigWatcher
|
|
path: CompositorService.isMango ? root.configPath : ""
|
|
watchChanges: CompositorService.isMango
|
|
onFileChanged: root.handleWatchedConfigChanged()
|
|
}
|
|
|
|
FileView {
|
|
id: mangoBindsWatcher
|
|
path: CompositorService.isMango ? root.bindsPath : ""
|
|
watchChanges: CompositorService.isMango
|
|
onFileChanged: root.handleWatchedConfigChanged()
|
|
}
|
|
|
|
FileView {
|
|
id: mangoColorsWatcher
|
|
path: CompositorService.isMango ? root.colorsPath : ""
|
|
watchChanges: CompositorService.isMango
|
|
onFileChanged: root.handleWatchedConfigChanged()
|
|
}
|
|
|
|
FileView {
|
|
id: mangoLayoutWatcher
|
|
path: CompositorService.isMango ? root.layoutPath : ""
|
|
watchChanges: CompositorService.isMango
|
|
onFileChanged: root.handleWatchedConfigChanged()
|
|
}
|
|
|
|
FileView {
|
|
id: mangoCursorWatcher
|
|
path: CompositorService.isMango ? root.cursorPath : ""
|
|
watchChanges: CompositorService.isMango
|
|
onFileChanged: root.handleWatchedConfigChanged()
|
|
}
|
|
|
|
FileView {
|
|
id: mangoOutputsWatcher
|
|
path: CompositorService.isMango ? root.outputsPath : ""
|
|
watchChanges: CompositorService.isMango
|
|
onFileChanged: root.handleWatchedConfigChanged()
|
|
}
|
|
|
|
FileView {
|
|
id: mangoWindowRulesWatcher
|
|
path: CompositorService.isMango ? root.windowRulesPath : ""
|
|
watchChanges: CompositorService.isMango
|
|
onFileChanged: root.handleWatchedConfigChanged()
|
|
}
|
|
|
|
DankSocket {
|
|
id: monitorsSocket
|
|
path: root.socketPath
|
|
connected: root.available
|
|
|
|
onConnectionStateChanged: {
|
|
if (connected)
|
|
send("watch all-monitors");
|
|
}
|
|
|
|
parser: SplitParser {
|
|
onRead: line => root._handleMonitors(line)
|
|
}
|
|
}
|
|
|
|
DankSocket {
|
|
id: clientsSocket
|
|
path: root.socketPath
|
|
connected: root.available
|
|
|
|
onConnectionStateChanged: {
|
|
if (connected)
|
|
send("watch all-clients");
|
|
}
|
|
|
|
parser: SplitParser {
|
|
onRead: line => root._handleClients(line)
|
|
}
|
|
}
|
|
|
|
function _handleMonitors(line) {
|
|
if (!line || !line.trim())
|
|
return;
|
|
let data;
|
|
try {
|
|
data = JSON.parse(line);
|
|
} catch (e) {
|
|
log.warn("Failed to parse all-monitors:", e);
|
|
return;
|
|
}
|
|
const monitors = data.monitors;
|
|
if (!Array.isArray(monitors))
|
|
return;
|
|
|
|
const newOutputs = {};
|
|
const newScales = {};
|
|
let newActive = "";
|
|
let newTagCount = root.tagCount;
|
|
let newKbLayout = root.currentKeyboardLayout;
|
|
|
|
for (const m of monitors) {
|
|
if (!m.name)
|
|
continue;
|
|
const activeTags = m.active_tags || [];
|
|
const inOverview = activeTags.length === 0 || activeTags.every(t => t === 0);
|
|
const tags = (m.tags || []).map(t => ({
|
|
// 0-based to match the legacy dwl tag model used by consumers
|
|
"tag": (t.index ?? 1) - 1,
|
|
"state": t.is_urgent ? 2 : (!inOverview && t.is_active ? 1 : 0),
|
|
"clients": t.client_count ?? 0,
|
|
"focused": !inOverview && !!t.is_active,
|
|
"urgent": !!t.is_urgent,
|
|
"layout": t.layout ?? ""
|
|
}));
|
|
newOutputs[m.name] = {
|
|
"name": m.name,
|
|
"active": !!m.active,
|
|
"x": m.x ?? 0,
|
|
"y": m.y ?? 0,
|
|
"width": m.width ?? 0,
|
|
"height": m.height ?? 0,
|
|
"scale": m.scale ?? 1.0,
|
|
"layoutIndex": m.layout_index ?? 0,
|
|
"layout": m.layout_index ?? 0,
|
|
"activeTags": activeTags,
|
|
"inOverview": inOverview,
|
|
"layoutSymbol": m.layout_symbol ?? "",
|
|
"lastOpenSurface": m.last_open_surface ?? "",
|
|
"keymode": m.keymode ?? "",
|
|
"kbLayout": m.keyboardlayout ?? "",
|
|
"tags": tags
|
|
};
|
|
if (typeof m.scale === "number" && m.scale > 0)
|
|
newScales[m.name] = m.scale;
|
|
if (m.active) {
|
|
newActive = m.name;
|
|
if (m.keyboardlayout)
|
|
newKbLayout = m.keyboardlayout;
|
|
}
|
|
if (tags.length > 0)
|
|
newTagCount = tags.length;
|
|
}
|
|
|
|
root.outputs = newOutputs;
|
|
root.displayScales = newScales;
|
|
root.tagCount = newTagCount;
|
|
if (newActive)
|
|
root.activeOutput = newActive;
|
|
root.currentKeyboardLayout = newKbLayout;
|
|
root.stateChanged();
|
|
}
|
|
|
|
function _handleClients(line) {
|
|
if (!line || !line.trim())
|
|
return;
|
|
let data;
|
|
try {
|
|
data = JSON.parse(line);
|
|
} catch (e) {
|
|
log.warn("Failed to parse all-clients:", e);
|
|
return;
|
|
}
|
|
if (!Array.isArray(data.clients))
|
|
return;
|
|
root.windows = data.clients;
|
|
}
|
|
|
|
// ── Tag API (dwl-style tag model) ──────────────────────────────────────
|
|
|
|
function getOutputState(outputName) {
|
|
return (outputs && outputs[outputName]) ? outputs[outputName] : null;
|
|
}
|
|
|
|
function getActiveTags(outputName) {
|
|
const output = getOutputState(outputName);
|
|
if (!output || !output.tags)
|
|
return [];
|
|
return output.tags.filter(tag => tag.state === 1).map(tag => tag.tag);
|
|
}
|
|
|
|
// mango reports active_tags=[0] (no real tag selected) while the overview is open.
|
|
function isOutputInOverview(outputName) {
|
|
const output = getOutputState(outputName);
|
|
if (!output)
|
|
return false;
|
|
if (output.inOverview !== undefined)
|
|
return output.inOverview;
|
|
const at = output.activeTags || [];
|
|
return at.length === 0 || at.every(t => t === 0);
|
|
}
|
|
|
|
function getTagsWithClients(outputName) {
|
|
const output = getOutputState(outputName);
|
|
if (!output || !output.tags)
|
|
return [];
|
|
return output.tags.filter(tag => tag.clients > 0).map(tag => tag.tag);
|
|
}
|
|
|
|
function getUrgentTags(outputName) {
|
|
const output = getOutputState(outputName);
|
|
if (!output || !output.tags)
|
|
return [];
|
|
return output.tags.filter(tag => tag.state === 2).map(tag => tag.tag);
|
|
}
|
|
|
|
function getVisibleTags(outputName) {
|
|
const output = getOutputState(outputName);
|
|
if (!output || !output.tags)
|
|
return [];
|
|
if (isOutputInOverview(outputName))
|
|
return [];
|
|
const visibleTags = new Set();
|
|
output.tags.forEach(tag => {
|
|
if (tag.state === 1 || tag.clients > 0)
|
|
visibleTags.add(tag.tag);
|
|
});
|
|
return Array.from(visibleTags).sort((a, b) => a - b);
|
|
}
|
|
|
|
function getOutputScale(outputName) {
|
|
return displayScales[outputName];
|
|
}
|
|
|
|
// ── Window list ↔ wlr toplevel matching (per-tag sort/filter) ──────────
|
|
// Match mango clients to wlr foreign-toplevels by appId+title to enrich them
|
|
// with owning tags/monitor for per-tag filtering and stable ordering.
|
|
|
|
function _screenName(screenOrName) {
|
|
return (typeof screenOrName === "string") ? screenOrName : (screenOrName?.name ?? "");
|
|
}
|
|
|
|
function _orderedClients() {
|
|
const list = (windows || []).slice();
|
|
list.sort((a, b) => {
|
|
const ma = outputs[a.monitor], mb = outputs[b.monitor];
|
|
const ax = ma?.x ?? 1e9, ay = ma?.y ?? 1e9;
|
|
const bx = mb?.x ?? 1e9, by = mb?.y ?? 1e9;
|
|
if (ax !== bx)
|
|
return ax - bx;
|
|
if (ay !== by)
|
|
return ay - by;
|
|
if ((a.y ?? 0) !== (b.y ?? 0))
|
|
return (a.y ?? 0) - (b.y ?? 0);
|
|
if ((a.x ?? 0) !== (b.x ?? 0))
|
|
return (a.x ?? 0) - (b.x ?? 0);
|
|
return (a.id ?? 0) - (b.id ?? 0);
|
|
});
|
|
return list;
|
|
}
|
|
|
|
function _matchAndEnrich(toplevels, clients) {
|
|
const used = new Set();
|
|
const result = [];
|
|
for (const client of clients) {
|
|
let bestMatch = null;
|
|
let bestScore = -1;
|
|
for (const toplevel of toplevels) {
|
|
if (used.has(toplevel))
|
|
continue;
|
|
if (toplevel.appId !== client.appid)
|
|
continue;
|
|
let score = 1;
|
|
if (client.title && toplevel.title) {
|
|
if (toplevel.title === client.title)
|
|
score = 3;
|
|
else if (toplevel.title.includes(client.title) || client.title.includes(toplevel.title))
|
|
score = 2;
|
|
}
|
|
if (score > bestScore) {
|
|
bestScore = score;
|
|
bestMatch = toplevel;
|
|
if (score === 3)
|
|
break;
|
|
}
|
|
}
|
|
if (!bestMatch)
|
|
continue;
|
|
used.add(bestMatch);
|
|
|
|
const enriched = {
|
|
"appId": bestMatch.appId,
|
|
"title": bestMatch.title,
|
|
"activated": !!client.is_focused,
|
|
"mangoWindowId": client.id,
|
|
"mangoTags": client.tags || [],
|
|
"mangoMonitor": client.monitor
|
|
};
|
|
for (let prop in bestMatch) {
|
|
if (!(prop in enriched))
|
|
enriched[prop] = bestMatch[prop];
|
|
}
|
|
result.push(enriched);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
function sortToplevels(toplevels) {
|
|
if (!toplevels || toplevels.length === 0 || windows.length === 0)
|
|
return [...toplevels];
|
|
const enriched = _matchAndEnrich(toplevels, _orderedClients());
|
|
const used = new Set(enriched.map(e => e.mangoWindowId));
|
|
// Append wlr toplevels that had no mango client match (rare).
|
|
const matchedTitles = new Set(enriched.map(e => e.title + "\u0000" + e.appId));
|
|
for (const t of toplevels) {
|
|
if (!matchedTitles.has((t.title || "") + "\u0000" + (t.appId || "")))
|
|
enriched.push(t);
|
|
}
|
|
return enriched;
|
|
}
|
|
|
|
function _activeTagSet(screenName) {
|
|
const out = outputs[screenName];
|
|
return new Set((out?.activeTags) || []);
|
|
}
|
|
|
|
function filterCurrentWorkspace(toplevels, screenOrName) {
|
|
const screenName = _screenName(screenOrName);
|
|
if (!screenName)
|
|
return toplevels;
|
|
// Overview reports active_tags=[0], which would match no client tags.
|
|
if (isOutputInOverview(screenName))
|
|
return toplevels;
|
|
const active = _activeTagSet(screenName);
|
|
if (active.size === 0)
|
|
return toplevels;
|
|
|
|
const onActive = tags => (tags || []).some(t => active.has(t));
|
|
|
|
if (toplevels.length > 0 && toplevels[0].mangoTags !== undefined)
|
|
return toplevels.filter(t => t.mangoMonitor === screenName && onActive(t.mangoTags));
|
|
|
|
const clients = (windows || []).filter(c => c.monitor === screenName && onActive(c.tags));
|
|
return _matchAndEnrich(toplevels, clients);
|
|
}
|
|
|
|
function filterCurrentDisplay(toplevels, screenOrName) {
|
|
const screenName = _screenName(screenOrName);
|
|
if (!toplevels || toplevels.length === 0 || !screenName)
|
|
return toplevels;
|
|
|
|
if (toplevels.length > 0 && toplevels[0].mangoMonitor !== undefined)
|
|
return toplevels.filter(t => t.mangoMonitor === screenName);
|
|
|
|
const clients = (windows || []).filter(c => c.monitor === screenName);
|
|
return _matchAndEnrich(toplevels, clients);
|
|
}
|
|
|
|
// ── Commands (mango verb IPC: mmsg dispatch <func>,<args>) ─────────────
|
|
|
|
function suppressWatchedConfigReloads(ms) {
|
|
root._ignoreWatchedReloadUntil = Math.max(root._ignoreWatchedReloadUntil, Date.now() + (ms || 1500));
|
|
}
|
|
|
|
function handleWatchedConfigChanged() {
|
|
if (!CompositorService.isMango || !root.available)
|
|
return;
|
|
const now = Date.now();
|
|
if (now < root._ignoreWatchedReloadUntil)
|
|
return;
|
|
if (now - root._lastWatchedReloadAt < 700)
|
|
return;
|
|
root._lastWatchedReloadAt = now;
|
|
root.reloadConfig(true, false);
|
|
}
|
|
|
|
function reloadConfig(showToast, suppressWatch) {
|
|
if (!CompositorService.isMango || !root.available)
|
|
return;
|
|
const shouldShowToast = showToast !== false;
|
|
const shouldSuppressWatch = suppressWatch !== false;
|
|
if (shouldSuppressWatch)
|
|
suppressWatchedConfigReloads(1500);
|
|
Proc.runCommand("mango-reload", ["mmsg", "dispatch", "reload_config"], (output, exitCode) => {
|
|
if (exitCode !== 0) {
|
|
log.warn("mmsg reload_config failed:", output);
|
|
if (shouldShowToast)
|
|
ToastService.showError(I18n.tr("mango: failed to reload config"), output || "", "", "mango-config");
|
|
return;
|
|
}
|
|
if (shouldShowToast)
|
|
ToastService.showInfo(I18n.tr("mango: config reloaded"), "", "", "mango-config");
|
|
});
|
|
}
|
|
|
|
function quit() {
|
|
Quickshell.execDetached(["mmsg", "dispatch", "quit"]);
|
|
}
|
|
|
|
// mango tag dispatches act on the focused monitor; tagIndex is 0-based
|
|
// (dwl model), mango `view`/`toggleview` take a 1-based tag number.
|
|
function switchToTag(outputName, tagIndex) {
|
|
Quickshell.execDetached(["mmsg", "dispatch", "view," + (tagIndex + 1)]);
|
|
}
|
|
|
|
function toggleTag(outputName, tagIndex) {
|
|
Quickshell.execDetached(["mmsg", "dispatch", "toggleview," + (tagIndex + 1)]);
|
|
}
|
|
|
|
// mango's tiling layouts are a fixed compiled-in set the IPC doesn't expose,
|
|
// so mirror it here in mango's layouts[] order (layout_index aligns). The
|
|
// parallel name list exists because `setlayout` dispatches by name, not index.
|
|
readonly property var layouts: ["T", "S", "G", "M", "K", "CT", "RT", "VS", "VT", "VG", "VK", "DW", "F", "VF"]
|
|
readonly property var _layoutNames: ["tile", "scroller", "grid", "monocle", "deck", "center_tile", "right_tile", "vertical_scroller", "vertical_tile", "vertical_grid", "vertical_deck", "dwindle", "fair", "vertical_fair"]
|
|
|
|
function setLayout(outputName, index) {
|
|
const name = _layoutNames[index];
|
|
if (name)
|
|
Quickshell.execDetached(["mmsg", "dispatch", "setlayout," + name]);
|
|
}
|
|
|
|
function cycleKeyboardLayout() {
|
|
Quickshell.execDetached(["mmsg", "dispatch", "switch_keyboard_layout"]);
|
|
}
|
|
|
|
function powerOffMonitors() {
|
|
const screens = Quickshell.screens || [];
|
|
for (let i = 0; i < screens.length; i++) {
|
|
if (screens[i] && screens[i].name)
|
|
Quickshell.execDetached(["mmsg", "dispatch", "sleep_monitor," + screens[i].name]);
|
|
}
|
|
}
|
|
|
|
function powerOnMonitors() {
|
|
const screens = Quickshell.screens || [];
|
|
for (let i = 0; i < screens.length; i++) {
|
|
if (screens[i] && screens[i].name)
|
|
Quickshell.execDetached(["mmsg", "dispatch", "wakeup_monitor," + screens[i].name]);
|
|
}
|
|
}
|
|
|
|
// ── Config generation (mango config fragments under ~/.config/mango/dms) ─
|
|
|
|
Connections {
|
|
target: SettingsData
|
|
function onBarConfigsChanged() {
|
|
if (!CompositorService.isMango)
|
|
return;
|
|
const newGaps = Math.max(4, (SettingsData.barConfigs[0]?.spacing ?? 4));
|
|
if (newGaps === root._lastGapValue)
|
|
return;
|
|
root._lastGapValue = newGaps;
|
|
generateLayoutConfig();
|
|
}
|
|
}
|
|
|
|
Connections {
|
|
target: CompositorService
|
|
function onIsMangoChanged() {
|
|
if (CompositorService.isMango)
|
|
generateLayoutConfig();
|
|
}
|
|
}
|
|
|
|
function transformToMango(transform) {
|
|
switch (transform) {
|
|
case "Normal":
|
|
return 0;
|
|
case "90":
|
|
return 1;
|
|
case "180":
|
|
return 2;
|
|
case "270":
|
|
return 3;
|
|
case "Flipped":
|
|
return 4;
|
|
case "Flipped90":
|
|
return 5;
|
|
case "Flipped180":
|
|
return 6;
|
|
case "Flipped270":
|
|
return 7;
|
|
default:
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
function generateOutputsConfig(outputsData, callback) {
|
|
if (!outputsData || Object.keys(outputsData).length === 0) {
|
|
if (callback)
|
|
callback(false);
|
|
return;
|
|
}
|
|
let lines = ["# Auto-generated by DMS - do not edit manually", ""];
|
|
|
|
for (const outputName in outputsData) {
|
|
const output = outputsData[outputName];
|
|
if (!output)
|
|
continue;
|
|
let width = 1920;
|
|
let height = 1080;
|
|
let refreshRate = 60;
|
|
const configured = (output.configured_mode || "").match(/^(\d+)x(\d+)@([\d.]+)$/);
|
|
if (configured) {
|
|
width = parseInt(configured[1], 10);
|
|
height = parseInt(configured[2], 10);
|
|
refreshRate = Math.round(parseFloat(configured[3]));
|
|
} else if (output.modes && output.current_mode !== undefined) {
|
|
const mode = output.modes[output.current_mode];
|
|
if (mode) {
|
|
width = mode.width || 1920;
|
|
height = mode.height || 1080;
|
|
refreshRate = Math.round((mode.refresh_rate || 60000) / 1000);
|
|
}
|
|
}
|
|
|
|
const x = output.logical?.x ?? 0;
|
|
const y = output.logical?.y ?? 0;
|
|
const scale = output.logical?.scale ?? 1.0;
|
|
const transform = transformToMango(output.logical?.transform ?? "Normal");
|
|
const vrr = output.vrr_enabled ? 1 : 0;
|
|
|
|
// Anchor the name regex: mango matches `name:` unanchored (first-match
|
|
// wins), so a bare "DP-1" would also match "eDP-1" and collapse outputs.
|
|
const rule = ["name:^" + outputName + "$", "width:" + width, "height:" + height, "refresh:" + refreshRate, "x:" + x, "y:" + y, "scale:" + scale, "rr:" + transform, "vrr:" + vrr].join(",");
|
|
|
|
lines.push("monitorrule=" + rule);
|
|
}
|
|
|
|
lines.push("");
|
|
|
|
const content = lines.join("\n");
|
|
|
|
Proc.runCommand("mango-write-outputs", ["sh", "-c", `mkdir -p "${mangoDmsDir}" && cat > "${outputsPath}" << 'EOF'\n${content}EOF`], (output, exitCode) => {
|
|
if (exitCode !== 0) {
|
|
log.warn("Failed to write outputs config:", output);
|
|
if (callback)
|
|
callback(false);
|
|
return;
|
|
}
|
|
log.info("Generated outputs config at", outputsPath);
|
|
if (CompositorService.isMango)
|
|
reloadConfig();
|
|
if (callback)
|
|
callback(true);
|
|
});
|
|
}
|
|
|
|
function generateLayoutConfig() {
|
|
if (!CompositorService.isMango)
|
|
return;
|
|
|
|
const defaultRadius = typeof SettingsData !== "undefined" ? SettingsData.cornerRadius : 12;
|
|
const defaultGaps = typeof SettingsData !== "undefined" ? Math.max(4, (SettingsData.barConfigs[0]?.spacing ?? 4)) : 4;
|
|
const defaultBorderSize = 2;
|
|
|
|
const cornerRadius = (typeof SettingsData !== "undefined" && SettingsData.mangoLayoutRadiusOverride >= 0) ? SettingsData.mangoLayoutRadiusOverride : defaultRadius;
|
|
const gapsOverride = typeof SettingsData !== "undefined" ? SettingsData.mangoLayoutGapsOverride : -1;
|
|
const manageGaps = gapsOverride !== -2;
|
|
const gapsIn = gapsOverride >= 0 ? gapsOverride : defaultGaps;
|
|
const gapsOut = (gapsOverride >= 0 && SettingsData.mangoLayoutGapsOutOverride >= 0) ? SettingsData.mangoLayoutGapsOutOverride : gapsIn;
|
|
const borderSize = (typeof SettingsData !== "undefined" && SettingsData.mangoLayoutBorderSize >= 0) ? SettingsData.mangoLayoutBorderSize : defaultBorderSize;
|
|
|
|
let content = `# Auto-generated by DMS - do not edit manually
|
|
border_radius=${cornerRadius}
|
|
borderpx=${borderSize}
|
|
`;
|
|
|
|
if (manageGaps)
|
|
content += `gappih=${gapsIn}
|
|
gappiv=${gapsIn}
|
|
gappoh=${gapsOut}
|
|
gappov=${gapsOut}
|
|
`;
|
|
|
|
Proc.runCommand("mango-write-layout", ["sh", "-c", `mkdir -p "${mangoDmsDir}" && cat > "${layoutPath}" << 'EOF'\n${content}EOF`], (output, exitCode) => {
|
|
if (exitCode !== 0) {
|
|
log.warn("Failed to write layout config:", output);
|
|
return;
|
|
}
|
|
log.info("Generated layout config at", layoutPath);
|
|
reloadConfig();
|
|
});
|
|
}
|
|
|
|
function generateCursorConfig() {
|
|
if (!CompositorService.isMango)
|
|
return;
|
|
|
|
const settings = typeof SettingsData !== "undefined" ? SettingsData.cursorSettings : null;
|
|
if (!settings) {
|
|
Proc.runCommand("mango-write-cursor", ["sh", "-c", `mkdir -p "${mangoDmsDir}" && : > "${cursorPath}"`], (output, exitCode) => {
|
|
if (exitCode !== 0)
|
|
log.warn("Failed to write cursor config:", output);
|
|
});
|
|
return;
|
|
}
|
|
|
|
const themeName = settings.theme === "System Default" ? (SettingsData.systemDefaultCursorTheme || "") : settings.theme;
|
|
const size = settings.size || 24;
|
|
const hideTimeout = settings.mango?.cursorHideTimeout || 0;
|
|
const naturalScrolling = SettingsData.mangoTrackpadNaturalScrolling ? 1 : 0;
|
|
|
|
let content = `# Auto-generated by DMS - do not edit manually
|
|
trackpad_natural_scrolling=${naturalScrolling}
|
|
cursor_size=${size}`;
|
|
|
|
if (themeName)
|
|
content += `\ncursor_theme=${themeName}`;
|
|
|
|
if (hideTimeout > 0)
|
|
content += `\ncursor_hide_timeout=${hideTimeout}`;
|
|
|
|
content += `\n`;
|
|
|
|
Proc.runCommand("mango-write-cursor", ["sh", "-c", `mkdir -p "${mangoDmsDir}" && cat > "${cursorPath}" << 'EOF'\n${content}EOF`], (output, exitCode) => {
|
|
if (exitCode !== 0) {
|
|
log.warn("Failed to write cursor config:", output);
|
|
return;
|
|
}
|
|
log.info("Generated cursor config at", cursorPath);
|
|
reloadConfig();
|
|
});
|
|
}
|
|
}
|