Files

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;
}
}