mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-10-21 18:54:01 -04:00
813 lines
31 KiB
QML
813 lines
31 KiB
QML
pragma Singleton
|
|
pragma ComponentBehavior: Bound
|
|
|
|
import QtQuick
|
|
import Quickshell
|
|
import Quickshell.Io
|
|
import qs.Common
|
|
import qs.Services
|
|
import qs.DCommon.Common as DCommon
|
|
import "../Common/OutputModel.js" as OutputModel
|
|
|
|
// 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
|
|
|
|
// Mango reads only $HOME/.config/mango, whatever XDG_CONFIG_HOME says.
|
|
readonly property string configDir: Quickshell.env("HOME") + "/.config"
|
|
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 outputsPath: mangoDmsDir + "/outputs.conf"
|
|
readonly property string layoutPath: mangoDmsDir + "/layout.conf"
|
|
readonly property string cursorPath: mangoDmsDir + "/cursor.conf"
|
|
readonly property string inputPath: mangoDmsDir + "/input.conf"
|
|
readonly property string windowRulesPath: mangoDmsDir + "/windowrules.conf"
|
|
|
|
// Mango after 0.17.5 renamed every config key to snake_case with no aliases.
|
|
property bool snakeKeys: false
|
|
property bool dialectReady: false
|
|
property bool inputGenerationPending: false
|
|
property bool inputGenerationRunning: false
|
|
property int _lastGapValue: -1
|
|
property real _ignoreWatchedReloadUntil: 0
|
|
property string _lastMonitorsLine: ""
|
|
property string _lastClientsLine: ""
|
|
|
|
// 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
|
|
|
|
// 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: 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: mangoInputWatcher
|
|
path: CompositorService.isMango ? root.inputPath : ""
|
|
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()
|
|
}
|
|
|
|
DCommon.DSocket {
|
|
id: monitorsSocket
|
|
path: root.socketPath
|
|
connected: CompositorService.isMango && root.available
|
|
|
|
onConnectionStateChanged: {
|
|
if (linkUp) {
|
|
send("watch all-monitors");
|
|
root.refreshLayouts();
|
|
}
|
|
}
|
|
|
|
parser: SplitParser {
|
|
onRead: line => root._handleMonitors(line)
|
|
}
|
|
}
|
|
|
|
DCommon.DSocket {
|
|
id: clientsSocket
|
|
path: root.socketPath
|
|
connected: CompositorService.isMango && root.available
|
|
|
|
onConnectionStateChanged: {
|
|
if (linkUp)
|
|
send("watch all-clients");
|
|
}
|
|
|
|
parser: SplitParser {
|
|
onRead: line => root._handleClients(line)
|
|
}
|
|
}
|
|
|
|
// mango closes the connection after each non-watch command; queued
|
|
// dispatches drain one per reconnect cycle.
|
|
DCommon.DSocket {
|
|
id: dispatchSocket
|
|
path: root.socketPath
|
|
connected: CompositorService.isMango && root.available
|
|
reconnectBaseMs: 25
|
|
|
|
onConnectionStateChanged: {
|
|
if (linkUp) {
|
|
root._dispatchConnectionUsed = false;
|
|
root._flushDispatch();
|
|
return;
|
|
}
|
|
const entry = root._dispatchInFlight;
|
|
root._dispatchInFlight = null;
|
|
if (entry?.callback)
|
|
entry.callback("");
|
|
}
|
|
|
|
parser: SplitParser {
|
|
onRead: line => root._onDispatchReply(line)
|
|
}
|
|
}
|
|
|
|
property var _dispatchQueue: []
|
|
property var _dispatchInFlight: null
|
|
property bool _dispatchConnectionUsed: false
|
|
|
|
function dispatch(command, callback) {
|
|
request("dispatch " + command, callback);
|
|
}
|
|
|
|
function request(command, callback) {
|
|
if (!root.available)
|
|
return;
|
|
_dispatchQueue.push({
|
|
"command": command,
|
|
"callback": callback ?? null
|
|
});
|
|
_flushDispatch();
|
|
}
|
|
|
|
function _flushDispatch() {
|
|
if (_dispatchInFlight || _dispatchConnectionUsed || _dispatchQueue.length === 0 || !dispatchSocket.linkUp)
|
|
return;
|
|
_dispatchConnectionUsed = true;
|
|
_dispatchInFlight = _dispatchQueue.shift();
|
|
dispatchSocket.send(_dispatchInFlight.command);
|
|
}
|
|
|
|
function _onDispatchReply(line) {
|
|
const entry = _dispatchInFlight;
|
|
_dispatchInFlight = null;
|
|
if (entry?.callback)
|
|
entry.callback(line);
|
|
}
|
|
|
|
function _handleMonitors(line) {
|
|
// Mango resends the full snapshot on every arrange/focus change, mostly unchanged.
|
|
if (!line || !line.trim() || line === root._lastMonitorsLine)
|
|
return;
|
|
root._lastMonitorsLine = line;
|
|
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": !inOverview && t.is_active ? 1 : (t.is_urgent ? 2 : 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() || line === root._lastClientsLine)
|
|
return;
|
|
root._lastClientsLine = line;
|
|
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;
|
|
}
|
|
|
|
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 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];
|
|
}
|
|
|
|
// 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,
|
|
"sourceToplevel": bestMatch,
|
|
"activated": !!client.is_focused,
|
|
"mangoWindowId": client.id,
|
|
"mangoTags": client.tags || [],
|
|
"mangoMonitor": client.monitor,
|
|
"mangoVisible": client.is_visible === true
|
|
};
|
|
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());
|
|
// Toplevels with no mango client yet (the client list lags) go last instead of vanishing.
|
|
const matched = new Set(enriched.map(e => e.sourceToplevel));
|
|
for (const t of toplevels) {
|
|
if (!matched.has(t))
|
|
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;
|
|
|
|
// is_visible covers global windows; tags still keep minimized ones listed.
|
|
const onActive = tags => (tags || []).some(t => active.has(t));
|
|
|
|
if (toplevels.length > 0 && toplevels[0].mangoTags !== undefined)
|
|
return toplevels.filter(t => t.mangoMonitor === screenName && (t.mangoVisible || onActive(t.mangoTags)));
|
|
|
|
const clients = (windows || []).filter(c => c.monitor === screenName && (c.is_visible === true || 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);
|
|
}
|
|
|
|
function suppressWatchedConfigReloads(ms) {
|
|
root._ignoreWatchedReloadUntil = Math.max(root._ignoreWatchedReloadUntil, Date.now() + (ms || 1500));
|
|
}
|
|
|
|
// Trailing edge: an editor's save arrives as several events and the first may see a half-written file.
|
|
Timer {
|
|
id: watchedReloadTimer
|
|
interval: 400
|
|
onTriggered: root.reloadConfig(false, false)
|
|
}
|
|
|
|
DeferredAction {
|
|
id: inputGenerationAction
|
|
onTriggered: root.doGenerateInputConfig()
|
|
}
|
|
|
|
function handleWatchedConfigChanged() {
|
|
if (!CompositorService.isMango || !root.available)
|
|
return;
|
|
if (Date.now() < root._ignoreWatchedReloadUntil)
|
|
return;
|
|
watchedReloadTimer.restart();
|
|
}
|
|
|
|
function reloadConfig(showToast, suppressWatch) {
|
|
if (!CompositorService.isMango || !root.available)
|
|
return;
|
|
const shouldShowToast = showToast !== false && SessionData.showConfigReloadToast;
|
|
const shouldSuppressWatch = suppressWatch !== false;
|
|
if (shouldSuppressWatch)
|
|
suppressWatchedConfigReloads(1500);
|
|
dispatch("reload_config", line => {
|
|
// Empty means the socket dropped before replying; the reload may still have run.
|
|
if (!line)
|
|
return;
|
|
let ok = false;
|
|
try {
|
|
ok = JSON.parse(line).success === true;
|
|
} catch (e) {}
|
|
if (!ok) {
|
|
log.warn("reload_config dispatch failed:", line);
|
|
if (shouldShowToast)
|
|
ToastService.showError(I18n.tr("mango: failed to reload config"), line || "", "", "mango-config");
|
|
return;
|
|
}
|
|
if (shouldShowToast)
|
|
ToastService.showInfo(I18n.tr("mango: config reloaded"), "", "", "mango-config");
|
|
root.refreshLayouts();
|
|
});
|
|
}
|
|
|
|
function quit() {
|
|
dispatch("quit");
|
|
}
|
|
|
|
function dispatchOnOutput(outputName, command) {
|
|
if (!outputName || !getOutputState(outputName))
|
|
return;
|
|
// Always queue the focus: IPC snapshots can lag behind earlier commands.
|
|
dispatch("focusmon," + outputName);
|
|
dispatch(command);
|
|
}
|
|
|
|
// The dwl model is 0-based; Mango view/toggleview take a 1-based tag number.
|
|
function switchToTag(outputName, tagIndex) {
|
|
dispatchOnOutput(outputName, "view," + (tagIndex + 1));
|
|
}
|
|
|
|
function toggleTag(outputName, tagIndex) {
|
|
dispatchOnOutput(outputName, "toggleview," + (tagIndex + 1));
|
|
}
|
|
|
|
// Mango before 0.16.1 has no `get layouts`; retain its compiled-in order.
|
|
property var _availableLayouts: null
|
|
readonly property var layouts: _availableLayouts ? _availableLayouts.map(layout => layout.symbol) : ["T", "S", "G", "M", "K", "CT", "RT", "VS", "VT", "VG", "VK", "DW", "F", "VF"]
|
|
readonly property var _layoutNames: _availableLayouts ? _availableLayouts.map(layout => layout.name) : ["tile", "scroller", "grid", "monocle", "deck", "center_tile", "right_tile", "vertical_scroller", "vertical_tile", "vertical_grid", "vertical_deck", "dwindle", "fair", "vertical_fair"]
|
|
|
|
function refreshLayouts() {
|
|
request("get layouts", line => {
|
|
let data;
|
|
try {
|
|
data = JSON.parse(line);
|
|
} catch (e) {
|
|
return;
|
|
}
|
|
if (!Array.isArray(data?.layouts) || data.layouts.length === 0)
|
|
return;
|
|
if (!data.layouts.every(layout => typeof layout?.name === "string" && layout.name.length > 0 && typeof layout.symbol === "string"))
|
|
return;
|
|
root._availableLayouts = data.layouts;
|
|
});
|
|
}
|
|
|
|
function setLayout(outputName, index) {
|
|
const name = _layoutNames[index];
|
|
if (name)
|
|
dispatchOnOutput(outputName, "setlayout," + name);
|
|
}
|
|
|
|
function cycleKeyboardLayout() {
|
|
dispatch("switch_keyboard_layout");
|
|
}
|
|
|
|
function powerOffMonitors() {
|
|
const screens = Quickshell.screens || [];
|
|
for (let i = 0; i < screens.length; i++) {
|
|
if (screens[i] && screens[i].name)
|
|
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)
|
|
dispatch("wakeup_monitor," + screens[i].name);
|
|
}
|
|
}
|
|
|
|
Connections {
|
|
target: SettingsData
|
|
enabled: CompositorService.isMango
|
|
function onBarConfigsChanged() {
|
|
const newGaps = Math.max(4, (SettingsData.getPrimaryBarConfig()?.spacing ?? 4));
|
|
if (newGaps === root._lastGapValue)
|
|
return;
|
|
root._lastGapValue = newGaps;
|
|
generateLayoutConfig();
|
|
}
|
|
}
|
|
|
|
Connections {
|
|
target: CompositorService
|
|
function onIsMangoChanged() {
|
|
if (CompositorService.isMango)
|
|
detectDialect();
|
|
}
|
|
}
|
|
|
|
// Also respells the DMS fragments for the installed Mango; an older dms keeps legacy keys.
|
|
function detectDialect() {
|
|
Proc.runCommand("mango-dialect", [Proc.dmsBin, "config", "mango-migrate"], (output, exitCode) => {
|
|
if (exitCode !== 0)
|
|
log.warn("mango-migrate exited", exitCode, "- some fragments may keep the old key spelling");
|
|
root.snakeKeys = output.trim().split("\n").pop() === "snake";
|
|
root.dialectReady = true;
|
|
generateLayoutConfig();
|
|
generateInputConfig();
|
|
});
|
|
}
|
|
|
|
function generateOutputsConfig(outputsData, callback, skipReload) {
|
|
if (!outputsData || Object.keys(outputsData).length === 0) {
|
|
if (callback)
|
|
callback(false);
|
|
return;
|
|
}
|
|
// outputs.conf is sourced first, so its rules win over config.conf's; read both for carried fields.
|
|
Proc.runCommand("mango-read-outputs", ["sh", "-c", `cat "${outputsPath}" "${configPath}" 2>/dev/null`], existing => {
|
|
const lines = ["# Auto-generated by DMS - do not edit manually", ""].concat(OutputModel.mangoMonitorRuleLines(outputsData, existing, root.snakeKeys), [""]);
|
|
const content = lines.join("\n");
|
|
|
|
suppressWatchedConfigReloads(1500);
|
|
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 && !skipReload)
|
|
reloadConfig(false);
|
|
if (callback)
|
|
callback(true);
|
|
});
|
|
});
|
|
}
|
|
|
|
function generateLayoutConfig() {
|
|
if (!CompositorService.isMango)
|
|
return;
|
|
|
|
const defaultGaps = typeof SettingsData !== "undefined" ? Math.max(4, (SettingsData.getPrimaryBarConfig()?.spacing ?? 4)) : 4;
|
|
const defaultBorderSize = 2;
|
|
|
|
const cornerRadius = Theme.windowRadius;
|
|
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;
|
|
|
|
const snake = root.snakeKeys;
|
|
let content = `# Auto-generated by DMS - do not edit manually
|
|
border_radius=${cornerRadius}
|
|
${snake ? "border_px" : "borderpx"}=${borderSize}
|
|
`;
|
|
|
|
if (manageGaps)
|
|
content += snake ? `gap_inner_horizontal=${gapsIn}
|
|
gap_inner_vertical=${gapsIn}
|
|
gap_outer_horizontal=${gapsOut}
|
|
gap_outer_vertical=${gapsOut}
|
|
` : `gappih=${gapsIn}
|
|
gappiv=${gapsIn}
|
|
gappoh=${gapsOut}
|
|
gappov=${gapsOut}
|
|
`;
|
|
|
|
suppressWatchedConfigReloads(1500);
|
|
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(false);
|
|
});
|
|
}
|
|
|
|
function generateCursorConfig() {
|
|
if (!CompositorService.isMango)
|
|
return;
|
|
|
|
const settings = typeof SettingsData !== "undefined" ? SettingsData.cursorSettings : null;
|
|
if (!settings) {
|
|
suppressWatchedConfigReloads(1500);
|
|
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;
|
|
|
|
let content = `# Auto-generated by DMS - do not edit manually
|
|
cursor_size=${size}`;
|
|
|
|
if (themeName)
|
|
content += `\ncursor_theme=${themeName}`;
|
|
|
|
if (hideTimeout > 0)
|
|
content += `\ncursor_hide_timeout=${hideTimeout}`;
|
|
|
|
content += `\n`;
|
|
|
|
suppressWatchedConfigReloads(1500);
|
|
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(false);
|
|
});
|
|
}
|
|
|
|
function generateInputConfig() {
|
|
if (!CompositorService.isMango)
|
|
return;
|
|
inputGenerationPending = true;
|
|
inputGenerationAction.schedule();
|
|
}
|
|
|
|
function doGenerateInputConfig() {
|
|
if (inputGenerationRunning)
|
|
return;
|
|
inputGenerationPending = false;
|
|
if (!CompositorService.isMango)
|
|
return;
|
|
// numlock is spelled per dialect, so the first write waits for mango-migrate
|
|
if (!dialectReady) {
|
|
inputGenerationPending = true;
|
|
return;
|
|
}
|
|
inputGenerationRunning = true;
|
|
|
|
const content = ["# Auto-generated by DMS - do not edit manually"].concat(mangoInputLines(SettingsData, snakeKeys), [""]).join("\n");
|
|
// Exit 3 = unchanged, so a shell start does not reload Mango for nothing
|
|
const script = `mkdir -p "${mangoDmsDir}" && cat > "${inputPath}.tmp" << 'EOF'\n${content}EOF\nif cmp -s "${inputPath}.tmp" "${inputPath}"; then rm -f "${inputPath}.tmp"; exit 3; fi\nmv -f "${inputPath}.tmp" "${inputPath}"`;
|
|
suppressWatchedConfigReloads(1500);
|
|
Proc.runCommand("mango-write-input", ["sh", "-c", script], (output, exitCode) => {
|
|
inputGenerationRunning = false;
|
|
if (exitCode === 3) {
|
|
if (inputGenerationPending)
|
|
inputGenerationAction.schedule();
|
|
return;
|
|
}
|
|
if (exitCode !== 0) {
|
|
log.warn("Failed to write input config:", output);
|
|
} else {
|
|
log.info("Generated input config at", inputPath);
|
|
reloadConfig(false);
|
|
}
|
|
if (inputGenerationPending)
|
|
inputGenerationAction.schedule();
|
|
});
|
|
}
|
|
|
|
// Mouse and trackpad keys are the split names Mango has had since 0.16.1; libinput enums are numeric
|
|
function mangoInputLines(s, snake) {
|
|
const flag = value => value ? 1 : 0;
|
|
const profile = name => name === "flat" ? 1 : name === "adaptive" ? 2 : -1;
|
|
const scrollMethod = name => ({
|
|
"no-scroll": 0,
|
|
"two-finger": 1,
|
|
"edge": 2,
|
|
"on-button-down": 4
|
|
})[name] ?? -1;
|
|
const clickMethod = name => name === "button-areas" ? 1 : name === "clickfinger" ? 2 : -1;
|
|
const lines = [];
|
|
// -1 is a "default" choice: the key stays out so Mango's own default applies
|
|
const add = (key, value) => {
|
|
if (value !== -1 && value !== "")
|
|
lines.push(`${key}=${value}`);
|
|
};
|
|
add("mouse_accel_speed", s.mouseAccelSpeed.toFixed(2));
|
|
add("mouse_accel_profile", profile(s.mouseAccelProfile));
|
|
add("mouse_natural_scrolling", flag(s.mouseNaturalScroll));
|
|
add("mouse_left_handed", flag(s.mouseLeftHanded));
|
|
add("mouse_middle_button_emulation", flag(s.mouseMiddleEmulation));
|
|
add("axis_scroll_factor", s.mouseScrollFactor.toFixed(2));
|
|
add("mouse_scroll_method", scrollMethod(s.mouseScrollMethod));
|
|
add("sloppy_focus", s.focusFollowsMouse);
|
|
add("tap_to_click", flag(s.touchpadTapToClick));
|
|
add("tap_and_drag", flag(s.touchpadTapAndDrag));
|
|
add("drag_lock", flag(s.touchpadDragLock));
|
|
add("trackpad_accel_speed", s.touchpadAccelSpeed.toFixed(2));
|
|
add("trackpad_accel_profile", profile(s.touchpadAccelProfile));
|
|
add("trackpad_natural_scrolling", flag(s.touchpadNaturalScroll));
|
|
add("trackpad_scroll_factor", s.touchpadScrollFactor.toFixed(2));
|
|
add("trackpad_scroll_method", scrollMethod(s.touchpadScrollMethod));
|
|
add("trackpad_click_method", clickMethod(s.touchpadClickMethod));
|
|
add("trackpad_disable_while_typing", flag(s.touchpadDisableWhileTyping));
|
|
add("trackpad_middle_button_emulation", flag(s.touchpadMiddleEmulation));
|
|
add("trackpad_send_events_mode", s.touchpadDisableOnExternalMouse ? 2 : 0);
|
|
|
|
// Unconfigured keyboard keeps the user's own xkb_rules_* keys and the XKB defaults
|
|
const keyboardConfigured = s.keyboardLayouts || s.keyboardVariants || s.keyboardModel || s.keyboardOptions || s.keyboardRepeatDelay > 0 || s.keyboardRepeatRate > 0 || s.keyboardNumlock;
|
|
if (!keyboardConfigured)
|
|
return lines;
|
|
add("xkb_rules_layout", s.keyboardLayouts);
|
|
add("xkb_rules_variant", s.keyboardVariants);
|
|
add("xkb_rules_model", s.keyboardModel);
|
|
add("xkb_rules_options", s.keyboardOptions);
|
|
if (s.keyboardRepeatDelay > 0)
|
|
add("repeat_delay", s.keyboardRepeatDelay);
|
|
if (s.keyboardRepeatRate > 0)
|
|
add("repeat_rate", s.keyboardRepeatRate);
|
|
add(snake ? "numlock_on" : "numlockon", flag(s.keyboardNumlock));
|
|
return lines;
|
|
}
|
|
}
|