mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-06-08 12:13:31 -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:
@@ -259,130 +259,56 @@ func getGhosttyPath() string {
|
||||
func TestMergeHyprlandMonitorSections(t *testing.T) {
|
||||
cd := &ConfigDeployer{}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
newConfig string
|
||||
existingConfig string
|
||||
wantError bool
|
||||
wantContains []string
|
||||
wantNotContains []string
|
||||
}{
|
||||
{
|
||||
name: "no existing monitors",
|
||||
newConfig: `# ==================
|
||||
# MONITOR CONFIG
|
||||
# ==================
|
||||
# monitor = eDP-2, 2560x1600@239.998993, 2560x0, 1, vrr, 1
|
||||
t.Run("no monitors in existing", func(t *testing.T) {
|
||||
tmp := t.TempDir()
|
||||
out, err := cd.mergeHyprlandMonitorSections(`hl.config({})`, `input { kb_layout = us }`, tmp)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, `hl.config({})`, out)
|
||||
_, e := os.Stat(filepath.Join(tmp, "outputs.lua"))
|
||||
assert.True(t, os.IsNotExist(e))
|
||||
})
|
||||
|
||||
# ==================
|
||||
# ENVIRONMENT VARS
|
||||
# ==================
|
||||
env = XDG_CURRENT_DESKTOP,niri`,
|
||||
existingConfig: `# Some other config
|
||||
input {
|
||||
kb_layout = us
|
||||
}`,
|
||||
wantError: false,
|
||||
wantContains: []string{"MONITOR CONFIG", "ENVIRONMENT VARS"},
|
||||
},
|
||||
{
|
||||
name: "merge single monitor",
|
||||
newConfig: `# ==================
|
||||
# MONITOR CONFIG
|
||||
# ==================
|
||||
# monitor = eDP-2, 2560x1600@239.998993, 2560x0, 1, vrr, 1
|
||||
|
||||
# ==================
|
||||
# ENVIRONMENT VARS
|
||||
# ==================`,
|
||||
existingConfig: `# My config
|
||||
monitor = DP-1, 1920x1080@144, 0x0, 1
|
||||
input {
|
||||
kb_layout = us
|
||||
}`,
|
||||
wantError: false,
|
||||
wantContains: []string{
|
||||
"MONITOR CONFIG",
|
||||
"monitor = DP-1, 1920x1080@144, 0x0, 1",
|
||||
"Monitors from existing configuration",
|
||||
},
|
||||
wantNotContains: []string{
|
||||
"monitor = eDP-2", // Example monitor should be removed
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "merge multiple monitors",
|
||||
newConfig: `# ==================
|
||||
# MONITOR CONFIG
|
||||
# ==================
|
||||
# monitor = eDP-2, 2560x1600@239.998993, 2560x0, 1, vrr, 1
|
||||
|
||||
# ==================
|
||||
# ENVIRONMENT VARS
|
||||
# ==================`,
|
||||
existingConfig: `monitor = DP-1, 1920x1080@144, 0x0, 1
|
||||
t.Run("writes outputs lua from hyprlang monitors", func(t *testing.T) {
|
||||
tmp := t.TempDir()
|
||||
existing := `monitor = DP-1, 1920x1080@144, 0x0, 1
|
||||
# monitor = HDMI-A-1, 1920x1080@60, 1920x0, 1
|
||||
monitor = eDP-1, 2560x1440@165, auto, 1.25`,
|
||||
wantError: false,
|
||||
wantContains: []string{
|
||||
"monitor = DP-1",
|
||||
"# monitor = HDMI-A-1", // Commented monitor preserved
|
||||
"monitor = eDP-1",
|
||||
"Monitors from existing configuration",
|
||||
},
|
||||
wantNotContains: []string{
|
||||
"monitor = eDP-2", // Example monitor should be removed
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "preserve commented monitors",
|
||||
newConfig: `# ==================
|
||||
# MONITOR CONFIG
|
||||
# ==================
|
||||
# monitor = eDP-2, 2560x1600@239.998993, 2560x0, 1, vrr, 1
|
||||
monitor = eDP-1, 2560x1440@165, auto, 1.25`
|
||||
out, err := cd.mergeHyprlandMonitorSections(`return`, existing, tmp)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, `return`, out)
|
||||
b, err := os.ReadFile(filepath.Join(tmp, "outputs.lua"))
|
||||
require.NoError(t, err)
|
||||
s := string(b)
|
||||
assert.Contains(t, s, "hl.monitor")
|
||||
assert.Contains(t, s, "DP-1")
|
||||
assert.Contains(t, s, "HDMI-A-1")
|
||||
assert.Contains(t, s, "eDP-1")
|
||||
assert.Contains(t, s, "preferred") // fallback rule at end
|
||||
})
|
||||
|
||||
# ==================`,
|
||||
existingConfig: `# monitor = DP-1, 1920x1080@144, 0x0, 1
|
||||
# monitor = HDMI-A-1, 1920x1080@60, 1920x0, 1`,
|
||||
wantError: false,
|
||||
wantContains: []string{
|
||||
"# monitor = DP-1",
|
||||
"# monitor = HDMI-A-1",
|
||||
"Monitors from existing configuration",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "no monitor config section",
|
||||
newConfig: `# Some config without monitor section
|
||||
input {
|
||||
kb_layout = us
|
||||
}`,
|
||||
existingConfig: `monitor = DP-1, 1920x1080@144, 0x0, 1`,
|
||||
wantError: true,
|
||||
},
|
||||
}
|
||||
t.Run("skips when outputs lua already exists", func(t *testing.T) {
|
||||
tmp := t.TempDir()
|
||||
path := filepath.Join(tmp, "outputs.lua")
|
||||
require.NoError(t, os.WriteFile(path, []byte("-- keep\n"), 0o644))
|
||||
_, err := cd.mergeHyprlandMonitorSections(`x`, `monitor = DP-1, 1920x1080@144, 0x0, 1`, tmp)
|
||||
require.NoError(t, err)
|
||||
b, err := os.ReadFile(path)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "-- keep\n", string(b))
|
||||
})
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
result, err := cd.mergeHyprlandMonitorSections(tt.newConfig, tt.existingConfig, tmpDir)
|
||||
func TestHyprlangMonitorLineToLuaPreservesOptions(t *testing.T) {
|
||||
got, err := hyprlangMonitorLineToLua(`monitor = DP-1, 1920x1080@144, 0x0, 1, transform, 1, vrr, 2, bitdepth, 10, cm, hdr, sdrbrightness, 1.2, sdrsaturation, 0.98`)
|
||||
require.NoError(t, err)
|
||||
|
||||
if tt.wantError {
|
||||
assert.Error(t, err)
|
||||
return
|
||||
}
|
||||
|
||||
require.NoError(t, err)
|
||||
|
||||
for _, want := range tt.wantContains {
|
||||
assert.Contains(t, result, want, "merged config should contain: %s", want)
|
||||
}
|
||||
|
||||
for _, notWant := range tt.wantNotContains {
|
||||
assert.NotContains(t, result, notWant, "merged config should NOT contain: %s", notWant)
|
||||
}
|
||||
})
|
||||
}
|
||||
assert.Contains(t, got, `output = "DP-1"`)
|
||||
assert.Contains(t, got, `transform = 1`)
|
||||
assert.Contains(t, got, `vrr = 2`)
|
||||
assert.Contains(t, got, `bitdepth = 10`)
|
||||
assert.Contains(t, got, `cm = "hdr"`)
|
||||
assert.Contains(t, got, `sdrbrightness = 1.2`)
|
||||
assert.Contains(t, got, `sdrsaturation = 0.98`)
|
||||
}
|
||||
|
||||
func TestHyprlandConfigDeployment(t *testing.T) {
|
||||
@@ -398,6 +324,10 @@ func TestHyprlandConfigDeployment(t *testing.T) {
|
||||
cd := NewConfigDeployer(logChan)
|
||||
|
||||
t.Run("deploy hyprland config to empty directory", func(t *testing.T) {
|
||||
td, err := os.MkdirTemp("", "dankinstall-hyprland-empty")
|
||||
require.NoError(t, err)
|
||||
defer os.RemoveAll(td)
|
||||
os.Setenv("HOME", td)
|
||||
result, err := cd.deployHyprlandConfig(deps.TerminalGhostty, true)
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -408,12 +338,16 @@ func TestHyprlandConfigDeployment(t *testing.T) {
|
||||
|
||||
content, err := os.ReadFile(result.Path)
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, string(content), "# MONITOR CONFIG")
|
||||
assert.Contains(t, string(content), "source = ./dms/binds.conf")
|
||||
assert.Contains(t, string(content), "exec-once = ")
|
||||
assert.Contains(t, string(content), `require("dms.binds")`)
|
||||
assert.Contains(t, string(content), "DMS_STARTUP_BEGIN")
|
||||
assert.Contains(t, string(content), "hl.config(")
|
||||
})
|
||||
|
||||
t.Run("deploy hyprland config with existing monitors", func(t *testing.T) {
|
||||
td, err := os.MkdirTemp("", "dankinstall-hyprland-merge")
|
||||
require.NoError(t, err)
|
||||
defer os.RemoveAll(td)
|
||||
os.Setenv("HOME", td)
|
||||
existingContent := `# My existing Hyprland config
|
||||
monitor = DP-1, 1920x1080@144, 0x0, 1
|
||||
monitor = HDMI-A-1, 3840x2160@60, 1920x0, 1.5
|
||||
@@ -422,11 +356,17 @@ general {
|
||||
gaps_in = 10
|
||||
}
|
||||
`
|
||||
hyprPath := filepath.Join(tempDir, ".config", "hypr", "hyprland.conf")
|
||||
err := os.MkdirAll(filepath.Dir(hyprPath), 0o755)
|
||||
hyprPath := filepath.Join(td, ".config", "hypr", "hyprland.conf")
|
||||
err = os.MkdirAll(filepath.Dir(hyprPath), 0o755)
|
||||
require.NoError(t, err)
|
||||
err = os.WriteFile(hyprPath, []byte(existingContent), 0o644)
|
||||
require.NoError(t, err)
|
||||
dmsDir := filepath.Join(td, ".config", "hypr", "dms")
|
||||
require.NoError(t, os.MkdirAll(dmsDir, 0o755))
|
||||
require.NoError(t, os.WriteFile(filepath.Join(dmsDir, "binds.conf"), []byte("bind = SUPER, T, exec, foot\n"), 0o644))
|
||||
require.NoError(t, os.WriteFile(filepath.Join(dmsDir, "cursor.conf"), []byte("env = XCURSOR_SIZE,24\n"), 0o644))
|
||||
require.NoError(t, os.WriteFile(filepath.Join(filepath.Dir(hyprPath), "hyprland.conf.backup.old"), []byte("old backup\n"), 0o644))
|
||||
require.NoError(t, os.WriteFile(filepath.Join(dmsDir, "binds.conf.backup.old"), []byte("old dms backup\n"), 0o644))
|
||||
|
||||
result, err := cd.deployHyprlandConfig(deps.TerminalKitty, true)
|
||||
require.NoError(t, err)
|
||||
@@ -440,13 +380,76 @@ general {
|
||||
backupContent, err := os.ReadFile(result.BackupPath)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, existingContent, string(backupContent))
|
||||
assert.Contains(t, result.BackupPath, hyprlandBackupDirName)
|
||||
assert.NoFileExists(t, hyprPath)
|
||||
assert.FileExists(t, filepath.Join(filepath.Dir(result.BackupPath), "dms", "binds.conf"))
|
||||
assert.FileExists(t, filepath.Join(filepath.Dir(result.BackupPath), "dms", "cursor.conf"))
|
||||
assert.FileExists(t, filepath.Join(filepath.Dir(result.BackupPath), "hyprland.conf.backup.old"))
|
||||
assert.FileExists(t, filepath.Join(filepath.Dir(result.BackupPath), "dms", "binds.conf.backup.old"))
|
||||
assert.NoFileExists(t, filepath.Join(dmsDir, "binds.conf"))
|
||||
assert.NoFileExists(t, filepath.Join(dmsDir, "cursor.conf"))
|
||||
assert.NoFileExists(t, filepath.Join(filepath.Dir(hyprPath), "hyprland.conf.backup.old"))
|
||||
assert.NoFileExists(t, filepath.Join(dmsDir, "binds.conf.backup.old"))
|
||||
|
||||
newContent, err := os.ReadFile(result.Path)
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, string(newContent), "monitor = DP-1, 1920x1080@144")
|
||||
assert.Contains(t, string(newContent), "monitor = HDMI-A-1, 3840x2160@60")
|
||||
assert.Contains(t, string(newContent), "source = ./dms/binds.conf")
|
||||
assert.NotContains(t, string(newContent), "monitor = eDP-2")
|
||||
assert.Contains(t, string(newContent), `require("dms.binds")`)
|
||||
|
||||
outputsPath := filepath.Join(td, ".config", "hypr", "dms", "outputs.lua")
|
||||
outBytes, err := os.ReadFile(outputsPath)
|
||||
require.NoError(t, err)
|
||||
outs := string(outBytes)
|
||||
assert.Contains(t, outs, `hl.monitor`)
|
||||
assert.Contains(t, outs, "DP-1")
|
||||
assert.Contains(t, outs, "HDMI-A-1")
|
||||
})
|
||||
|
||||
t.Run("deploy hyprland config removes root legacy symlink when lua exists", func(t *testing.T) {
|
||||
td, err := os.MkdirTemp("", "dankinstall-hyprland-lua-conf-symlink")
|
||||
require.NoError(t, err)
|
||||
defer os.RemoveAll(td)
|
||||
os.Setenv("HOME", td)
|
||||
|
||||
configDir := filepath.Join(td, ".config", "hypr")
|
||||
require.NoError(t, os.MkdirAll(configDir, 0o755))
|
||||
luaPath := filepath.Join(configDir, "hyprland.lua")
|
||||
confPath := filepath.Join(configDir, "hyprland.conf")
|
||||
require.NoError(t, os.WriteFile(luaPath, []byte(`require("dms.binds")`+"\n"), 0o644))
|
||||
require.NoError(t, os.Symlink(filepath.Join(configDir, "missing-legacy.conf"), confPath))
|
||||
|
||||
result, err := cd.deployHyprlandConfig(deps.TerminalKitty, true)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, luaPath, result.Path)
|
||||
_, err = os.Lstat(confPath)
|
||||
assert.True(t, os.IsNotExist(err), "root hyprland.conf symlink should be moved out of the live config directory")
|
||||
_, err = os.Lstat(filepath.Join(filepath.Dir(result.BackupPath), "hyprland.conf"))
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("deploy hyprland config refreshes managed binds but preserves user binds", func(t *testing.T) {
|
||||
td, err := os.MkdirTemp("", "dankinstall-hyprland-refresh-binds")
|
||||
require.NoError(t, err)
|
||||
defer os.RemoveAll(td)
|
||||
os.Setenv("HOME", td)
|
||||
|
||||
dmsDir := filepath.Join(td, ".config", "hypr", "dms")
|
||||
require.NoError(t, os.MkdirAll(dmsDir, 0o755))
|
||||
require.NoError(t, os.WriteFile(filepath.Join(dmsDir, "binds.lua"), []byte("-- stale managed binds\n"), 0o644))
|
||||
userBinds := "-- custom user binds\n"
|
||||
require.NoError(t, os.WriteFile(filepath.Join(dmsDir, "binds-user.lua"), []byte(userBinds), 0o644))
|
||||
|
||||
_, err = cd.deployHyprlandConfig(deps.TerminalKitty, true)
|
||||
require.NoError(t, err)
|
||||
|
||||
managed, err := os.ReadFile(filepath.Join(dmsDir, "binds.lua"))
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, string(managed), `hl.bind("SUPER + F", hl.dsp.window.fullscreen({ mode = "maximized", action = "toggle" }))`)
|
||||
assert.Contains(t, string(managed), `hl.bind("SUPER + minus", hl.dsp.exec_cmd([[hyprctl dispatch resizeactive -10% 0]]), { repeating = true })`)
|
||||
|
||||
user, err := os.ReadFile(filepath.Join(dmsDir, "binds-user.lua"))
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, userBinds, string(user))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -459,10 +462,10 @@ func TestNiriConfigStructure(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestHyprlandConfigStructure(t *testing.T) {
|
||||
assert.Contains(t, HyprlandConfig, "# MONITOR CONFIG")
|
||||
assert.Contains(t, HyprlandConfig, "# STARTUP APPS")
|
||||
assert.Contains(t, HyprlandConfig, "# INPUT CONFIG")
|
||||
assert.Contains(t, HyprlandConfig, "source = ./dms/binds.conf")
|
||||
assert.Contains(t, HyprlandLuaConfig, `require("dms.binds")`)
|
||||
assert.Contains(t, HyprlandLuaConfig, "DMS_STARTUP_BEGIN")
|
||||
assert.Contains(t, HyprlandLuaConfig, "hl.config(")
|
||||
assert.Contains(t, HyprlandLuaConfig, "input =")
|
||||
}
|
||||
|
||||
func TestGhosttyConfigStructure(t *testing.T) {
|
||||
@@ -789,4 +792,37 @@ func TestShouldReplaceConfigDeployIfMissing(t *testing.T) {
|
||||
}
|
||||
assert.True(t, foundGhostty, "expected Ghostty config to be deployed when replaceConfigs is true")
|
||||
})
|
||||
|
||||
t.Run("hyprland legacy config exists skips when replace false", func(t *testing.T) {
|
||||
tempDir, err := os.MkdirTemp("", "dankinstall-hyprland-legacy-skip-test")
|
||||
require.NoError(t, err)
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
originalHome := os.Getenv("HOME")
|
||||
os.Setenv("HOME", tempDir)
|
||||
defer os.Setenv("HOME", originalHome)
|
||||
|
||||
hyprConf := filepath.Join(tempDir, ".config", "hypr", "hyprland.conf")
|
||||
require.NoError(t, os.MkdirAll(filepath.Dir(hyprConf), 0o755))
|
||||
require.NoError(t, os.WriteFile(hyprConf, []byte("monitor = , preferred, auto, 1\n"), 0o644))
|
||||
|
||||
logChan := make(chan string, 100)
|
||||
cd := NewConfigDeployer(logChan)
|
||||
results, err := cd.deployConfigurationsInternal(
|
||||
context.Background(),
|
||||
deps.WindowManagerHyprland,
|
||||
deps.TerminalGhostty,
|
||||
nil,
|
||||
allFalse,
|
||||
nil,
|
||||
true,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
for _, r := range results {
|
||||
if r.ConfigType == "Hyprland" && r.Deployed {
|
||||
t.Fatalf("expected Hyprland deployment to be skipped when legacy config exists and replace=false")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user