1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-06-13 14:36:32 -04:00

feat(Hyprland): Introduce Lua support for Hyprland configurations

- Note: We do not convert your existing conf configs to lua. This update only reflects DMS defaults state
- Updated README.md to reflect changes
- Updated Keyboard shortcut support
This commit is contained in:
purian23
2026-05-18 13:06:58 -04:00
parent 8dd891f93a
commit 0b55bf5dac
48 changed files with 3756 additions and 1057 deletions
+38
View File
@@ -0,0 +1,38 @@
function shQuote(value) {
return "'" + String(value ?? "").replace(/'/g, "'\\''") + "'";
}
function dirname(path) {
const idx = String(path ?? "").lastIndexOf("/");
return idx > 0 ? path.substring(0, idx) : ".";
}
function buildRepairScript(options) {
const configFile = options.configFile;
const backupFile = options.backupFile;
const fragments = options.fragmentFiles || (options.fragmentFile ? [options.fragmentFile] : []);
const includes = options.includes || [{
grepPattern: options.grepPattern,
includeLine: options.includeLine
}];
const commands = [];
if (backupFile)
commands.push(`cp ${shQuote(configFile)} ${shQuote(backupFile)} 2>/dev/null || true`);
const dirs = {};
for (const fragment of fragments)
dirs[dirname(fragment)] = true;
for (const dir in dirs)
commands.push(`mkdir -p ${shQuote(dir)}`);
if (fragments.length > 0)
commands.push("touch " + fragments.map(shQuote).join(" "));
for (const include of includes) {
if (!include.grepPattern || !include.includeLine)
continue;
commands.push(`if ! grep -v '^[[:space:]]*\\(//\\|#\\|--\\)' ${shQuote(configFile)} 2>/dev/null | grep -q ${shQuote(include.grepPattern)}; then echo '' >> ${shQuote(configFile)} && printf '%s\\n' ${shQuote(include.includeLine)} >> ${shQuote(configFile)}; fi`);
}
return commands.join("; ");
}
+1 -1
View File
@@ -178,7 +178,7 @@ sudo systemctl enable greetd
#### Legacy installation (deprecated)
If you prefer the old method with separate shell scripts and config files:
1. Copy `assets/dms-niri.kdl` or `assets/dms-hypr.conf` to `/etc/greetd`
1. Copy `assets/dms-niri.kdl` or `assets/dms-hypr.lua` (legacy: `assets/dms-hypr.conf`) to `/etc/greetd`
2. Copy `assets/greet-niri.sh` or `assets/greet-hyprland.sh` to `/usr/local/bin/start-dms-greetd.sh`
3. Edit the config file and replace `_DMS_PATH_` with your DMS installation path
4. Configure greetd to use `/usr/local/bin/start-dms-greetd.sh`
@@ -1,3 +1,4 @@
# Deprecated: greetd expects Hyprland 0.55+ Lua; use `/etc/greetd/dms-hypr.lua` instead.
env = DMS_RUN_GREETER,1
exec = sh -c "qs -p _DMS_PATH_; hyprctl dispatch exit"
@@ -0,0 +1,8 @@
-- Minimal Hyprland (Lua) session for greetd — replace _DMS_PATH_ with your DMS checkout.
-- Copy to `/etc/greetd/dms-hypr.lua` alongside `greet-hyprland.sh`.
hl.env("DMS_RUN_GREETER", "1")
hl.on("hyprland.start", function()
hl.exec_cmd('sh -c "qs -p _DMS_PATH_; hyprctl dispatch exit"')
end)
@@ -5,7 +5,7 @@ export QT_QPA_PLATFORM=wayland
export QT_WAYLAND_DISABLE_WINDOWDECORATION=1
export EGL_PLATFORM=gbm
if command -v start-hyprland >/dev/null 2>&1; then
exec start-hyprland -- -c /etc/greetd/dms-hypr.conf
exec start-hyprland -- -c /etc/greetd/dms-hypr.lua
else
exec Hyprland -c /etc/greetd/dms-hypr.conf
exec Hyprland -c /etc/greetd/dms-hypr.lua
fi
@@ -7,6 +7,7 @@ import Quickshell
import Quickshell.Io
import qs.Common
import qs.Services
import "../../../Common/ConfigIncludeResolve.js" as ConfigIncludeResolve
Singleton {
id: root
@@ -1074,10 +1075,86 @@ Singleton {
return result;
}
function hyprLuaField(line, field) {
const re = new RegExp("\\b" + field + "\\s*=\\s*(\\\"(?:\\\\\\\\.|[^\\\"])*\\\"|'(?:\\\\\\\\.|[^'])*'|\\[\\[.*?\\]\\]|[^,}\\s]+)");
const match = line.match(re);
if (!match)
return undefined;
const raw = match[1].trim();
if (raw.startsWith("[[") && raw.endsWith("]]"))
return raw.slice(2, -2);
if (raw.startsWith("\"")) {
try {
return JSON.parse(raw);
} catch (e) {
return raw.slice(1, -1);
}
}
if (raw.startsWith("'") && raw.endsWith("'"))
return raw.slice(1, -1).replace(/\\'/g, "'");
if (raw === "true")
return true;
if (raw === "false")
return false;
const num = Number(raw);
return isNaN(num) ? raw : num;
}
function parseHyprlandLuaMonitorLine(line) {
if (!line.match(/^\s*hl\.monitor\s*\(/))
return null;
const name = hyprLuaField(line, "output");
if (name === undefined)
return null;
const disabled = hyprLuaField(line, "disabled") === true;
const mode = hyprLuaField(line, "mode") || "preferred";
const position = hyprLuaField(line, "position") || "0x0";
const scaleValue = hyprLuaField(line, "scale");
const transform = Number(hyprLuaField(line, "transform") ?? 0);
const vrrMode = Number(hyprLuaField(line, "vrr") ?? 0);
const posMatch = String(position).match(/^(-?\d+)x(-?\d+)$/);
const modeMatch = String(mode).match(/^(\d+)x(\d+)@([\d.]+)/);
const settings = {
"disabled": disabled || undefined,
"bitdepth": hyprLuaField(line, "bitdepth"),
"colorManagement": hyprLuaField(line, "cm"),
"sdrBrightness": hyprLuaField(line, "sdrbrightness"),
"sdrSaturation": hyprLuaField(line, "sdrsaturation"),
"supportsWideColor": hyprLuaField(line, "supports_wide_color"),
"supportsHdr": hyprLuaField(line, "supports_hdr"),
"vrrFullscreenOnly": vrrMode === 2 ? true : undefined
};
return {
"name": String(name),
"logical": {
"x": posMatch ? parseInt(posMatch[1]) : 0,
"y": posMatch ? parseInt(posMatch[2]) : 0,
"scale": typeof scaleValue === "number" ? scaleValue : 1.0,
"transform": hyprlandToTransform(transform)
},
"modes": modeMatch ? [{
"width": parseInt(modeMatch[1]),
"height": parseInt(modeMatch[2]),
"refresh_rate": Math.round(parseFloat(modeMatch[3]) * 1000)
}] : [],
"current_mode": modeMatch ? 0 : -1,
"vrr_enabled": vrrMode >= 1,
"vrr_supported": vrrMode > 0,
"hyprlandSettings": settings,
"mirror": hyprLuaField(line, "mirror") || ""
};
}
function parseHyprlandOutputs(content) {
const result = {};
const lines = content.split("\n");
for (const line of lines) {
const luaMonitor = parseHyprlandLuaMonitorLine(line);
if (luaMonitor) {
result[luaMonitor.name] = luaMonitor;
continue;
}
const disableMatch = line.match(/^\s*monitor\s*=\s*([^,]+),\s*disable\s*$/);
if (disableMatch) {
const name = disableMatch[1].trim();
@@ -1269,10 +1346,10 @@ Singleton {
};
case "hyprland":
return {
"configFile": configDir + "/hypr/hyprland.conf",
"outputsFile": configDir + "/hypr/dms/outputs.conf",
"grepPattern": 'source.*dms/outputs.conf',
"includeLine": "source = ./dms/outputs.conf"
"configFile": configDir + "/hypr/hyprland.lua",
"outputsFile": configDir + "/hypr/dms/outputs.lua",
"grepPattern": "dms.outputs",
"includeLine": "require(\"dms.outputs\")"
};
case "dwl":
return {
@@ -1296,7 +1373,7 @@ Singleton {
return;
}
const filename = (compositor === "niri") ? "outputs.kdl" : "outputs.conf";
const filename = (compositor === "niri") ? "outputs.kdl" : ((compositor === "hyprland") ? "outputs.lua" : "outputs.conf");
const compositorArg = (compositor === "dwl") ? "mangowc" : compositor;
checkingInclude = true;
@@ -1326,11 +1403,17 @@ Singleton {
return;
fixingInclude = true;
const outputsDir = paths.outputsFile.substring(0, paths.outputsFile.lastIndexOf("/"));
const unixTime = Math.floor(Date.now() / 1000);
const backupFile = paths.configFile + ".backup" + unixTime;
const script = ConfigIncludeResolve.buildRepairScript({
configFile: paths.configFile,
backupFile: backupFile,
fragmentFile: paths.outputsFile,
grepPattern: paths.grepPattern,
includeLine: paths.includeLine
});
Proc.runCommand("fix-outputs-include", ["sh", "-c", `cp "${paths.configFile}" "${backupFile}" 2>/dev/null; ` + `mkdir -p "${outputsDir}" && ` + `touch "${paths.outputsFile}" && ` + `if ! grep -v '^[[:space:]]*\\(//\\|#\\)' "${paths.configFile}" 2>/dev/null | grep -q '${paths.grepPattern}'; then ` + `echo '' >> "${paths.configFile}" && ` + `echo '${paths.includeLine}' >> "${paths.configFile}"; fi`], (output, exitCode) => {
Proc.runCommand("fix-outputs-include", ["sh", "-c", script], (output, exitCode) => {
fixingInclude = false;
if (exitCode !== 0)
return;
+38 -4
View File
@@ -3,6 +3,7 @@ pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import qs.Common
import qs.Modals.Common
import qs.Services
import qs.Widgets
@@ -96,6 +97,32 @@ Item {
expandedKey = bindData.action;
}
function confirmRemoveBind(key, remainingKey) {
removeBindConfirm.showWithOptions({
title: I18n.tr("Remove Shortcut?"),
message: KeybindsService.currentProvider === "hyprland" ? I18n.tr("Remove the shortcut %1? An unbind entry will be saved to dms/binds-user.lua so it stays removed across DMS updates.").arg(key) : I18n.tr("Remove the shortcut %1?").arg(key),
confirmText: I18n.tr("Remove"),
confirmColor: Theme.primary,
onConfirm: () => {
KeybindsService.removeBind(key);
keybindsTab._editingKey = remainingKey;
}
});
}
function confirmResetBind(key, remainingKey) {
removeBindConfirm.showWithOptions({
title: I18n.tr("Reset to Default?"),
message: I18n.tr("Drop your override for %1 so the DMS default action re-applies?").arg(key),
confirmText: I18n.tr("Reset"),
confirmColor: Theme.primary,
onConfirm: () => {
KeybindsService.resetBind(key);
keybindsTab._editingKey = remainingKey;
}
});
}
function _onSaveSuccess() {
if (showingNewBind) {
showingNewBind = false;
@@ -129,6 +156,10 @@ Item {
onTriggered: keybindsTab._updateFiltered()
}
ConfirmModal {
id: removeBindConfirm
}
Connections {
target: KeybindsService
function onBindsLoaded() {
@@ -238,7 +269,7 @@ Item {
}
StyledText {
readonly property string bindsFile: KeybindsService.currentProvider === "niri" ? "dms/binds.kdl" : "dms/binds.conf"
readonly property string bindsFile: KeybindsService.currentProvider === "niri" ? "dms/binds.kdl" : KeybindsService.currentProvider === "hyprland" ? "dms/binds-user.lua" : "dms/binds.conf"
text: I18n.tr("Click any shortcut to edit. Changes save to %1").arg(bindsFile)
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
@@ -336,7 +367,7 @@ Item {
}
StyledText {
readonly property string bindsFile: KeybindsService.currentProvider === "niri" ? "dms/binds.kdl" : "dms/binds.conf"
readonly property string bindsFile: KeybindsService.currentProvider === "niri" ? "dms/binds.kdl" : KeybindsService.currentProvider === "hyprland" ? "dms/binds-user.lua" : "dms/binds.conf"
text: {
if (warningBox.showSetup)
return I18n.tr("Click 'Setup' to create %1 and add include to config.").arg(bindsFile);
@@ -623,8 +654,11 @@ Item {
}
onRemoveBind: key => {
const remainingKey = bindItem.keys.find(k => k.key !== key)?.key ?? "";
KeybindsService.removeBind(key);
keybindsTab._editingKey = remainingKey;
keybindsTab.confirmRemoveBind(key, remainingKey);
}
onResetBind: key => {
const remainingKey = bindItem.keys.find(k => k.key !== key)?.key ?? "";
keybindsTab.confirmResetBind(key, remainingKey);
}
onIsExpandedChanged: {
if (!isExpanded || !keybindsTab._editingKey)
+14 -7
View File
@@ -7,6 +7,7 @@ import qs.Modals.FileBrowser
import qs.Services
import qs.Widgets
import qs.Modules.Settings.Widgets
import "../../Common/ConfigIncludeResolve.js" as ConfigIncludeResolve
Item {
id: themeColorsTab
@@ -39,10 +40,10 @@ Item {
};
case "hyprland":
return {
"configFile": configDir + "/hypr/hyprland.conf",
"cursorFile": configDir + "/hypr/dms/cursor.conf",
"grepPattern": 'source.*dms/cursor.conf',
"includeLine": "source = ./dms/cursor.conf"
"configFile": configDir + "/hypr/hyprland.lua",
"cursorFile": configDir + "/hypr/dms/cursor.lua",
"grepPattern": "dms.cursor",
"includeLine": "require(\"dms.cursor\")"
};
case "dwl":
return {
@@ -66,7 +67,7 @@ Item {
return;
}
const filename = (compositor === "niri") ? "cursor.kdl" : "cursor.conf";
const filename = (compositor === "niri") ? "cursor.kdl" : ((compositor === "hyprland") ? "cursor.lua" : "cursor.conf");
const compositorArg = (compositor === "dwl") ? "mangowc" : compositor;
checkingCursorInclude = true;
@@ -95,10 +96,16 @@ Item {
if (!paths)
return;
fixingCursorInclude = true;
const cursorDir = paths.cursorFile.substring(0, paths.cursorFile.lastIndexOf("/"));
const unixTime = Math.floor(Date.now() / 1000);
const backupFile = paths.configFile + ".backup" + unixTime;
Proc.runCommand("fix-cursor-include", ["sh", "-c", `cp "${paths.configFile}" "${backupFile}" 2>/dev/null; ` + `mkdir -p "${cursorDir}" && ` + `touch "${paths.cursorFile}" && ` + `if ! grep -v '^[[:space:]]*\\(//\\|#\\)' "${paths.configFile}" 2>/dev/null | grep -q '${paths.grepPattern}'; then ` + `echo '' >> "${paths.configFile}" && ` + `echo '${paths.includeLine}' >> "${paths.configFile}"; fi`], (output, exitCode) => {
const script = ConfigIncludeResolve.buildRepairScript({
configFile: paths.configFile,
backupFile: backupFile,
fragmentFile: paths.cursorFile,
grepPattern: paths.grepPattern,
includeLine: paths.includeLine
});
Proc.runCommand("fix-cursor-include", ["sh", "-c", script], (output, exitCode) => {
fixingCursorInclude = false;
if (exitCode !== 0)
return;
+16 -9
View File
@@ -8,6 +8,7 @@ import Quickshell.Wayland
import qs.Common
import qs.Services
import qs.Widgets
import "../../Common/ConfigIncludeResolve.js" as ConfigIncludeResolve
Item {
id: root
@@ -54,10 +55,10 @@ Item {
};
case "hyprland":
return {
"configFile": configDir + "/hypr/hyprland.conf",
"rulesFile": configDir + "/hypr/dms/windowrules.conf",
"grepPattern": 'source.*dms/windowrules.conf',
"includeLine": "source = ./dms/windowrules.conf"
"configFile": configDir + "/hypr/hyprland.lua",
"rulesFile": configDir + "/hypr/dms/windowrules.lua",
"grepPattern": "dms.windowrules",
"includeLine": "require(\"dms.windowrules\")"
};
default:
return null;
@@ -135,7 +136,7 @@ Item {
return;
}
const filename = (compositor === "niri") ? "windowrules.kdl" : "windowrules.conf";
const filename = (compositor === "niri") ? "windowrules.kdl" : "windowrules.lua";
checkingInclude = true;
Proc.runCommand("check-windowrules-include", ["dms", "config", "resolve-include", compositor, filename], (output, exitCode) => {
checkingInclude = false;
@@ -162,10 +163,16 @@ Item {
if (!paths)
return;
fixingInclude = true;
const rulesDir = paths.rulesFile.substring(0, paths.rulesFile.lastIndexOf("/"));
const unixTime = Math.floor(Date.now() / 1000);
const backupFile = paths.configFile + ".backup" + unixTime;
Proc.runCommand("fix-windowrules-include", ["sh", "-c", `cp "${paths.configFile}" "${backupFile}" 2>/dev/null; ` + `mkdir -p "${rulesDir}" && ` + `touch "${paths.rulesFile}" && ` + `if ! grep -v '^[[:space:]]*\\(//\\|#\\)' "${paths.configFile}" 2>/dev/null | grep -q '${paths.grepPattern}'; then ` + `echo '' >> "${paths.configFile}" && ` + `echo '${paths.includeLine}' >> "${paths.configFile}"; fi`], (output, exitCode) => {
const script = ConfigIncludeResolve.buildRepairScript({
configFile: paths.configFile,
backupFile: backupFile,
fragmentFile: paths.rulesFile,
grepPattern: paths.grepPattern,
includeLine: paths.includeLine
});
Proc.runCommand("fix-windowrules-include", ["sh", "-c", script], (output, exitCode) => {
fixingInclude = false;
if (exitCode !== 0)
return;
@@ -252,7 +259,7 @@ Item {
}
StyledText {
text: I18n.tr("Define rules for window behavior. Saves to %1").arg(CompositorService.isNiri ? "dms/windowrules.kdl" : "dms/windowrules.conf")
text: I18n.tr("Define rules for window behavior. Saves to %1").arg(CompositorService.isNiri ? "dms/windowrules.kdl" : "dms/windowrules.lua")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
@@ -351,7 +358,7 @@ Item {
}
StyledText {
readonly property string rulesFile: CompositorService.isNiri ? "dms/windowrules.kdl" : "dms/windowrules.conf"
readonly property string rulesFile: CompositorService.isNiri ? "dms/windowrules.kdl" : "dms/windowrules.lua"
text: warningBox.showSetup ? I18n.tr("Click 'Setup' to create %1 and add include to your compositor config.").arg(rulesFile) : I18n.tr("%1 exists but is not included. Window rules won't apply.").arg(rulesFile)
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
+57 -69
View File
@@ -14,10 +14,10 @@ Singleton {
readonly property string configDir: Paths.strip(StandardPaths.writableLocation(StandardPaths.ConfigLocation))
readonly property string hyprDmsDir: configDir + "/hypr/dms"
readonly property string outputsPath: hyprDmsDir + "/outputs.conf"
readonly property string layoutPath: hyprDmsDir + "/layout.conf"
readonly property string cursorPath: hyprDmsDir + "/cursor.conf"
readonly property string windowrulesPath: hyprDmsDir + "/windowrules.conf"
readonly property string outputsPath: hyprDmsDir + "/outputs.lua"
readonly property string layoutPath: hyprDmsDir + "/layout.lua"
readonly property string cursorPath: hyprDmsDir + "/cursor.lua"
readonly property string windowrulesPath: hyprDmsDir + "/windowrules.lua"
property int _lastGapValue: -1
@@ -31,7 +31,7 @@ Singleton {
function ensureWindowrulesConfig() {
Proc.runCommand("hypr-ensure-windowrules", ["sh", "-c", `mkdir -p "${hyprDmsDir}" && [ ! -f "${windowrulesPath}" ] && touch "${windowrulesPath}" || true`], (output, exitCode) => {
if (exitCode !== 0)
log.warn("Failed to ensure windowrules.conf:", output);
log.warn("Failed to ensure windowrules.lua:", output);
});
}
@@ -62,6 +62,18 @@ Singleton {
return outputName;
}
function luaQuoted(str) {
return JSON.stringify(String(str ?? ""));
}
function forceFlagValue(value) {
if (value === true)
return 1;
if (value === false)
return -1;
return Number(value);
}
function generateOutputsConfig(outputsData, hyprlandSettings, callback) {
if (!outputsData || Object.keys(outputsData).length === 0) {
if (callback)
@@ -70,8 +82,7 @@ Singleton {
}
const settings = hyprlandSettings || SettingsData.hyprlandOutputSettings;
let lines = ["# Auto-generated by DMS - do not edit manually", ""];
let monitorv2Blocks = [];
let lines = ["-- Auto-generated by DMS do not edit manually", ""];
for (const outputName in outputsData) {
const output = outputsData[outputName];
@@ -82,7 +93,7 @@ Singleton {
const outputSettings = settings[identifier] || {};
if (outputSettings.disabled) {
lines.push("monitor = " + identifier + ", disable");
lines.push(`hl.monitor({ output = ${luaQuoted(identifier)}, disabled = true })`);
continue;
}
@@ -98,68 +109,42 @@ Singleton {
const position = x + "x" + y;
const scale = output.logical?.scale ?? 1.0;
let monitorLine = "monitor = " + identifier + ", " + resolution + ", " + position + ", " + scale;
const parts = [`output = ${luaQuoted(identifier)}`, `mode = ${luaQuoted(resolution)}`, `position = ${luaQuoted(position)}`, `scale = ${Number(scale)}`];
const transform = transformToHyprland(output.logical?.transform ?? "Normal");
if (transform !== 0)
monitorLine += ", transform, " + transform;
parts.push(`transform = ${transform}`);
if (output.vrr_supported) {
const vrrMode = outputSettings.vrrFullscreenOnly ? 2 : (output.vrr_enabled ? 1 : 0);
monitorLine += ", vrr, " + vrrMode;
parts.push(`vrr = ${vrrMode}`);
}
if (output.mirror && output.mirror.length > 0)
monitorLine += ", mirror, " + output.mirror;
parts.push(`mirror = ${luaQuoted(output.mirror)}`);
if (outputSettings.bitdepth && outputSettings.bitdepth !== 8)
monitorLine += ", bitdepth, " + outputSettings.bitdepth;
parts.push(`bitdepth = ${Number(outputSettings.bitdepth)}`);
if (outputSettings.colorManagement && outputSettings.colorManagement !== "auto")
monitorLine += ", cm, " + outputSettings.colorManagement;
parts.push(`cm = ${luaQuoted(outputSettings.colorManagement)}`);
if (outputSettings.sdrBrightness !== undefined && outputSettings.sdrBrightness !== 1.0)
monitorLine += ", sdrbrightness, " + outputSettings.sdrBrightness;
parts.push(`sdrbrightness = ${Number(outputSettings.sdrBrightness)}`);
if (outputSettings.sdrSaturation !== undefined && outputSettings.sdrSaturation !== 1.0)
monitorLine += ", sdrsaturation, " + outputSettings.sdrSaturation;
parts.push(`sdrsaturation = ${Number(outputSettings.sdrSaturation)}`);
lines.push(monitorLine);
if (outputSettings.supportsWideColor !== undefined)
parts.push(`supports_wide_color = ${forceFlagValue(outputSettings.supportsWideColor)}`);
const needsMonitorv2 = outputSettings.supportsHdr || outputSettings.supportsWideColor || outputSettings.sdrMinLuminance !== undefined || outputSettings.sdrMaxLuminance !== undefined || outputSettings.minLuminance !== undefined || outputSettings.maxLuminance !== undefined || outputSettings.maxAvgLuminance !== undefined;
if (outputSettings.supportsHdr !== undefined)
parts.push(`supports_hdr = ${forceFlagValue(outputSettings.supportsHdr)}`);
if (needsMonitorv2) {
let block = "monitorv2 {\n";
block += " output = " + identifier + "\n";
if (outputSettings.supportsWideColor)
block += " supports_wide_color = true\n";
if (outputSettings.supportsHdr)
block += " supports_hdr = true\n";
if (outputSettings.sdrMinLuminance !== undefined)
block += " sdr_min_luminance = " + outputSettings.sdrMinLuminance + "\n";
if (outputSettings.sdrMaxLuminance !== undefined)
block += " sdr_max_luminance = " + outputSettings.sdrMaxLuminance + "\n";
if (outputSettings.minLuminance !== undefined)
block += " min_luminance = " + outputSettings.minLuminance + "\n";
if (outputSettings.maxLuminance !== undefined)
block += " max_luminance = " + outputSettings.maxLuminance + "\n";
if (outputSettings.maxAvgLuminance !== undefined)
block += " max_avg_luminance = " + outputSettings.maxAvgLuminance + "\n";
block += "}";
monitorv2Blocks.push(block);
}
}
if (monitorv2Blocks.length > 0) {
lines.push("");
for (const block of monitorv2Blocks)
lines.push(block);
lines.push("hl.monitor({ " + parts.join(", ") + " })");
}
lines.push("");
const content = lines.join("\n");
Proc.runCommand("hypr-write-outputs", ["sh", "-c", `mkdir -p "${hyprDmsDir}" && cat > "${outputsPath}" << 'EOF'\n${content}EOF`], (output, exitCode) => {
@@ -196,17 +181,18 @@ Singleton {
const gaps = (typeof SettingsData !== "undefined" && SettingsData.hyprlandLayoutGapsOverride >= 0) ? SettingsData.hyprlandLayoutGapsOverride : defaultGaps;
const borderSize = (typeof SettingsData !== "undefined" && SettingsData.hyprlandLayoutBorderSize >= 0) ? SettingsData.hyprlandLayoutBorderSize : defaultBorderSize;
let content = `# Auto-generated by DMS - do not edit manually
let content = `-- Auto-generated by DMS do not edit manually
general {
gaps_in = ${gaps}
gaps_out = ${gaps}
border_size = ${borderSize}
}
decoration {
rounding = ${cornerRadius}
}
hl.config({
general = {
gaps_in = ${gaps},
gaps_out = ${gaps},
border_size = ${borderSize},
},
decoration = {
rounding = ${cornerRadius},
},
})
`;
Proc.runCommand("hypr-write-layout", ["sh", "-c", `mkdir -p "${hyprDmsDir}" && cat > "${layoutPath}" << 'EOF'\n${content}EOF`], (output, exitCode) => {
@@ -271,7 +257,7 @@ decoration {
const settings = typeof SettingsData !== "undefined" ? SettingsData.cursorSettings : null;
if (!settings) {
Proc.runCommand("hypr-write-cursor", ["sh", "-c", `mkdir -p "${hyprDmsDir}" && : > "${cursorPath}"`], (output, exitCode) => {
Proc.runCommand("hypr-write-cursor", ["sh", "-c", `mkdir -p "${hyprDmsDir}" && printf '%s\\n' "-- Auto-generated by DMS — do not edit manually" "" > "${cursorPath}"`], (output, exitCode) => {
if (exitCode !== 0)
log.warn("Failed to write cursor config:", output);
});
@@ -289,32 +275,34 @@ decoration {
const hasCursorSettings = hideOnKeyPress || hideOnTouch || inactiveTimeout > 0;
if (!hasTheme && !hasNonDefaultSize && !hasCursorSettings) {
Proc.runCommand("hypr-write-cursor", ["sh", "-c", `mkdir -p "${hyprDmsDir}" && : > "${cursorPath}"`], (output, exitCode) => {
Proc.runCommand("hypr-write-cursor", ["sh", "-c", `mkdir -p "${hyprDmsDir}" && printf '%s\\n' "-- Auto-generated by DMS — do not edit manually" "" > "${cursorPath}"`], (output, exitCode) => {
if (exitCode !== 0)
log.warn("Failed to write cursor config:", output);
});
return;
}
let lines = ["# Auto-generated by DMS - do not edit manually", ""];
let lines = ["-- Auto-generated by DMS do not edit manually", ""];
if (hasTheme) {
lines.push(`env = HYPRCURSOR_THEME,${themeName}`);
lines.push(`env = XCURSOR_THEME,${themeName}`);
lines.push(`hl.env("HYPRCURSOR_THEME", ${luaQuoted(themeName)})`);
lines.push(`hl.env("XCURSOR_THEME", ${luaQuoted(themeName)})`);
}
lines.push(`env = HYPRCURSOR_SIZE,${size}`);
lines.push(`env = XCURSOR_SIZE,${size}`);
lines.push(`hl.env("HYPRCURSOR_SIZE", ${luaQuoted(String(size))})`);
lines.push(`hl.env("XCURSOR_SIZE", ${luaQuoted(String(size))})`);
if (hasCursorSettings) {
lines.push("");
lines.push("cursor {");
lines.push("hl.config({");
lines.push("\tcursor = {");
if (hideOnKeyPress)
lines.push(" hide_on_key_press = true");
lines.push("\t\thide_on_key_press = true,");
if (hideOnTouch)
lines.push(" hide_on_touch = true");
lines.push("\t\thide_on_touch = true,");
if (inactiveTimeout > 0)
lines.push(` inactive_timeout = ${inactiveTimeout}`);
lines.push("}");
lines.push(`\t\tinactive_timeout = ${inactiveTimeout},`);
lines.push("\t},");
lines.push("})");
}
lines.push("");
+58 -8
View File
@@ -7,6 +7,7 @@ import Quickshell
import Quickshell.Io
import qs.Common
import qs.Services
import "../Common/ConfigIncludeResolve.js" as ConfigIncludeResolve
import "../Common/KeybindActions.js" as Actions
Singleton {
@@ -82,6 +83,7 @@ Singleton {
case "niri":
return compositorConfigDir + "/dms/binds.kdl";
case "hyprland":
return compositorConfigDir + "/dms/binds.lua";
case "mangowc":
return compositorConfigDir + "/dms/binds.conf";
default:
@@ -93,7 +95,7 @@ Singleton {
case "niri":
return compositorConfigDir + "/config.kdl";
case "hyprland":
return compositorConfigDir + "/hyprland.conf";
return compositorConfigDir + "/hyprland.lua";
case "mangowc":
return compositorConfigDir + "/config.conf";
default:
@@ -247,8 +249,8 @@ Singleton {
root.lastError = "";
root.dmsBindsIncluded = true;
root.dmsBindsFixed();
const bindsFile = root.currentProvider === "niri" ? "dms/binds.kdl" : "dms/binds.conf";
ToastService.showInfo(I18n.tr("Binds include added"), I18n.tr("%1 is now included in config").arg(bindsFile), "", "keybinds");
const bindsRel = root.currentProvider === "niri" ? "dms/binds.kdl" : root.currentProvider === "hyprland" ? "dms/binds.lua" : "dms/binds.conf";
ToastService.showInfo(I18n.tr("Binds include added"), I18n.tr("%1 is now included in config").arg(bindsRel), "", "keybinds");
Qt.callLater(root.forceReload);
}
}
@@ -262,13 +264,36 @@ Singleton {
let script;
switch (currentProvider) {
case "niri":
script = `mkdir -p "${compositorConfigDir}/dms" && touch "${compositorConfigDir}/dms/binds.kdl" && cp "${mainConfigPath}" "${backupPath}" && echo 'include "dms/binds.kdl"' >> "${mainConfigPath}"`;
script = ConfigIncludeResolve.buildRepairScript({
configFile: mainConfigPath,
backupFile: backupPath,
fragmentFile: compositorConfigDir + "/dms/binds.kdl",
grepPattern: 'include.*"dms/binds.kdl"',
includeLine: 'include "dms/binds.kdl"'
});
break;
case "hyprland":
script = `mkdir -p "${compositorConfigDir}/dms" && touch "${compositorConfigDir}/dms/binds.conf" && cp "${mainConfigPath}" "${backupPath}" && echo 'source = ./dms/binds.conf' >> "${mainConfigPath}"`;
script = ConfigIncludeResolve.buildRepairScript({
configFile: mainConfigPath,
backupFile: backupPath,
fragmentFiles: [compositorConfigDir + "/dms/binds.lua", compositorConfigDir + "/dms/binds-user.lua"],
includes: [{
grepPattern: "dms.binds",
includeLine: "require(\"dms.binds\")"
}, {
grepPattern: "dms.binds-user",
includeLine: "require(\"dms.binds-user\")"
}]
});
break;
case "mangowc":
script = `mkdir -p "${compositorConfigDir}/dms" && touch "${compositorConfigDir}/dms/binds.conf" && cp "${mainConfigPath}" "${backupPath}" && echo 'source = ./dms/binds.conf' >> "${mainConfigPath}"`;
script = ConfigIncludeResolve.buildRepairScript({
configFile: mainConfigPath,
backupFile: backupPath,
fragmentFile: compositorConfigDir + "/dms/binds.conf",
grepPattern: "source.*dms/binds.conf",
includeLine: "source = ./dms/binds.conf"
});
break;
default:
fixing = false;
@@ -321,6 +346,7 @@ Singleton {
"statusMessage": status.statusMessage ?? ""
};
}
_maybeWarnHyprlandLegacyConf();
if (!_rawData?.binds) {
_allBinds = {};
@@ -365,10 +391,13 @@ Singleton {
for (var i = 0; i < binds.length; i++) {
const bind = binds[i];
const action = bind.action || "";
const sourceStr = bind.source || "config";
const keyData = {
"key": bind.key || "",
"source": bind.source || "config",
"isOverride": bind.source === "dms",
"source": sourceStr,
"isOverride": sourceStr === "dms",
"isDMSManaged": sourceStr === "dms" || sourceStr === "dms-default",
"hasDefault": bind.hasDefault === true,
"cooldownMs": bind.cooldownMs || 0,
"flags": bind.flags || "",
"allowWhenLocked": bind.allowWhenLocked || false,
@@ -456,6 +485,19 @@ Singleton {
_pendingSavedKey = bindData.key;
}
property bool _hyprlandLegacyWarnShown: false
function _maybeWarnHyprlandLegacyConf() {
if (_hyprlandLegacyWarnShown)
return;
if (currentProvider !== "hyprland")
return;
if (!dmsStatus.exists || dmsStatus.included)
return;
_hyprlandLegacyWarnShown = true;
ToastService.showWarning(I18n.tr("Hyprland config still uses hyprlang"), I18n.tr("DMS Settings now writes Lua. Edits won't apply until you migrate."), "dms setup", "hyprland-migration");
}
function removeBind(key) {
if (!key)
return;
@@ -464,6 +506,14 @@ Singleton {
bindRemoved(key);
}
function resetBind(key) {
if (!key)
return;
removeProcess.command = ["dms", "keybinds", "reset", currentProvider, key];
removeProcess.running = true;
bindRemoved(key);
}
function isDmsAction(action) {
return Actions.isDmsAction(action);
}
+18 -4
View File
@@ -66,13 +66,12 @@ Item {
signal toggleExpand
signal saveBind(string originalKey, var newData)
signal removeBind(string key)
signal resetBind(string key)
signal cancelEdit
implicitHeight: contentColumn.implicitHeight
height: implicitHeight
Component.onDestruction: _destroyShortcutInhibitor()
Component.onCompleted: {
if (isNew && isExpanded)
resetEdits();
@@ -831,9 +830,12 @@ Item {
color: root._actionType === modelData.id ? Theme.surfaceContainerHighest : Theme.surfaceContainer
border.color: root._actionType === modelData.id ? Theme.outline : (typeArea.containsMouse ? Theme.outlineVariant : "transparent")
border.width: 1
clip: true
RowLayout {
anchors.centerIn: parent
anchors.fill: parent
anchors.leftMargin: Theme.spacingS
anchors.rightMargin: Theme.spacingS
spacing: Theme.spacingXS
DankIcon {
@@ -843,10 +845,13 @@ Item {
}
StyledText {
Layout.fillWidth: true
text: typeDelegate.modelData.label
font.pixelSize: Theme.fontSizeSmall
color: root._actionType === typeDelegate.modelData.id ? Theme.surfaceText : Theme.surfaceVariantText
visible: typeDelegate.width > 100
elide: Text.ElideRight
horizontalAlignment: Text.AlignHCenter
}
}
@@ -1763,10 +1768,19 @@ Item {
iconName: "delete"
iconSize: Theme.iconSize - 4
iconColor: Theme.error
visible: root.editingKeyIndex >= 0 && root.editingKeyIndex < root.keys.length && root.keys[root.editingKeyIndex].isOverride && !root.isNew
visible: root.editingKeyIndex >= 0 && root.editingKeyIndex < root.keys.length && (root.keys[root.editingKeyIndex].isDMSManaged || root.keys[root.editingKeyIndex].isOverride) && !root.isNew
onClicked: root.removeBind(root._originalKey)
}
DankButton {
text: I18n.tr("Reset to default")
buttonHeight: root._buttonHeight
backgroundColor: Theme.surfaceContainer
textColor: Theme.primary
visible: root.editingKeyIndex >= 0 && root.editingKeyIndex < root.keys.length && root.keys[root.editingKeyIndex].isOverride === true && root.keys[root.editingKeyIndex].hasDefault === true && !root.isNew
onClicked: root.resetBind(root._originalKey)
}
Item {
Layout.fillWidth: true
}
+2 -2
View File
@@ -1,3 +1,3 @@
[templates.dmshyprland]
input_path = 'SHELL_DIR/matugen/templates/hypr-colors.conf'
output_path = 'CONFIG_DIR/hypr/dms/colors.conf'
input_path = 'SHELL_DIR/matugen/templates/hypr-colors.lua'
output_path = 'CONFIG_DIR/hypr/dms/colors.lua'
@@ -1,25 +0,0 @@
# Auto-generated by DMS - do not edit manually
# Remove source = ./dms/colors.conf from your config to override.
$primary = rgb({{colors.primary.default.hex_stripped}})
$outline = rgb({{colors.outline.default.hex_stripped}})
$error = rgb({{colors.error.default.hex_stripped}})
general {
col.active_border = $primary
col.inactive_border = $outline
}
group {
col.border_active = $primary
col.border_inactive = $outline
col.border_locked_active = $error
col.border_locked_inactive = $outline
groupbar {
col.active = $primary
col.inactive = $outline
col.locked_active = $error
col.locked_inactive = $outline
}
}
@@ -0,0 +1,27 @@
-- Auto-generated by DMS Matugen hook — do not edit manually.
-- Remove require("dms.colors") from hyprland.lua to override.
hl.config({
general = {
col = {
active_border = "rgb({{colors.primary.default.hex_stripped}})",
inactive_border = "rgb({{colors.outline.default.hex_stripped}})",
},
},
group = {
col = {
border_active = "rgb({{colors.primary.default.hex_stripped}})",
border_inactive = "rgb({{colors.outline.default.hex_stripped}})",
border_locked_active = "rgb({{colors.error.default.hex_stripped}})",
border_locked_inactive = "rgb({{colors.outline.default.hex_stripped}})",
},
groupbar = {
col = {
active = "rgb({{colors.primary.default.hex_stripped}})",
inactive = "rgb({{colors.outline.default.hex_stripped}})",
locked_active = "rgb({{colors.error.default.hex_stripped}})",
locked_inactive = "rgb({{colors.outline.default.hex_stripped}})",
},
},
},
})