1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-06-13 06:33:30 -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
+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