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 ` 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" 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) return; 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), [""]); 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; // layer_animations would tween the dot lift and the island sheet let content = `# Auto-generated by DMS - do not edit manually border_radius=${cornerRadius} border_px=${borderSize} layer_rule=no_animation:1,layer_name:^(quickshell)$ layer_rule=no_animation:1,layer_name:^dms:.* `; if (manageGaps) content += `gap_inner_horizontal=${gapsIn} gap_inner_vertical=${gapsIn} gap_outer_horizontal=${gapsOut} gap_outer_vertical=${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; inputGenerationRunning = true; const content = ["# Auto-generated by DMS - do not edit manually"].concat(mangoInputLines(SettingsData), [""]).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) { 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("numlock_on", flag(s.keyboardNumlock)); return lines; } }