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:
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user